generated from avior/template-web-astro
193
src/models/Adapters/LDAPAdapter.ts
Normal file
193
src/models/Adapters/LDAPAdapter.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { objectClone, objectLoop, objectMap, objectRemap } from '@dzeio/object-util'
|
||||
import ldap from 'ldapjs'
|
||||
import type DaoAdapter from 'models/DaoAdapter'
|
||||
import type { DBPull } from 'models/DaoAdapter'
|
||||
import { type Query } from 'models/Query'
|
||||
import Schema, { type Implementation, type Model } from 'models/Schema'
|
||||
type LDAPFields = 'uid' | 'mail' | 'givenname' | 'sn' | 'jpegPhoto' | 'password'
|
||||
|
||||
export default class LDAPAdapter<T extends Model> implements DaoAdapter<T> {
|
||||
|
||||
private reverseReference: Partial<Record<LDAPFields | string, keyof T>> = {}
|
||||
private attributes: Array<LDAPFields | string> = []
|
||||
|
||||
public constructor(
|
||||
public readonly schema: Schema<T>,
|
||||
public readonly options: {
|
||||
url: string
|
||||
dnSuffix: string
|
||||
adminUsername: string
|
||||
adminPassword: string
|
||||
fieldsCorrespondance?: Partial<Record<keyof T, LDAPFields | string>>
|
||||
}
|
||||
) {
|
||||
objectLoop(options.fieldsCorrespondance ?? {}, (value, key) => {
|
||||
this.reverseReference[value] = key
|
||||
this.attributes.push(value)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: make it clearer what it does
|
||||
public async create(_obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async read(query?: Query<Implementation<T>> | undefined): Promise<DBPull<T>> {
|
||||
const passwordField = this.options.fieldsCorrespondance?.password ?? 'password'
|
||||
const doLogin = !!query?.[passwordField]
|
||||
|
||||
const emptyResult = {
|
||||
rows: 0,
|
||||
rowsTotal: 0,
|
||||
page: 1,
|
||||
pageTotal: 0,
|
||||
data: []
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return emptyResult
|
||||
}
|
||||
|
||||
// console.log(await this.ldapFind({mail: 'f.bouillon@aptatio.com'}))
|
||||
|
||||
const userdn = objectMap(query, (value, key) => `${(this.options.fieldsCorrespondance as any)[key] ?? key}=${value}`)
|
||||
?.filter((it) => it.slice(0, it.indexOf('=')) !== passwordField)
|
||||
?.join(',')
|
||||
if (!doLogin) {
|
||||
const client = await this.bind(`cn=${this.options.adminUsername},${this.options.dnSuffix}`, this.options.adminPassword)
|
||||
// @ts-expect-error nique ta mere
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const results = (await this.ldapFind(client, objectMap(query, (value, key) => [key as string, '', value as string])
|
||||
.map((it) => ({key: it[0] as LDAPFields, value: it[2]}))!
|
||||
)).map((it) => this.schema.parse(
|
||||
objectRemap(it, (value, key) => ({key: this.reverseReference[key as string] as string, value: value}))
|
||||
)).filter((it): it is Implementation<T> => !!it)
|
||||
|
||||
return {
|
||||
rows: results.length,
|
||||
rowsTotal: results.length,
|
||||
page: 1,
|
||||
pageTotal: 1,
|
||||
data: results
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const clone = objectClone(query)
|
||||
delete clone.password
|
||||
const res = await this.read(clone)
|
||||
const user = res.data[0]
|
||||
if (!user) {
|
||||
return emptyResult
|
||||
}
|
||||
const password = query.password as string ?? ''
|
||||
const client = await this.bind(`cn=${user[this.reverseReference.uid as keyof typeof user]!},${this.options.dnSuffix}`, password)
|
||||
// @ts-expect-error nique x2
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const results = (await this.ldapFind(client, objectMap(clone, (value, key) => ({key: key as keyof LDAPFields, value: value}))
|
||||
)).map((it) => this.schema.parse(
|
||||
objectRemap(it, (value, key) => ({key: this.reverseReference[key as string] as string, value: value}))
|
||||
)).filter((it): it is Implementation<T> => !!it)
|
||||
|
||||
if (results.length !== 1) {
|
||||
return emptyResult
|
||||
}
|
||||
|
||||
return {
|
||||
rows: results.length,
|
||||
rowsTotal: results.length,
|
||||
page: 1,
|
||||
pageTotal: 1,
|
||||
data: results
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('error, user not found', e)
|
||||
return emptyResult
|
||||
}
|
||||
}
|
||||
|
||||
public async update(_obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
public async patch(_id: string, _obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
public async delete(_obj: Partial<Implementation<T>>): Promise<boolean> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
private bind(dn: string, password: string): Promise<ldap.Client> {
|
||||
const client = ldap.createClient({
|
||||
url: this.options.url
|
||||
})
|
||||
return new Promise<ldap.Client>((res, rej) => {
|
||||
client.on('connect', () => {
|
||||
client.bind(dn, password, (err) => {
|
||||
if (err) {
|
||||
console.error('error binding as', dn, err)
|
||||
client.unbind()
|
||||
return
|
||||
}
|
||||
console.log('binded as', dn)
|
||||
res(client)
|
||||
})
|
||||
})
|
||||
.on('timeout', (err) => rej(err))
|
||||
.on('connectTimeout', (err) => rej(err))
|
||||
.on('error', (err) => rej(err))
|
||||
.on('connectError', (err) => rej(err))
|
||||
})
|
||||
}
|
||||
|
||||
private async ldapFind(client: ldap.Client, filters: Array<{key: LDAPFields, value: string}>): Promise<Array<Record<LDAPFields, string | Array<string> | undefined>>> {
|
||||
|
||||
if (filters.length === 0) {
|
||||
return []
|
||||
}
|
||||
const firstFilter = filters.shift()!
|
||||
return new Promise<Array<Record<LDAPFields, string | Array<string> | undefined>>>((res, rej) => {
|
||||
const users: Array<Record<LDAPFields, string | Array<string> | undefined>> = []
|
||||
client.search(
|
||||
this.options.dnSuffix, {
|
||||
filter: new ldap.EqualityFilter({
|
||||
attribute: firstFilter.key as any,
|
||||
value: firstFilter.value,
|
||||
}),
|
||||
scope: 'sub',
|
||||
attributes: this.attributes
|
||||
}, (err, search) => {
|
||||
if (err) {
|
||||
rej(err)
|
||||
}
|
||||
// console.log('search', search, err)
|
||||
search.on('searchEntry', (entry) => {
|
||||
users.push(this.parseUser(entry))
|
||||
}).on('error', (err2) => {
|
||||
rej(err2)
|
||||
client.unbind()
|
||||
console.error('error in search lol', err2)
|
||||
}).on('end', () => {
|
||||
console.log(users)
|
||||
res(users)
|
||||
|
||||
client.unbind()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private parseUser(usr: ldap.SearchEntry): Record<LDAPFields, string | Array<string> | undefined> {
|
||||
const user: Record<string, string | Array<string> | undefined> = { dn: usr.objectName ?? undefined }
|
||||
usr.attributes.forEach((attribute) => {
|
||||
user[attribute.type] =
|
||||
attribute.values.length === 1 ? attribute.values[0] : attribute.values
|
||||
})
|
||||
return user
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user