mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Added user rebinding on reconnect in LDAP (#9339)
* Added reconnect handling code for LDAP * Update api/src/auth/drivers/ldap.ts * Improved error handling * Updated re-bind so we can await it when required * Added bind check before operations to ensure client has user * Cleaned up reconnect handling in LDAP * Minor cleanup * Minor cleanup
This commit is contained in:
@@ -7,6 +7,7 @@ import ldap, {
|
||||
InappropriateAuthenticationError,
|
||||
InvalidCredentialsError,
|
||||
InsufficientAccessRightsError,
|
||||
OperationsError,
|
||||
} from 'ldapjs';
|
||||
import ms from 'ms';
|
||||
import Joi from 'joi';
|
||||
@@ -37,58 +38,75 @@ interface UserInfo {
|
||||
const INVALID_ACCOUNT_FLAGS = 0x800012;
|
||||
|
||||
export class LDAPAuthDriver extends AuthDriver {
|
||||
bindClient: Promise<Client>;
|
||||
bindClient: Client;
|
||||
usersService: UsersService;
|
||||
config: Record<string, any>;
|
||||
|
||||
constructor(options: AuthDriverOptions, config: Record<string, any>) {
|
||||
super(options, config);
|
||||
|
||||
const { bindDn, bindPassword, ...additionalConfig } = config;
|
||||
const { bindDn, bindPassword, userDn, provider, clientUrl } = config;
|
||||
|
||||
if (
|
||||
!bindDn ||
|
||||
!bindPassword ||
|
||||
!additionalConfig.userDn ||
|
||||
!additionalConfig.provider ||
|
||||
(!additionalConfig.clientUrl && !additionalConfig.client?.socketPath)
|
||||
) {
|
||||
throw new InvalidConfigException('Invalid provider config', { provider: additionalConfig.provider });
|
||||
if (!bindDn || !bindPassword || !userDn || !provider || (!clientUrl && !config.client?.socketPath)) {
|
||||
throw new InvalidConfigException('Invalid provider config', { provider });
|
||||
}
|
||||
|
||||
this.bindClient = new Promise((resolve, reject) => {
|
||||
const clientConfig = typeof additionalConfig.client === 'object' ? additionalConfig.client : {};
|
||||
const client = ldap.createClient({ url: additionalConfig.clientUrl, reconnect: true, ...clientConfig });
|
||||
const clientConfig = typeof config.client === 'object' ? config.client : {};
|
||||
|
||||
client.on('error', (err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
client.bind(bindDn, bindPassword, (err: Error | null) => {
|
||||
if (err) {
|
||||
const error = handleError(err);
|
||||
|
||||
if (error instanceof InvalidCredentialsException) {
|
||||
reject(new InvalidConfigException('Invalid bind user', { provider: additionalConfig.provider }));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(client);
|
||||
});
|
||||
this.bindClient = ldap.createClient({ url: clientUrl, reconnect: true, ...clientConfig });
|
||||
this.bindClient.on('error', (err: Error) => {
|
||||
logger.warn(err);
|
||||
});
|
||||
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
||||
this.config = additionalConfig;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private async validateBindClient(): Promise<void> {
|
||||
const { bindDn, bindPassword, provider } = this.config;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Healthcheck bind user
|
||||
this.bindClient.search(bindDn, {}, (err: Error | null, res: SearchCallbackResponse) => {
|
||||
if (err) {
|
||||
reject(handleError(err));
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('searchEntry', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
res.on('error', (err: Error) => {
|
||||
if (!(err instanceof OperationsError)) {
|
||||
reject(handleError(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebind on OperationsError
|
||||
this.bindClient.bind(bindDn, bindPassword, (err: Error | null) => {
|
||||
if (err) {
|
||||
const error = handleError(err);
|
||||
|
||||
if (error instanceof InvalidCredentialsException) {
|
||||
reject(new InvalidConfigException('Invalid bind user', { provider }));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchUserDn(identifier: string): Promise<string | undefined> {
|
||||
const { userDn, userAttribute } = this.config;
|
||||
const client = await this.bindClient;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Search for the user in LDAP by attribute
|
||||
client.search(
|
||||
this.bindClient.search(
|
||||
userDn,
|
||||
{
|
||||
attributes: ['cn'],
|
||||
@@ -119,12 +137,11 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
}
|
||||
|
||||
private async fetchUserInfo(userDn: string): Promise<UserInfo | undefined> {
|
||||
const client = await this.bindClient;
|
||||
const { mailAttribute } = this.config;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Fetch user info in LDAP by domain component
|
||||
client.search(
|
||||
this.bindClient.search(
|
||||
userDn,
|
||||
{ attributes: ['givenName', 'sn', mailAttribute ?? 'mail', 'userAccountControl'] },
|
||||
(err: Error | null, res: SearchCallbackResponse) => {
|
||||
@@ -166,13 +183,11 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const client = await this.bindClient;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let userGroups: string[] = [];
|
||||
|
||||
// Search for the user info in LDAP by group attribute
|
||||
client.search(
|
||||
this.bindClient.search(
|
||||
groupDn,
|
||||
{
|
||||
attributes: ['cn'],
|
||||
@@ -220,6 +235,8 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
await this.validateBindClient();
|
||||
|
||||
const userDn = await this.fetchUserDn(payload.identifier);
|
||||
|
||||
if (!userDn) {
|
||||
@@ -299,6 +316,8 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
}
|
||||
|
||||
async refresh(user: User): Promise<SessionData> {
|
||||
await this.validateBindClient();
|
||||
|
||||
const userInfo = await this.fetchUserInfo(user.external_identifier!);
|
||||
|
||||
if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {
|
||||
|
||||
Reference in New Issue
Block a user