From a79d36d2edb52048ee6cc83b33ea95693755154a Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 7 Aug 2025 17:20:35 +0300 Subject: [PATCH 1/4] fix: the test was using POST instead of GET, updated the method --- apps/meteor/tests/end-to-end/api/LDAP.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/LDAP.ts b/apps/meteor/tests/end-to-end/api/LDAP.ts index d9ac0c65b8ca3..c8e3cff5cfcd2 100644 --- a/apps/meteor/tests/end-to-end/api/LDAP.ts +++ b/apps/meteor/tests/end-to-end/api/LDAP.ts @@ -83,7 +83,7 @@ describe('LDAP', function () { it('should not allow testing LDAP connection if user does NOT have the test-admin-options permission', async () => { await updatePermission('test-admin-options', []); await request - .post(api('ldap.testConnection')) + .get(api('ldap.testConnection')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) From ecc4d9fb874efbc13d09e5a2e0bfa4d41cdc731f Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 7 Aug 2025 17:21:14 +0300 Subject: [PATCH 2/4] feat: add openapi support for ldap APIs --- apps/meteor/app/api/server/v1/ldap.ts | 120 +++++++++++++++++++++----- apps/meteor/ee/server/api/ldap.ts | 74 ++++++++++++---- 2 files changed, 156 insertions(+), 38 deletions(-) diff --git a/apps/meteor/app/api/server/v1/ldap.ts b/apps/meteor/app/api/server/v1/ldap.ts index 4b112cfbf3351..00866e364158a 100644 --- a/apps/meteor/app/api/server/v1/ldap.ts +++ b/apps/meteor/app/api/server/v1/ldap.ts @@ -1,42 +1,116 @@ import { LDAP } from '@rocket.chat/core-services'; +import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; -API.v1.addRoute( - 'ldap.testConnection', - { authRequired: true, permissionsRequired: ['test-admin-options'] }, - { - async post() { +type ldapTestSearchProps = { + username: string; +}; + +const ldapTestSearchPropsSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +const isLdapTestSearch = ajv.compile(ldapTestSearchPropsSchema); + +const ldapEndpoints = API.v1 + .get( + 'ldap.testConnection', + { + authRequired: true, + permissionsRequired: ['test-admin-options'], + response: { + 200: ajv.compile<{ + message: string; + }>({ + type: 'object', + properties: { + message: { + type: 'string', + enum: ['LDAP_Connection_successful'], + }, + success: { + type: 'boolean', + enum: [true], + description: 'Whether the connection was successful or not', + }, + }, + required: ['success', 'message'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { if (!this.userId) { - throw new Error('error-invalid-user'); + throw new Meteor.Error('error-invalid-user'); } if (settings.get('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); + throw new Meteor.Error('LDAP_disabled'); } try { await LDAP.testConnection(); } catch (error) { SystemLogger.error(error); - throw new Error('Connection_failed'); + throw new Meteor.Error('Connection_failed'); } return API.v1.success({ message: 'LDAP_Connection_successful' as const, }); }, - }, -); - -API.v1.addRoute( - 'ldap.testSearch', - { authRequired: true, permissionsRequired: ['test-admin-options'] }, - { - async post() { + ) + .post( + 'ldap.testSearch', + { + authRequired: true, + permissionsRequired: ['test-admin-options'], + body: isLdapTestSearch, + response: { + 200: ajv.compile<{ + message: string; + }>({ + type: 'object', + properties: { + message: { + type: 'string', + enum: ['LDAP_User_Found'], + }, + success: { + type: 'boolean', + enum: [true], + description: 'Whether the connection was successful or not', + }, + }, + required: ['success', 'message'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { check( this.bodyParams, Match.ObjectIncluding({ @@ -45,11 +119,11 @@ API.v1.addRoute( ); if (!this.userId) { - throw new Error('error-invalid-user'); + throw new Meteor.Error('error-invalid-user'); } if (settings.get('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); + throw new Meteor.Error('LDAP_disabled'); } await LDAP.testSearch(this.bodyParams.username); @@ -58,5 +132,11 @@ API.v1.addRoute( message: 'LDAP_User_Found' as const, }); }, - }, -); + ); + +export type LdapEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends LdapEndpoints {} +} diff --git a/apps/meteor/ee/server/api/ldap.ts b/apps/meteor/ee/server/api/ldap.ts index b69a63407320c..c5fc692ad6628 100644 --- a/apps/meteor/ee/server/api/ldap.ts +++ b/apps/meteor/ee/server/api/ldap.ts @@ -1,36 +1,74 @@ +import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; + +import type { ExtractRoutesFromAPI } from '../../../app/api/server/ApiClass'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; import { settings } from '../../../app/settings/server'; import { LDAPEE } from '../sdk'; -API.v1.addRoute( +const ldapEndpoints = API.v1.post( 'ldap.syncNow', { authRequired: true, forceTwoFactorAuthenticationForNonEnterprise: true, twoFactorRequired: true, + body: ajv.compile(false), // license: ['ldap-enterprise'], + response: { + 200: ajv.compile<{ + message: string; + }>({ + type: 'object', + properties: { + message: { + type: 'string', + enum: ['Sync_in_progress'], + }, + success: { + type: 'boolean', + enum: [true], + description: 'Whether the connection was successful or not', + }, + }, + required: ['success', 'message'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, }, - { - async post() { - if (!this.userId) { - throw new Error('error-invalid-user'); - } - if (!(await hasPermissionAsync(this.userId, 'sync-auth-services-users'))) { - throw new Error('error-not-authorized'); - } + async function action() { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user'); + } - if (settings.get('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); - } + if (!(await hasPermissionAsync(this.userId, 'sync-auth-services-users'))) { + throw new Meteor.Error('error-not-authorized'); + } - await LDAPEE.sync(); - await LDAPEE.syncAvatars(); + if (settings.get('LDAP_Enable') !== true) { + throw new Meteor.Error('LDAP_disabled'); + } - return API.v1.success({ - message: 'Sync_in_progress' as const, - }); - }, + await LDAPEE.sync(); + await LDAPEE.syncAvatars(); + + return API.v1.success({ + message: 'Sync_in_progress' as const, + }); }, ); + +export type LdapEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends LdapEndpoints {} +} From 3cbb447e92e9624cfd437147ffc9821aded31173 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 7 Aug 2025 17:21:41 +0300 Subject: [PATCH 3/4] fix: remove unused ldap from rest-typings --- packages/rest-typings/src/index.ts | 2 -- packages/rest-typings/src/v1/ldap.ts | 40 ---------------------------- 2 files changed, 42 deletions(-) delete mode 100644 packages/rest-typings/src/v1/ldap.ts diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 8198854729d41..aa2feaf02d03e 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -27,7 +27,6 @@ import type { InstancesEndpoints } from './v1/instances'; import type { IntegrationsEndpoints } from './v1/integrations'; import type { IntegrationHooksEndpoints } from './v1/integrations/hooks'; import type { InvitesEndpoints } from './v1/invites'; -import type { LDAPEndpoints } from './v1/ldap'; import type { LicensesEndpoints } from './v1/licenses'; import type { MailerEndpoints } from './v1/mailer'; import type { MeEndpoints } from './v1/me'; @@ -66,7 +65,6 @@ export interface Endpoints EmojiCustomEndpoints, GroupsEndpoints, ImEndpoints, - LDAPEndpoints, RoomsEndpoints, PushEndpoints, RolesEndpoints, diff --git a/packages/rest-typings/src/v1/ldap.ts b/packages/rest-typings/src/v1/ldap.ts deleted file mode 100644 index 1591ff0333939..0000000000000 --- a/packages/rest-typings/src/v1/ldap.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Ajv from 'ajv'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -type ldapTestSearchProps = { - username: string; -}; - -const ldapTestSearchPropsSchema = { - type: 'object', - properties: { - username: { - type: 'string', - }, - }, - required: ['username'], - additionalProperties: false, -}; - -export const isLdapTestSearch = ajv.compile(ldapTestSearchPropsSchema); - -export type LDAPEndpoints = { - '/v1/ldap.testConnection': { - POST: () => { - message: string; - }; - }; - '/v1/ldap.testSearch': { - POST: (params: ldapTestSearchProps) => { - message: string; - }; - }; - '/v1/ldap.syncNow': { - POST: () => { - message: string; - }; - }; -}; From 8df3d3c1300e0ca8915f6d871cdea4447b0aba10 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Thu, 7 Aug 2025 17:24:36 +0300 Subject: [PATCH 4/4] Create breezy-months-destroy.md --- .changeset/breezy-months-destroy.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/breezy-months-destroy.md diff --git a/.changeset/breezy-months-destroy.md b/.changeset/breezy-months-destroy.md new file mode 100644 index 0000000000000..a4f1868cf47f1 --- /dev/null +++ b/.changeset/breezy-months-destroy.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat ldap APIs endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation.