Merge pull request #2641 from Infisical/feat/ldap-static-dynamic-secret

feat: static ldap credentials
This commit is contained in:
Sheen
2024-11-10 13:01:56 +08:00
committed by GitHub
6 changed files with 564 additions and 210 deletions
@@ -7,7 +7,7 @@ import { z } from "zod";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models"; import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => { const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
@@ -193,6 +193,28 @@ export const LdapProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs); const client = await getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
} else {
const username = generateUsername(); const username = generateUsername();
const password = generatePassword(); const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif }); const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
@@ -208,14 +230,39 @@ export const LdapProvider = (): TDynamicProviderFns => {
} }
throw new BadRequestError({ message: (err as Error).message }); throw new BadRequestError({ message: (err as Error).message });
} }
}
}; };
const revoke = async (inputs: unknown, entityId: string) => { const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs); const client = await getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
}
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif }); const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
await executeLdif(connection, revocationLdif); await executeLdif(client, revocationLdif);
return { entityId }; return { entityId };
}; };
@@ -12,6 +12,11 @@ export enum ElasticSearchAuthTypes {
ApiKey = "api-key" ApiKey = "api-key"
} }
export enum LdapCredentialType {
Dynamic = "dynamic",
Static = "static"
}
export const DynamicSecretRedisDBSchema = z.object({ export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(), host: z.string().trim().toLowerCase(),
port: z.number(), port: z.number(),
@@ -195,16 +200,26 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1) clientSecret: z.string().trim().min(1)
}); });
export const LdapSchema = z.object({ export const LdapSchema = z.union([
z.object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
binddn: z.string().trim().min(1), binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Dynamic).optional().default(LdapCredentialType.Dynamic),
creationLdif: z.string().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional() rollbackLdif: z.string().optional()
}); }),
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Static),
rotationLdif: z.string().min(1)
})
]);
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
@@ -10,9 +10,11 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
1. Create a user with the necessary permissions to create users in your LDAP server. 1. Create a user with the necessary permissions to create users in your LDAP server.
2. Ensure your LDAP server is reachable via Infisical instance. 2. Ensure your LDAP server is reachable via Infisical instance.
## Set up Dynamic Secrets with LDAP ## Create LDAP Credentials
<Steps> <Tabs>
<Tab title="Dynamic">
<Steps>
<Step title="Open Secret Overview Dashboard"> <Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step> </Step>
@@ -52,6 +54,10 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
CA certificate to use for TLS in case of a secure connection. CA certificate to use for TLS in case of a secure connection.
</ParamField> </ParamField>
<ParamField path="Credential Type" type="enum">
The type of LDAP credential - select Dynamic.
</ParamField>
<ParamField path="Creation LDIF" type="text" required> <ParamField path="Creation LDIF" type="text" required>
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions. LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret. Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
@@ -117,8 +123,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
changetype: delete changetype: delete
``` ```
</ParamField> </ParamField>
</Step>
</Step>
<Step title="Click `Submit`"> <Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard. After submitting the form, you will see a dynamic secret created in the dashboard.
@@ -143,10 +148,115 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF. Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png) ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step> </Step>
</Steps> </Steps>
</Tab>
<Tab title="Static">
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="URL" type="string" required>
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
</ParamField>
<ParamField path="BIND DN" type="string" required>
DN to bind to. This should have permissions to create a new users.
</ParamField>
<ParamField path="BIND Password" type="string" required>
Password for the given DN.
</ParamField>
<ParamField path="CA" type="text">
CA certificate to use for TLS in case of a secure connection.
</ParamField>
<ParamField path="Credential Type" type="enum">
The type of LDAP credential - select Static.
</ParamField>
<ParamField path="Rotation LDIF" type="text" required>
LDIF to run for rotating the credentals of an LDAP user. This can include extra LDAP steps based on your needs.
Here `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the password generated by the dynamic secret.
Note that the `-` characters and the empty lines found at the end of the examples are necessary based on the LDIF format.
**OpenLDAP** Example:
```
dn: cn=sheencaps capadngan,ou=people,dc=acme,dc=com
changetype: modify
replace: userPassword
password: {{Password}}
-
```
**Active Directory** Example:
```
dn: cn=sheencaps capadngan,ou=people,dc=acme,dc=com
changetype: modify
replace: unicodePwd
unicodePwd::{{EncodedPassword}}
-
```
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
</ParamField>
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step>
</Steps>
</Tab>
</Tabs>
## Active Directory Integration ## Active Directory Integration
@@ -199,9 +199,11 @@ export type TDynamicSecretProvider =
binddn: string; binddn: string;
bindpass: string; bindpass: string;
ca?: string | undefined; ca?: string | undefined;
creationLdif: string; credentialType: string;
revocationLdif: string; creationLdif?: string;
revocationLdif?: string;
rollbackLdif?: string; rollbackLdif?: string;
rotationLdif?: string;
}; };
} }
| { | {
@@ -8,21 +8,47 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api"; import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
enum CredentialType {
Dynamic = "dynamic",
Static = "static"
}
const credentialTypes = [
{
label: "Dynamic",
value: CredentialType.Dynamic
},
{
label: "Static",
value: CredentialType.Static
}
] as const;
const formSchema = z.object({ const formSchema = z.object({
provider: z.object({ provider: z.discriminatedUnion("credentialType", [
z.object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
binddn: z.string().trim().min(1), binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
credentialType: z.literal(CredentialType.Dynamic),
creationLdif: z.string().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional() rollbackLdif: z.string().optional()
}), }),
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(CredentialType.Static),
rotationLdif: z.string().min(1)
})
]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
@@ -67,6 +93,8 @@ export const LdapInputForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
setValue,
watch,
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@@ -78,11 +106,14 @@ export const LdapInputForm = ({
ca: "", ca: "",
creationLdif: "", creationLdif: "",
revocationLdif: "", revocationLdif: "",
rollbackLdif: "" rollbackLdif: "",
credentialType: CredentialType.Dynamic
} }
} }
}); });
const selectedCredentialType = watch("provider.credentialType");
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
@@ -238,6 +269,50 @@ export const LdapInputForm = ({
)} )}
/> />
<Controller
control={control}
name="provider.credentialType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Credential Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(e) => {
const ldifFields = [
"provider.creationLdif",
"provider.revocationLdif",
"provider.rollbackLdif",
"provider.rotationLdif"
] as const;
ldifFields.forEach((f) => {
setValue(f, "");
});
field.onChange(e);
}}
>
{credentialTypes.map((credentialType) => (
<SelectItem
value={credentialType.value}
key={`credential-type-${credentialType.value}`}
>
{credentialType.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{selectedCredentialType === CredentialType.Dynamic && (
<>
<Controller <Controller
control={control} control={control}
name="provider.creationLdif" name="provider.creationLdif"
@@ -279,6 +354,23 @@ export const LdapInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
</>
)}
{selectedCredentialType === CredentialType.Static && (
<Controller
control={control}
name="provider.rotationLdif"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rotation LDIF"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<TextArea {...field} />
</FormControl>
)}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -5,22 +5,47 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api"; import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
enum CredentialType {
Dynamic = "dynamic",
Static = "static"
}
const credentialTypes = [
{
label: "Dynamic",
value: CredentialType.Dynamic
},
{
label: "Static",
value: CredentialType.Static
}
] as const;
const formSchema = z.object({ const formSchema = z.object({
inputs: z inputs: z.discriminatedUnion("credentialType", [
.object({ z.object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
binddn: z.string().trim().min(1), binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
credentialType: z.literal(CredentialType.Dynamic),
creationLdif: z.string().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional() rollbackLdif: z.string().optional()
}),
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(CredentialType.Static),
rotationLdif: z.string().min(1)
}) })
.partial(), ]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -67,7 +92,9 @@ export const EditDynamicSecretLdapForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit,
watch,
setValue
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
values: { values: {
@@ -81,6 +108,7 @@ export const EditDynamicSecretLdapForm = ({
}); });
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const selectedCredentialType = watch("inputs.credentialType");
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished // wait till previous request is finished
@@ -202,6 +230,49 @@ export const EditDynamicSecretLdapForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="inputs.credentialType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Credential Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(e) => {
const ldifFields = [
"inputs.creationLdif",
"inputs.revocationLdif",
"inputs.rollbackLdif",
"inputs.rotationLdif"
] as const;
ldifFields.forEach((f) => {
setValue(f, "");
});
field.onChange(e);
}}
>
{credentialTypes.map((credentialType) => (
<SelectItem
value={credentialType.value}
key={`credential-type-${credentialType.value}`}
>
{credentialType.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{selectedCredentialType === CredentialType.Dynamic && (
<>
<Controller <Controller
control={control} control={control}
name="inputs.creationLdif" name="inputs.creationLdif"
@@ -241,6 +312,23 @@ export const EditDynamicSecretLdapForm = ({
</FormControl> </FormControl>
)} )}
/> />
</>
)}
{selectedCredentialType === CredentialType.Static && (
<Controller
control={control}
name="inputs.rotationLdif"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rotation LDIF"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<TextArea {...field} />
</FormControl>
)}
/>
)}
</div> </div>
<div className="mt-4 flex items-center space-x-4"> <div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}> <Button type="submit" isLoading={isSubmitting}>