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 { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models";
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
@@ -193,6 +193,28 @@ export const LdapProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
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 password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
@@ -208,14 +230,39 @@ export const LdapProvider = (): TDynamicProviderFns => {
}
throw new BadRequestError({ message: (err as Error).message });
}
}
};
const revoke = async (inputs: unknown, entityId: string) => {
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 });
await executeLdif(connection, revocationLdif);
await executeLdif(client, revocationLdif);
return { entityId };
};
@@ -12,6 +12,11 @@ export enum ElasticSearchAuthTypes {
ApiKey = "api-key"
}
export enum LdapCredentialType {
Dynamic = "dynamic",
Static = "static"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@@ -195,16 +200,26 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1)
});
export const LdapSchema = z.object({
export const LdapSchema = z.union([
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.Dynamic).optional().default(LdapCredentialType.Dynamic),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
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 {
SqlDatabase = "sql-database",
@@ -10,8 +10,10 @@ 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.
2. Ensure your LDAP server is reachable via Infisical instance.
## Set up Dynamic Secrets with LDAP
## Create LDAP Credentials
<Tabs>
<Tab title="Dynamic">
<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.
@@ -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.
</ParamField>
<ParamField path="Credential Type" type="enum">
The type of LDAP credential - select Dynamic.
</ParamField>
<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.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
@@ -117,7 +123,6 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
changetype: delete
```
</ParamField>
</Step>
<Step title="Click `Submit`">
@@ -143,11 +148,116 @@ 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.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step>
</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
- Passwords in Active Directory are set using the `unicodePwd` field. This must be proceeded by two colons `::` as shown in the example. [Source](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password)
@@ -199,9 +199,11 @@ export type TDynamicSecretProvider =
binddn: string;
bindpass: string;
ca?: string | undefined;
creationLdif: string;
revocationLdif: string;
credentialType: string;
creationLdif?: string;
revocationLdif?: string;
rollbackLdif?: string;
rotationLdif?: string;
};
}
| {
@@ -8,21 +8,47 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
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 { 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({
provider: z.object({
provider: z.discriminatedUnion("credentialType", [
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.Dynamic),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
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) => {
const valMs = ms(val);
@@ -67,6 +93,8 @@ export const LdapInputForm = ({
const {
control,
formState: { isSubmitting },
setValue,
watch,
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
@@ -78,11 +106,14 @@ export const LdapInputForm = ({
ca: "",
creationLdif: "",
revocationLdif: "",
rollbackLdif: ""
rollbackLdif: "",
credentialType: CredentialType.Dynamic
}
}
});
const selectedCredentialType = watch("provider.credentialType");
const createDynamicSecret = useCreateDynamicSecret();
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
control={control}
name="provider.creationLdif"
@@ -279,6 +354,23 @@ export const LdapInputForm = ({
</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>
@@ -5,22 +5,47 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
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 { 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({
inputs: z
.object({
inputs: z.discriminatedUnion("credentialType", [
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.Dynamic),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
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) => {
const valMs = ms(val);
if (valMs < 60 * 1000)
@@ -67,7 +92,9 @@ export const EditDynamicSecretLdapForm = ({
const {
control,
formState: { isSubmitting },
handleSubmit
handleSubmit,
watch,
setValue
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
@@ -81,6 +108,7 @@ export const EditDynamicSecretLdapForm = ({
});
const updateDynamicSecret = useUpdateDynamicSecret();
const selectedCredentialType = watch("inputs.credentialType");
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished
@@ -202,6 +230,49 @@ export const EditDynamicSecretLdapForm = ({
</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
control={control}
name="inputs.creationLdif"
@@ -241,6 +312,23 @@ export const EditDynamicSecretLdapForm = ({
</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 className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>