1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-09 07:20:05 +00:00

Merge pull request #293 from sasjs/ldap

feat: integratedLDAP authentication
This commit is contained in:
Allan Bowe
2022-10-03 13:09:07 +01:00
committed by GitHub
24 changed files with 1165 additions and 86 deletions

View File

@@ -18,6 +18,7 @@ export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'

163
api/src/utils/ldapClient.ts Normal file
View File

@@ -0,0 +1,163 @@
import { createClient, Client } from 'ldapjs'
import { ReturnCode } from './verifyEnvVariables'
export interface LDAPUser {
uid: string
username: string
displayName: string
}
export interface LDAPGroup {
name: string
members: string[]
}
export class LDAPClient {
private ldapClient: Client
private static classInstance: LDAPClient | null
private constructor() {
process.logger.info('creating LDAP client')
this.ldapClient = createClient({ url: process.env.LDAP_URL as string })
this.ldapClient.on('error', (error) => {
process.logger.error(error.message)
})
}
static async init() {
if (!LDAPClient.classInstance) {
LDAPClient.classInstance = new LDAPClient()
process.logger.info('binding LDAP client')
await LDAPClient.classInstance.bind().catch((error) => {
LDAPClient.classInstance = null
throw error
})
}
return LDAPClient.classInstance
}
private async bind() {
const promise = new Promise<void>((resolve, reject) => {
const { LDAP_BIND_DN, LDAP_BIND_PASSWORD } = process.env
this.ldapClient.bind(LDAP_BIND_DN!, LDAP_BIND_PASSWORD!, (error) => {
if (error) reject(error)
resolve()
})
})
await promise.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPUsers() {
const promise = new Promise<LDAPUser[]>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(objectClass=*)`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const users: LDAPUser[] = []
result.on('searchEntry', (entry) => {
users.push({
uid: entry.object.uid as string,
username: entry.object.username as string,
displayName: entry.object.displayname as string
})
})
result.on('end', (result) => {
resolve(users)
})
}
)
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPGroups() {
const promise = new Promise<LDAPGroup[]>((resolve, reject) => {
const { LDAP_GROUPS_BASE_DN } = process.env
this.ldapClient.search(LDAP_GROUPS_BASE_DN!, {}, (error, result) => {
if (error) reject(error)
const groups: LDAPGroup[] = []
result.on('searchEntry', (entry) => {
const members =
typeof entry.object.memberuid === 'string'
? [entry.object.memberuid]
: entry.object.memberuid
groups.push({
name: entry.object.cn as string,
members
})
})
result.on('end', (result) => {
resolve(groups)
})
})
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async verifyUser(username: string, password: string) {
const promise = new Promise<boolean>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(username=${username})`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const items: any = []
result.on('searchEntry', (entry) => {
items.push(entry.object)
})
result.on('end', (result) => {
if (result?.status !== 0 || items.length === 0) return reject()
// pick the first found
const user = items[0]
this.ldapClient.bind(user.dn, password, (error) => {
if (error) return reject(error)
resolve(true)
})
})
}
)
})
return await promise
.then(() => true)
.catch(() => {
throw new Error('Invalid password.')
})
}
}

View File

@@ -8,6 +8,11 @@ export enum ModeType {
Desktop = 'desktop'
}
export enum AuthProviderType {
LDAP = 'ldap',
Internal = 'internal'
}
export enum ProtocolType {
HTTP = 'http',
HTTPS = 'https'
@@ -64,6 +69,8 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyExecutablePaths())
errors.push(...verifyLDAPVariables())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -104,13 +111,24 @@ const verifyMODE = (): string[] => {
}
if (process.env.MODE === ModeType.Server) {
const { DB_CONNECT } = process.env
const { DB_CONNECT, AUTH_MECHANISM } = process.env
if (process.env.NODE_ENV !== 'test')
if (process.env.NODE_ENV !== 'test') {
if (!DB_CONNECT)
errors.push(
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
)
if (AUTH_MECHANISM) {
const authMechanismTypes = Object.values(AuthProviderType)
if (!authMechanismTypes.includes(AUTH_MECHANISM as AuthProviderType))
errors.push(
`- AUTH_MECHANISM '${AUTH_MECHANISM}'\n - valid options ${authMechanismTypes}`
)
} else {
process.env.AUTH_MECHANISM = DEFAULTS.AUTH_MECHANISM
}
}
}
return errors
@@ -280,8 +298,56 @@ const verifyExecutablePaths = () => {
return errors
}
const verifyLDAPVariables = () => {
const errors: string[] = []
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN,
MODE,
AUTH_MECHANISM
} = process.env
if (MODE === ModeType.Server && AUTH_MECHANISM === AuthProviderType.LDAP) {
if (!LDAP_URL) {
errors.push(
`- LDAP_URL is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_DN) {
errors.push(
`- LDAP_BIND_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_PASSWORD) {
errors.push(
`- LDAP_BIND_PASSWORD is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_USERS_BASE_DN) {
errors.push(
`- LDAP_USERS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_GROUPS_BASE_DN) {
errors.push(
`- LDAP_GROUPS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
}
return errors
}
const DEFAULTS = {
MODE: ModeType.Desktop,
AUTH_MECHANISM: AuthProviderType.Internal,
PROTOCOL: ProtocolType.HTTP,
PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE,