* 🌟Synchronization of the user with LDAP (#206)

* Add configuration with LDAP attributes mappings
* Add default company and remove application check since it's not configurable
* Change error handling, now all the requests are independent
This commit is contained in:
Anton Shepilov
2023-09-12 16:20:37 +02:00
committed by GitHub
parent 15d3602cf8
commit 79764988b8
7 changed files with 106 additions and 40 deletions
@@ -9,7 +9,7 @@ function serverErrorHandler(server: FastifyInstance): void {
? { ? {
statusCode: reply.statusCode, statusCode: reply.statusCode,
error: "Internal Server Error", error: "Internal Server Error",
message: "Something went wrong", message: "Something went wrong, " + err.message,
requestId: request.id, requestId: request.id,
} }
: err, : err,
@@ -14,6 +14,7 @@ import {
getApplicationObject, getApplicationObject,
} from "../../../applications/entities/application"; } from "../../../applications/entities/application";
import gr from "../../../global-resolver"; import gr from "../../../global-resolver";
import { logger } from "../../../../core/platform/framework/logger";
import { import {
ApplicationApiExecutionContext, ApplicationApiExecutionContext,
ApplicationLoginRequest, ApplicationLoginRequest,
@@ -171,20 +172,10 @@ export class ApplicationsApiController {
email: string; email: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
application_id: string;
company_id: string;
}; };
}>, }>,
): Promise<any> { ): Promise<any> {
const email = request.body.email.trim().toLocaleLowerCase(); const email = request.body.email.trim().toLocaleLowerCase();
const checkApplication = gr.services.applications.companyApps.get({
application_id: request.body.application_id,
company_id: request.body.company_id,
});
if (!checkApplication) {
throw new Error("Application is not allowed to sync users for this company.");
}
if (await gr.services.users.getByEmail(email)) { if (await gr.services.users.getByEmail(email)) {
throw new Error("This email is already used"); throw new Error("This email is already used");
@@ -203,12 +194,17 @@ export class ApplicationsApiController {
}); });
const user = await gr.services.users.create(newUser); const user = await gr.services.users.create(newUser);
await gr.services.companies.setUserRole(request.body.company_id, user.entity.id, "admin"); const company = await gr.services.companies.getCompany({
id: "00000000-0000-4000-0000-000000000000",
});
await gr.services.companies.setUserRole(company.id, user.entity.id, "member");
await gr.services.users.save(user.entity, { await gr.services.users.save(user.entity, {
user: { id: user.entity.id, server_request: true }, user: { id: user.entity.id, server_request: true },
}); });
} catch (err) { } catch (err) {
logger.error(err);
throw new Error("An unknown error occured"); throw new Error("An unknown error occured");
} }
return {}; return {};
@@ -1,4 +1,3 @@
import { AxiosInstance } from "axios";
import { ConsoleServiceClient } from "../client-interface"; import { ConsoleServiceClient } from "../client-interface";
import { import {
ConsoleCompany, ConsoleCompany,
@@ -26,7 +25,6 @@ import config from "config";
import { CompanyUserRole } from "src/services/user/web/types"; import { CompanyUserRole } from "src/services/user/web/types";
export class ConsoleRemoteClient implements ConsoleServiceClient { export class ConsoleRemoteClient implements ConsoleServiceClient {
version: "1"; version: "1";
client: AxiosInstance;
private infos: ConsoleOptions; private infos: ConsoleOptions;
private verifier: OidcJwtVerifier; private verifier: OidcJwtVerifier;
@@ -4,3 +4,7 @@ LDAP_BIND_CREDENTIALS=
LDAP_SEARCH_BASE=dc=example,dc=com LDAP_SEARCH_BASE=dc=example,dc=com
LDAP_SEARCH_FILTER=(objectClass=inetorgperson) LDAP_SEARCH_FILTER=(objectClass=inetorgperson)
API_URL=http://tdrive:4000/api/sync API_URL=http://tdrive:4000/api/sync
TDRIVE_URL=http://tdrive:4000/
TDRIVE_CREDENTIALS_ID=application-name
TDRIVE_CREDENTIALS_SECRET=application-secret
LDAP_ATTRIBUTE_MAPPINGS={"firstName": "givenName", "lastName": "sn", "email": "mail"}
@@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module",
"scripts": { "scripts": {
"build": "npm run build:clean && npm run build:ts", "build": "npm run build:clean && npm run build:ts",
"build:ts": "tsc", "build:ts": "tsc",
+88 -23
View File
@@ -1,5 +1,5 @@
import ldap from "ldapjs"; import ldap from "ldapjs";
import axios from "axios"; import axios, { AxiosError } from "axios";
import dotenv from "dotenv"; import dotenv from "dotenv";
interface UserAttributes { interface UserAttributes {
@@ -8,27 +8,93 @@ interface UserAttributes {
email: string; email: string;
} }
dotenv.config(); export interface IApiServiceApplicationTokenRequestParams {
id: string;
secret: string;
}
export interface IApiServiceApplicationTokenResponse {
resource: {
access_token: {
time: number;
expiration: number;
value: string;
type: string;
};
};
}
dotenv.config();
console.log("Run script with the following env: "); console.log("Run script with the following env: ");
console.log(process.env); console.log(process.env);
// LDAP server configuration
const ldapConfig = { const ldapConfig = {
url: process.env.LDAP_URL|| "localhost", url: process.env.LDAP_URL || "localhost",
bindDN: process.env.LDAP_BIND_DN || "", bindDN: process.env.LDAP_BIND_DN || "",
bindCredentials: process.env.LDAP_BIND_CREDENTIALS || "", bindCredentials: process.env.LDAP_BIND_CREDENTIALS || "",
searchBase: process.env.LDAP_SEARCH_BASE || "dc=example,dc=com", searchBase: process.env.LDAP_SEARCH_BASE || "dc=example,dc=com",
searchFilter: process.env.LDAP_SEARCH_FILTER || "(objectClass=inetorgperson)", searchFilter: process.env.LDAP_SEARCH_FILTER || "(objectClass=inetorgperson)",
mappings: JSON.parse(process.env.LDAP_ATTRIBUTE_MAPPINGS || "{}"),
timeout: 120, timeout: 120,
version: 3, version: 3,
}; };
const tdriveConfig = {
url: process.env.TDRIVE_URL || "http://localhost:4000/)",
credentials: {
id: process.env.TDRIVE_CREDENTIALS_ID || "application-name",
secret: process.env.TDRIVE_CREDENTIALS_SECRET || "application-secret",
}
};
const refreshToken = async (): Promise<string> => {
try {
const response = await axios.post<IApiServiceApplicationTokenRequestParams, { data: IApiServiceApplicationTokenResponse }>(
`${tdriveConfig.url.replace(/\/$/, '')}/api/console/v1/login`,
{
id: tdriveConfig.credentials.id,
secret: tdriveConfig.credentials.secret,
},
{
headers: {
Authorization: `Basic ${Buffer.from(`${tdriveConfig.credentials.id}:${tdriveConfig.credentials.secret}`).toString('base64')}`,
},
},
);
const {
resource: {
access_token: { value },
},
} = response.data;
//axiosClient.interceptors.response.use(this.handleResponse, this.handleErrors);
return value;
} catch (error) {
console.error('failed to get application token', error);
console.info('Using token ', tdriveConfig.credentials.id, tdriveConfig.credentials.secret);
console.info(`POST ${tdriveConfig.url.replace(/\/$/, '')}/api/console/v1/login`);
console.info(`Basic ${Buffer.from(`${tdriveConfig.credentials.id}:${tdriveConfig.credentials.secret}`).toString('base64')}`);
throw new Error("Unable to get access to token, see precious errors for details.");
}
};
// Create LDAP client // Create LDAP client
const client = ldap.createClient({ const client = ldap.createClient({
url: ldapConfig.url, url: ldapConfig.url,
}); });
const accessToken = await refreshToken()
const axiosClient = axios.create({
baseURL: tdriveConfig.url,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// Bind to LDAP server // Bind to LDAP server
client.bind(ldapConfig.bindDN, ldapConfig.bindCredentials, (err) => { client.bind(ldapConfig.bindDN, ldapConfig.bindCredentials, (err) => {
if (err) { if (err) {
@@ -41,7 +107,7 @@ client.bind(ldapConfig.bindDN, ldapConfig.bindCredentials, (err) => {
ldapConfig.searchBase, ldapConfig.searchBase,
{ {
filter: ldapConfig.searchFilter, filter: ldapConfig.searchFilter,
attributes: ["uid", "mail", "cn", "sn", "mobile"], attributes: [ldapConfig.mappings.firstName, ldapConfig.mappings.lastName, ldapConfig.mappings.email],
scope: "sub", scope: "sub",
derefAliases: 2, derefAliases: 2,
}, },
@@ -54,15 +120,24 @@ client.bind(ldapConfig.bindDN, ldapConfig.bindCredentials, (err) => {
const apiRequests: Promise<any>[] = []; const apiRequests: Promise<any>[] = [];
searchRes.on("searchEntry", (entry: any) => { searchRes.on("searchEntry", (entry: any) => {
console.log('Receive entry:: ' + JSON.stringify(entry.pojo));
// Handle each search result entry // Handle each search result entry
const userAttributes: UserAttributes = { const userAttributes: UserAttributes = {
first_name: entry.attributes[1]?.values[0], first_name: entry.attributes[0]?.values[0],
last_name: entry.attributes[2]?.values[0], last_name: entry.attributes[1]?.values[0],
email: entry.attributes[3]?.values[0], email: entry.attributes[2]?.values[0],
}; };
// Make API call to tdrive backend with the userAttributes if (userAttributes.email) {
apiRequests.push(axios.post(process.env.API_URL || "", userAttributes)); //Make API call to tdrive backend with the userAttributes
apiRequests.push(axiosClient.post(process.env.API_URL || "", userAttributes)
.catch((e: AxiosError<any>) => {
console.log(`Error for ${JSON.stringify(userAttributes)}: ${e.message}, body: ${e.response?.data?.message}`);
}));
} else {
console.log(`user ${JSON.stringify(userAttributes)} doesn't have an email`);
}
}); });
searchRes.on("error", (err) => { searchRes.on("error", (err) => {
@@ -75,19 +150,9 @@ client.bind(ldapConfig.bindDN, ldapConfig.bindCredentials, (err) => {
if (unbindErr) { if (unbindErr) {
console.error("LDAP unbind error:", unbindErr); console.error("LDAP unbind error:", unbindErr);
} else { } else {
Promise.all(apiRequests)
.then((responses) => { Promise.allSettled(apiRequests)
console.log( .finally(() => console.log("LDAP search COMPLETED."));
"API responses:",
responses.map((r) => r.data)
);
})
.catch((error) => {
console.error("API error:", error);
})
.finally(() => {
console.log("LDAP search completed successfully.");
});
} }
}); });
}); });
+5 -3
View File
@@ -1,10 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "esnext",
"module": "commonjs", "module": "esnext",
"moduleResolution": "node",
"outDir": "dist", "outDir": "dist",
"strict": true, "strict": true,
"esModuleInterop": true "esModuleInterop": true,
"useUnknownInCatchVariables": false
}, },
"include": ["src"] "include": ["src"]
} }