-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0e6aed7
commit 27afad5
Showing
1 changed file
with
201 additions
and
0 deletions.
There are no files selected for viewing
201 changes: 201 additions & 0 deletions
201
backend/src/ee/services/dynamic-secret/providers/ldap.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* eslint-disable */ | ||
|
||
import ldapjs from "ldapjs"; | ||
import { render } from "mustache"; | ||
import { customAlphabet } from "nanoid"; | ||
import { z } from "zod"; | ||
|
||
import { logger } from "@app/lib/logger"; | ||
import { alphaNumericNanoId } from "@app/lib/nanoid"; | ||
|
||
import { LdapSchema, TDynamicProviderFns } from "./models"; | ||
|
||
const ldif = require("ldif"); | ||
|
||
const generatePassword = () => { | ||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; | ||
return customAlphabet(charset, 64)(); | ||
}; | ||
|
||
const generateUsername = () => { | ||
return alphaNumericNanoId(32); | ||
}; | ||
|
||
const generateLDIF = ({ | ||
username, | ||
password, | ||
ldifTemplate | ||
}: { | ||
username: string; | ||
password?: string; | ||
ldifTemplate: string; | ||
}): string => { | ||
const data = { | ||
Username: username, | ||
Password: password | ||
}; | ||
|
||
const ldif = render(ldifTemplate, data); | ||
|
||
return ldif; | ||
}; | ||
|
||
export const LdapProvider = (): TDynamicProviderFns => { | ||
const validateProviderInputs = async (inputs: unknown) => { | ||
const providerInputs = await LdapSchema.parseAsync(inputs); | ||
return providerInputs; | ||
}; | ||
|
||
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => { | ||
return new Promise((resolve) => { | ||
const client = ldapjs.createClient({ | ||
url: providerInputs.url, | ||
tlsOptions: { | ||
ca: providerInputs.ca ? providerInputs.ca : null, | ||
rejectUnauthorized: !!providerInputs.ca | ||
}, | ||
reconnect: true, | ||
bindDN: providerInputs.binddn, | ||
bindCredentials: providerInputs.bindpass, | ||
log: logger | ||
}); | ||
|
||
client.on("error", (err) => { | ||
client.unbind(); | ||
throw new Error(err.message); | ||
}); | ||
|
||
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => { | ||
if (err) { | ||
client.unbind(); | ||
throw new Error(err.message); | ||
} else { | ||
resolve(client); | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
const validateConnection = async (inputs: unknown) => { | ||
const providerInputs = await validateProviderInputs(inputs); | ||
const client = await getClient(providerInputs); | ||
return client.connected; | ||
}; | ||
|
||
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => { | ||
const parsedEntries: any = ldif.parse(ldif_file).entries as any[]; | ||
const dnArray: string[] = []; | ||
|
||
for (const entry of parsedEntries) { | ||
const { dn } = entry; | ||
let response_dn: string; | ||
|
||
if (entry.type === "add") { | ||
const attributes: any = {}; | ||
|
||
entry.changes.forEach((change: any) => { | ||
const attrName = change.attribute.attribute; | ||
const attrValue = change.value.value; | ||
|
||
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue]; | ||
}); | ||
|
||
response_dn = await new Promise((resolve) => { | ||
client.add(dn, attributes, (err) => { | ||
if (err) { | ||
throw new Error(err.message); | ||
} else { | ||
resolve(dn); | ||
} | ||
}); | ||
}); | ||
} else if (entry.type === "modify") { | ||
const changes: any = []; | ||
|
||
entry.changes.forEach((change: any) => { | ||
changes.push( | ||
new ldapjs.Change({ | ||
operation: change.operation || "replace", | ||
modification: { | ||
[change.attribute.attribute]: Array.isArray(change.value.value) | ||
? change.value.value | ||
: [change.value.value] | ||
} | ||
}) | ||
); | ||
}); | ||
|
||
response_dn = await new Promise((resolve) => { | ||
client.modify(dn, changes, (err) => { | ||
if (err) { | ||
throw new Error(err.message); | ||
} else { | ||
resolve(dn); | ||
} | ||
}); | ||
}); | ||
} else if (entry.type === "delete") { | ||
response_dn = await new Promise((resolve) => { | ||
client.del(dn, (err) => { | ||
if (err) { | ||
throw new Error(err.message); | ||
} else { | ||
resolve(dn); | ||
} | ||
}); | ||
}); | ||
} else { | ||
client.unbind(); | ||
throw new Error(`Unsupported operation type ${entry.type}`); | ||
} | ||
|
||
dnArray.push(response_dn); | ||
} | ||
client.unbind(); | ||
return dnArray; | ||
}; | ||
|
||
const create = async (inputs: unknown) => { | ||
const providerInputs = await validateProviderInputs(inputs); | ||
const client = await getClient(providerInputs); | ||
|
||
const username = generateUsername(); | ||
const password = generatePassword(); | ||
const ldif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif }); | ||
|
||
try { | ||
const dnArray = await executeLdif(client, ldif); | ||
|
||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } }; | ||
} catch (err) { | ||
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif }); | ||
|
||
await executeLdif(client, rollbackLdif); | ||
|
||
throw new Error((err as Error).message); | ||
} | ||
}; | ||
|
||
const revoke = async (inputs: unknown, entityId: string) => { | ||
const providerInputs = await validateProviderInputs(inputs); | ||
const connection = await getClient(providerInputs); | ||
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif }); | ||
|
||
await executeLdif(connection, revocationLdif); | ||
|
||
return { entityId }; | ||
}; | ||
|
||
const renew = async (inputs: unknown, entityId: string) => { | ||
// Do nothing | ||
return { entityId }; | ||
}; | ||
|
||
return { | ||
validateProviderInputs, | ||
validateConnection, | ||
create, | ||
revoke, | ||
renew | ||
}; | ||
}; |