diff --git a/README.md b/README.md index 7bfc22269..b46a55cd2 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `ACCESS_GROUPS_STATIC_ENABLED` | string | Yes | Flag to enable/disable automatic assignment of predefined access groups to all users. | true | | `ACCESS_GROUPS_STATIC_VALUES` | string | Yes | Comma-separated list of access groups automatically assigned to all users. Example: "scicat, user". | | | `ACCESS_GROUPS_OIDCPAYLOAD_ENABLED` | string | Yes | Flag to enable/disable fetching access groups directly from OIDC response. Requires specifying a field via `OIDC_ACCESS_GROUPS_PROPERTY` to extract access groups. | false | +| `ACCESS_GROUPS_LDAPPAYLOAD_ENABLED` | string | Yes | Flag to enable/disable fetching access groups directly from Ldap response. Requires specifying a field via `LDAP_ACCESS_GROUPS_PROPERTY` to extract access groups. | false | | `DOI_PREFIX` | string | | The facility DOI prefix, with trailing slash. | | | `DOI_SHORT_SUFFIX` | string | | By default `uuidv4` is used to generate the DOI suffix but if this flag is `true` the shorter version of 10 random characters is used as DOI suffix. | | | `DOI_USERNAME` | string | | The facility DOI DataCite username. | | @@ -185,6 +186,9 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `LDAP_BIND_CREDENTIALS` | string | Yes | Credentials for your LDAP server. | | | `LDAP_SEARCH_BASE` | string | Yes | Search base for your LDAP server. | | | `LDAP_SEARCH_FILTER` | string | Yes | Search filter for your LDAP server. | | +| `LDAP_GROUP_SEARCH_BASE` | string | Yes | Search base for the user groups. | | +| `LDAP_GROUP_SEARCH_FILTER` | string | Yes | Search filter for the user groups. | | +| `LDAP_ACCESS_GROUPS_PROPERTY`| string | Yes | Target field to get the access groups value from Ldap response. | | | `OIDC_ISSUER` | string | Yes | URL of the OIDC server providing the authentication service. Example: https://identity.esss.dk/realm/ess. | | | `OIDC_CLIENT_ID` | string | Yes | Identity of the client used to obtain the user token. Example: scicat. | | | `OIDC_CLIENT_SECRET` | string | Yes | Secret to provide to the OIDC service to obtain the user token. Example: Aa1JIw3kv3mQlGFWhRrE3gOdkH6xreAwro. | | diff --git a/docs/index.md b/docs/index.md index 6946b0615..7a7004013 100644 --- a/docs/index.md +++ b/docs/index.md @@ -135,6 +135,8 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `LDAP_BIND_CREDENTIALS` | string | Yes | Credentials for your LDAP server. | | | `LDAP_SEARCH_BASE` | string | Yes | Search base for your LDAP server. | | | `LDAP_SEARCH_FILTER` | string | Yes | Search filter for your LDAP server. | | +| `LDAP_GROUP_SEARCH_BASE` | string | Yes | Search base for the user groups. | | +| `LDAP_GROUP_SEARCH_FILTER` | string | Yes | Search filter for the user groups. | | | `OIDC_ISSUER` | string | Yes | URL of the OIDC server providing the authentication service. Example: https://identity.esss.dk/realm/ess. | | | `OIDC_CLIENT_ID` | string | Yes | Identity of the client used to obtain the user token. Example: scicat. | | | `OIDC_CLIENT_SECRET` | string | Yes | Secret to provide to the OIDC service to obtain the user token. Example: Aa1JIw3kv3mQlGFWhRrE3gOdkH6xreAwro. | | diff --git a/src/auth/access-group-provider/access-group-from-ldap.service.spec.ts b/src/auth/access-group-provider/access-group-from-ldap.service.spec.ts new file mode 100644 index 000000000..623fed0de --- /dev/null +++ b/src/auth/access-group-provider/access-group-from-ldap.service.spec.ts @@ -0,0 +1,51 @@ +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { UserPayload } from "../interfaces/userPayload.interface"; +import { AccessGroupFromPayloadService } from "./access-group-from-payload.service"; + +describe("AccessGroupFromPayloadService", () => { + let service: AccessGroupFromPayloadService; + + const mockConfigService = { + get: () => "access_group_property", + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AccessGroupFromPayloadService, ConfigService], + }) + .overrideProvider(ConfigService) + .useValue(mockConfigService) + .compile(); + + service = module.get( + AccessGroupFromPayloadService, + ); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("Should resolve access groups", async () => { + const userPayload = { + userId: "test_user", + accessGroupProperty: "_groups", + payload: { + _groups: [ + { + dn: 'cn=test_group,cn=groups,cn=accounts,dc=example,dc=com', + cn: 'testgroup', + }, + { + dn: 'cn=example_group,cn=groups,cn=accounts,dc=example,dc=com', + cn: 'examplegroup', + } + ], + }, + }; + const expected = ["testgroup", "examplegroup"]; + const actual = await service.getAccessGroups(userPayload as UserPayload); + expect(actual).toEqual(expected); + }); +}); diff --git a/src/auth/access-group-provider/access-group-from-ldap.service.ts b/src/auth/access-group-provider/access-group-from-ldap.service.ts new file mode 100644 index 000000000..0cc8c4e4e --- /dev/null +++ b/src/auth/access-group-provider/access-group-from-ldap.service.ts @@ -0,0 +1,39 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { UserPayload } from "../interfaces/userPayload.interface"; +import { AccessGroupService } from "./access-group.service"; + +/** + * This service is used to get the access groups from the payload of the ldap IDP. + */ +@Injectable() +export class AccessGroupFromLdapService extends AccessGroupService { + constructor(private configService: ConfigService) { + super(); + } + + async getAccessGroups(userPayload: UserPayload): Promise { + let accessGroups: string[] = []; + + const accessGroupsProperty = userPayload.accessGroupProperty; + if (accessGroupsProperty) { + const payload: Record | undefined = userPayload.payload; + if ( + payload !== undefined && + Array.isArray(payload[accessGroupsProperty]) + ) { + for (const group of payload[accessGroupsProperty]) { + if ( + typeof group === "object" && + "cn" in group && + typeof group["cn"] === "string" + ) { + accessGroups.push(group["cn"]); + } + } + } + Logger.log(accessGroups, "AccessGroupFromLdapService"); + } + return accessGroups; + } +} diff --git a/src/auth/access-group-provider/access-group-from-payload.service.ts b/src/auth/access-group-provider/access-group-from-payload.service.ts index bc07ebb42..8df98ddac 100644 --- a/src/auth/access-group-provider/access-group-from-payload.service.ts +++ b/src/auth/access-group-provider/access-group-from-payload.service.ts @@ -24,10 +24,11 @@ export class AccessGroupFromPayloadService extends AccessGroupService { payload !== undefined && Array.isArray(payload[accessGroupsProperty]) ) { - accessGroups = - payload[accessGroupsProperty] !== undefined - ? (payload[accessGroupsProperty] as string[]) - : []; + for (var group of payload[accessGroupsProperty]) { + if (typeof group === "string") { + accessGroups.push(group); + } + } } Logger.log(accessGroups, "AccessGroupFromPayloadService"); } diff --git a/src/auth/access-group-provider/access-group-service-factory.ts b/src/auth/access-group-provider/access-group-service-factory.ts index 6c9f7c689..a565e08f1 100644 --- a/src/auth/access-group-provider/access-group-service-factory.ts +++ b/src/auth/access-group-provider/access-group-service-factory.ts @@ -3,6 +3,7 @@ import { AccessGroupFromStaticValuesService } from "./access-group-from-static-v import { AccessGroupService } from "./access-group.service"; import { AccessGroupFromGraphQLApiService } from "./access-group-from-graphql-api-call.service"; import { AccessGroupFromPayloadService } from "./access-group-from-payload.service"; +import { AccessGroupFromLdapService } from "./access-group-from-ldap.service"; import { HttpService } from "@nestjs/axios"; import { AccessGroupFromMultipleProvidersService } from "./access-group-from-multiple-providers.service"; import { Logger } from "@nestjs/common"; @@ -22,6 +23,9 @@ export const accessGroupServiceFactory = { const accessGroupsOIDCPayloadConfig = configService.get( "accessGroupsOIDCPayloadConfig", ); + const accessGroupsLdapPayloadConfig = configService.get( + "accessGroupsLdapPayloadConfig", + ); const accessGroupServices: AccessGroupService[] = []; if (accessGroupsStaticConfig?.enabled == true) { @@ -42,6 +46,15 @@ export const accessGroupServiceFactory = { new AccessGroupFromPayloadService(configService), ); } + if (accessGroupsLdapPayloadConfig?.enabled == true) { + Logger.log( + JSON.stringify(accessGroupsLdapPayloadConfig), + "loading ldap processor", + ); + accessGroupServices.push( + new AccessGroupFromLdapService(configService), + ); + } if (accessGroupsGraphQlConfig?.enabled == true) { Logger.log( diff --git a/src/auth/strategies/ldap.strategy.ts b/src/auth/strategies/ldap.strategy.ts index babbbd171..0fec3d560 100644 --- a/src/auth/strategies/ldap.strategy.ts +++ b/src/auth/strategies/ldap.strategy.ts @@ -58,6 +58,8 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") { userId: user.id as string, username: user.username, email: user.email, + accessGroupProperty: "_groups", + payload: payload, }; const accessGroups = await this.accessGroupService.getAccessGroups(userPayload); @@ -99,6 +101,8 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") { userId: user.id as string, username: user.username, email: user.email, + accessGroupProperty: "_groups", + payload: payload, }; const userIdentity = await this.usersService.findByIdUserIdentity( user._id, diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 1115bfbc1..78c311d4a 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -289,6 +289,10 @@ const configuration = () => { enabled: boolean(process.env?.ACCESS_GROUPS_OIDCPAYLOAD_ENABLED || false), accessGroupProperty: process.env?.OIDC_ACCESS_GROUPS_PROPERTY, // Example: groups }, + accessGroupsLdapPayloadConfig: { + enabled: boolean(process.env?.ACCESS_GROUPS_LDAPPAYLOAD_ENABLED || false), + accessGroupProperty: process.env?.LDAP_ACCESS_GROUPS_PROPERTY, // Example: groups + }, doiPrefix: process.env.DOI_PREFIX, expressSession: { secret: process.env.EXPRESS_SESSION_SECRET, @@ -312,6 +316,8 @@ const configuration = () => { bindCredentials: process.env.LDAP_BIND_CREDENTIALS || "", searchBase: process.env.LDAP_SEARCH_BASE || "", searchFilter: process.env.LDAP_SEARCH_FILTER || "", + groupSearchBase: process.env.LDAP_GROUP_SEARCH_BASE || "", + groupSearchFilter: process.env.LDAP_GROUP_SEARCH_FILTER || "", Mode: process.env.LDAP_MODE ?? "ad", externalIdAttr: process.env.LDAP_EXTERNAL_ID ?? "sAMAccountName", usernameAttr: process.env.LDAP_USERNAME ?? "displayName",