* 🌟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:
@@ -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",
|
||||||
|
|||||||
@@ -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.");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user