Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | |
Expand All @@ -187,6 +188,10 @@ 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. | |
| `LDAP_USERNAME_ATTR`| string | Yes | Target field to get the username from the Ldap response. Defaults to displayName. | |
| `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_ADDITIONAL_AUTHORIZED_PARTIES` | string | No | Comma-separated list of additional OIDC client IDs allowed to present tokens to this backend. Used for token exchange scenarios where a third-party client obtains a token on behalf of a user. The client ID must appear as the `azp` claim in the token. Example: `additional-client1, additional-client2`. | |
Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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. | |
| `EXPRESS_SESSION_SECRET` | string | No | Secret used to set up express session. Required if using OIDC authentication | |
| `HTTP_MAX_REDIRECTS` | number | Yes | Max redirects for HTTP requests. | 5 |
Expand All @@ -135,6 +136,10 @@ 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. | |
| `LDAP_USERNAME_ATTR`| string | Yes | Target field to get the username from the Ldap response. Defaults to displayName. | |
| `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. | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import { UserPayload } from "../interfaces/userPayload.interface";
import { AccessGroupFromLdapService } from "./access-group-from-ldap.service";

describe("AccessGroupFromLdapService", () => {
let service: AccessGroupFromLdapService;

const mockAccessService = new AccessGroupFromLdapService("cn");

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AccessGroupFromLdapService, ConfigService],
})
.overrideProvider(AccessGroupFromLdapService)
.useValue(mockAccessService)
.compile();

service = module.get<AccessGroupFromLdapService>(
AccessGroupFromLdapService,
);
});

it("should be defined", () => {
expect(service).toBeDefined();
});

it("Should resolve access groups", async () => {
const userPayload = {
userId: "test_user",
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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable, Logger } from "@nestjs/common";
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 accessGroupProperty: string) {
super();
}

async getAccessGroups(userPayload: UserPayload): Promise<string[]> {
const accessGroups: string[] = [];

const accessGroupsProperty = this.accessGroupProperty;
if (accessGroupsProperty) {
const payload: Record<string, unknown> | undefined = userPayload.payload;
if (payload !== undefined && Array.isArray(payload["_groups"])) {
for (const group of payload["_groups"]) {
if (
typeof group === "object" &&
accessGroupsProperty in group &&
typeof group[accessGroupsProperty] === "string"
) {
accessGroups.push(group[accessGroupsProperty]);
}
}
}
Logger.log(accessGroups, "AccessGroupFromLdapService");
}
return accessGroups;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class AccessGroupFromPayloadService extends AccessGroupService {

async getAccessGroups(userPayload: UserPayload): Promise<string[]> {
//const defaultAccessGroups: string[] = [];
let accessGroups: string[] = [];
const accessGroups: string[] = [];

const accessGroupsProperty = userPayload.accessGroupProperty;
if (accessGroupsProperty) {
Expand All @@ -24,10 +24,11 @@ export class AccessGroupFromPayloadService extends AccessGroupService {
payload !== undefined &&
Array.isArray(payload[accessGroupsProperty])
) {
accessGroups =
payload[accessGroupsProperty] !== undefined
? (payload[accessGroupsProperty] as string[])
: [];
for (const group of payload[accessGroupsProperty]) {
if (typeof group === "string") {
accessGroups.push(group);
}
}
}
Logger.log(accessGroups, "AccessGroupFromPayloadService");
}
Expand Down
16 changes: 16 additions & 0 deletions src/auth/access-group-provider/access-group-service-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { AccessGroupFromRestApiService } from "./access-group-from-rest-api-call.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";
Expand All @@ -23,6 +24,9 @@ export const accessGroupServiceFactory = {
const accessGroupsOIDCPayloadConfig = configService.get(
"accessGroupsOIDCPayloadConfig",
);
const accessGroupsLdapPayloadConfig = configService.get(
"accessGroupsLdapPayloadConfig",
);

const accessGroupsRestConfig = configService.get("accessGroupsRestConfig");

Expand All @@ -45,6 +49,18 @@ export const accessGroupServiceFactory = {
new AccessGroupFromPayloadService(configService),
);
}
if (accessGroupsLdapPayloadConfig?.enabled == true) {
Logger.log(
JSON.stringify(accessGroupsLdapPayloadConfig),
"loading ldap processor",
);

accessGroupServices.push(
new AccessGroupFromLdapService(
accessGroupsLdapPayloadConfig?.accessGroupProperty,
),
);
}

if (accessGroupsGraphQlConfig?.enabled == true) {
Logger.log(
Expand Down
32 changes: 24 additions & 8 deletions src/auth/strategies/ldap.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,36 @@ import { LdapConfig } from "src/config/configuration";

@Injectable()
export class LdapStrategy extends PassportStrategy(Strategy, "ldap") {
readonly ldapOptions: LdapConfig;

constructor(
configService: ConfigService,
private usersService: UsersService,
private accessGroupService: AccessGroupService,
) {
const ldapOptions = configService.get<LdapConfig>("ldap")!;

super(ldapOptions);
this.ldapOptions = ldapOptions;
}

async validate(
payload: Record<string, unknown>,
): Promise<Omit<User, "password">> {
// add exception if displayName is empty

const username = this.getUsername(payload);
const userFilter: FilterQuery<UserDocument> = {
$or: [
{ username: `ldap.${payload.displayName}` },
{ username: payload.displayName as string },
{ username: `ldap.${username}` },
{ username: username as string },
{ email: payload.mail as string },
],
};
const userExists = await this.usersService.userExists(userFilter);

if (!userExists) {
const createUser: CreateUserDto = {
username: payload.displayName as string, //`ldap.${payload.displayName}`,
username: username as string,
email: payload.mail as string,
authStrategy: "ldap",
};
Expand All @@ -58,6 +61,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") {
userId: user.id as string,
username: user.username,
email: user.email,
payload: payload,
};
const accessGroups =
await this.accessGroupService.getAccessGroups(userPayload);
Expand All @@ -67,9 +71,9 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") {
credentials: {},
externalId: payload.sAMAccountName as string,
profile: {
displayName: payload.displayName as string,
displayName: username as string,
email: payload.mail as string,
username: payload.displayName as string,
username: username as string,
thumbnailPhoto: payload.thumbnailPhoto
? "data:image/jpeg;base64," +
Buffer.from(payload.thumbnailPhoto as string, "binary").toString(
Expand Down Expand Up @@ -99,6 +103,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") {
userId: user.id as string,
username: user.username,
email: user.email,
payload: payload,
};
const userIdentity = await this.usersService.findByIdUserIdentity(
user._id,
Expand All @@ -121,13 +126,24 @@ export class LdapStrategy extends PassportStrategy(Strategy, "ldap") {
return user;
}

private getUsername(payload: Record<string, unknown>) {
const userattr = this.ldapOptions.server.usernameAttr;
if (userattr in payload) {
return payload[userattr] as string;
}
throw new InternalServerErrorException(
"usernameAttr incorrectly configured: " + userattr,
);
}

getProfile(payload: Record<string, unknown>) {
type ldapProfile = Profile & UserProfile;
const profile = {} as ldapProfile;
const username = this.getUsername(payload);

profile.displayName = payload.displayName as string;
profile.displayName = username as string;
profile.email = payload.mail as string;
profile.username = payload.displayName as string;
profile.username = username as string;
profile.thumbnailPhoto = payload.thumbnailPhoto
? "data:image/jpeg;base64," +
Buffer.from(payload.thumbnailPhoto as string, "binary").toString(
Expand Down
8 changes: 7 additions & 1 deletion src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ const configuration = () => {
apiUrl: process.env.ACCESS_GROUPS_SERVICE_REST_API_URL,
userIdField: process.env.ACCESS_GROUPS_SERVICE_REST_USER_ID_FIELD,
},
accessGroupsLdapPayloadConfig: {
enabled: boolean(process.env?.ACCESS_GROUPS_LDAPPAYLOAD_ENABLED || false),
accessGroupProperty: process.env?.LDAP_ACCESS_GROUPS_PROPERTY || "cn", // Examples: "cn" or "ou"
},
doiPrefix: process.env.DOI_PREFIX,
expressSession: {
secret: process.env.EXPRESS_SESSION_SECRET,
Expand All @@ -329,9 +333,11 @@ 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",
usernameAttr: process.env.LDAP_USERNAME_ATTR ?? "displayName",
},
},
oidc: {
Expand Down
Loading