Skip to content

Commit

Permalink
add customSecurityAttributes by default
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaenggli committed Dec 28, 2024
1 parent e1fbbfc commit 7864ff4
Show file tree
Hide file tree
Showing 18 changed files with 296 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- removed placeholder secrets from the dockerfile to prevent export error of SecretsUsedInArgOrEnv
- removed build for ppc64le arch due to build errors with the new node version
- updated dependencies, removed package fs:0.0.1-security as fs is npm default
- fetch customSecurityAttributes by default if entra app permissions are set correctly (probably also fixes #94)


### Fixed

Expand Down
2 changes: 1 addition & 1 deletion Docker_build_DEV.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ REM multiarch-dev
echo "Build DEV as multiarch"
pause
REM docker buildx build --no-cache --push -t ahaen/azuread-ldap-wrapper:dev --platform linux/amd64,linux/arm64/v8,linux/arm/v7 .
docker buildx build --no-cache --push -t ahaen/azuread-ldap-wrapper:dev --platform linux/amd64,linux/arm64,linux/s390x,linux/arm/v8,linux/arm/v7,linux/arm/v6 .
docker buildx build --no-cache --push -t ahaen/azuread-ldap-wrapper:tst --platform linux/arm64,linux/amd64,linux/s390x,linux/arm/v8,linux/arm/v7,linux/arm/v6 .
pause
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ENV GRAPH_FILTER_GROUPS="securityEnabled eq true"
ENV GRAPH_IGNORE_MFA_ERRORS="false"

RUN mkdir -p /app && chown -R node:node /app
RUN mkdir -p /app/.cache && chown -R node:node /app/.cache
RUN mkdir -p /app/.cache && chown -R node:node /app/.cache
RUN echo "This file was created by the dockerfile. It should not exist on a mapped volume." > /app/.cache/IshouldNotExist.txt

WORKDIR /app
Expand Down
Binary file added docs/content/security/cusSecAtt_permission.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/content/security/cusSecAtt_values.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions docs/content/security/customSecurityAttributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: custom security attributes
---

In some scenarios, you may want to include your assigned [custom security attributes](https://learn.microsoft.com/en-us/entra/fundamentals/custom-security-attributes-overview) for each user in the LDAP-wrapper.
![entra custom security attributes](../cusSecAtt_values.png)

## Prerequisites

Before the LDAP-wrapper can retrieve custom security attributes, ensure the following prerequisites are met:

- **Microsoft Entra**:
- Verify that you have the appropriate licenses to create and assign custom security attributes to your users.
- Ensure custom security attributes are properly configured in Microsoft Entra.
- **LDAP-wrapper**:
- Use version 2.0.3 or later, as earlier versions do not support this feature.

## Steps to Enable Custom Security Attributes in LDAP-wrapper

To include custom security attributes in LDAP, follow these steps:

1. Configure Application Permissions\
You need to grant specific permissions to the application registered in Azure:

- Navigate to your registered application in the Azure portal.
- Add the permission `CustomSecAttributeAssignment.Read.All` for the `Application` type.
- Grant admin consent for this permission.

![registered app permissions](../cusSecAtt_permission.png)

2. Optionally set the environment variables `LDAP_SECURE_ATTRIBUTES` or `LDAP_SENSITIVE_ATTRIBUTES` to secure the new attribute values as described below.

3. After updating the application permissions, restart the LDAP-wrapper (or the container hosting it) to load the updated settings.

4. Verify LDAP Entries\
Once the LDAP-wrapper is running, the assigned custom security attributes will be included in your users' LDAP entries in a flattened format.

- All attributes will have the prefix `cusSecAtt`.
- The wrapper automatically updates or removes these attributes if changes are made in Microsoft Entra.\
**Example of Flattened Attributes**\
Here are example attributes based on the values in the screenshot:
```js
{
"cusSecAtt_animals_hasPermissionToFish": true,
"cusSecAtt_animals_FavoriteAnimal": "dogs",
"cusSecAtt_animals_fish": "Tuna",
"cusSecAtt_animals_exampleMultiText": ["ex1","ex2"]
}
```

## Security Configuration

To enhance security and limit attribute visibility, you can use the two optional environment variables:
`LDAP_SECURE_ATTRIBUTES` or `LDAP_SENSITIVE_ATTRIBUTES`.

**LDAP_SECURE_ATTRIBUTES** allows you to define attributes that are only visible to superusers. These attributes remain hidden for regular users. Example:
`LDAP_SECURE_ATTRIBUTES=cusSecAtt_*|PlannedDischargeDate`

In this example, all attributes starting with cusSecAtt_ and the PlannedDischargeDate attribute are restricted to superusers.

**LDAP_SENSITIVE_ATTRIBUTES** defines sensitive attributes that are visible only to the respective user and superusers. Regular users cannot view these attributes for other users. Example:
`LDAP_SENSITIVE_ATTRIBUTES=cusSecAtt_middlename|PrivatePhoneNumber`

Here, attributes like cusSecAtt_middlename and PrivatePhoneNumber are considered sensitive and restricted accordingly.

## Additional Tips

- Testing: To ensure everything works, verify the LDAP entries using an LDAP browser or a query tool.
- Debugging: If the attributes don’t appear, check:
That the permissions are correctly configured and consented.
The LDAP-wrapper logs for any errors.

For more details on custom security attributes, refer to the [official Microsoft documentation](https://learn.microsoft.com/en-us/entra/fundamentals/custom-security-attributes-overview).
2 changes: 1 addition & 1 deletion docs/content/troubleshooting/_index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 4. Troubleshooting
title: Troubleshooting
---

The first step is always to look at the Docker log. Many errors are handled there. For further steps, e.g. Samba debugging, read this [guide](#samba-is-not-working-what-can-i-do). If you get stuck, feel free to open an issue - but please add the log files, maybe others will be able to read more from them than you.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/usage/authelia.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 6.2 Authelia with LDAP-wrapper
title: Authelia with LDAP-wrapper
---

Authelia supports LDAP authentication, enabling users to log in by authenticating against your LDAP directory. This guide outlines the steps to set up LDAP authentication with Authelia using LDAP-wrapper.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/usage/portainer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 6.1 Portainer
title: Portainer
---

Portainer supports LDAP authentication, allowing users to log in by authenticating against your LDAP directory. This guide outlines the steps to set up LDAP authentication over LDAP-wrapper with Portainer.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/usage/synology-radius-unifi.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 6.3 Synology Radius with UniFi
title: Synology Radius with UniFi
---

UniFi allows you to use a custom Radius server like the default package from Synology. Combined with the LDAP-wrapper, this creates a powerful setup for your users.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/usage/user-reported-examples.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 6.4 user-reported examples
title: user-reported examples
---

- [Using the wrapper to get a Sophos firewall to authenticate VPN users with AzureAD](https://github.com/ahaenggli/AzureAD-LDAP-wrapper/issues/88)
19 changes: 11 additions & 8 deletions docs/data/menu/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,23 @@ main:
- name: 2.3 Customize attributes
ref: '/configuration/customize-attributes'

- name: 4. Security
- name: 3. Security
ref: /security

- name: 5. Troubleshooting
sub:
- name: 3.1 custom security attributes
ref: '/security/customSecurityAttributes'

- name: 4. Troubleshooting
ref: /troubleshooting

- name: 6. Usage examples
- name: 5. Usage examples
ref: /usage
sub:
- name: 6.1 Portainer
- name: 5.1 Portainer
ref: '/usage/portainer'
- name: 6.2 Authelia
- name: 5.2 Authelia
ref: '/usage/authelia'
- name: 6.3 Synology Radius with UniFi
- name: 5.3 Synology Radius with UniFi
ref: '/usage/synology-radius-unifi'
- name: 6.4 user-reported examples
- name: 5.4 user-reported examples
ref: '/usage/user-reported-examples'
23 changes: 19 additions & 4 deletions src/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,15 @@ async function mergeAzureUserEntries(db) {
db[g].memberUid.push(userPrincipalName);
}

let filtered = db[upName] ?? {};

filtered = Object.keys(filtered)
.filter(key => !key.startsWith('cusSecAtt_'))
.reduce((obj, key) => {
obj[key] = filtered[key];
return obj;
}, {});

db[upName] = {
// default values
"objectClass": [
Expand Down Expand Up @@ -705,9 +714,10 @@ async function mergeAzureUserEntries(db) {
"entryCSN": helper.ldap_now() + ".000000Z#000000#000#000000",
"modifyTimestamp": helper.ldap_now() + "Z",

// merge existing values
...db[upName],

// merge existing values except values staring with cusSecAtt
//...db[upName],
...filtered,

// overwrite values from before
"cn": userPrincipalNameOU.toLowerCase(),
"AzureADuserPrincipalName": user.userPrincipalName,
Expand All @@ -732,7 +742,12 @@ async function mergeAzureUserEntries(db) {
"ou": ou.substring(3) || config.LDAP_DOMAIN,
};


// append all fetched data to the ldap entry
if (user.hasOwnProperty('customSecurityAttributes') && user.customSecurityAttributes)
{
let flattendAttributes = helper.flattenObjectAndIgnoreOdata({ "cusSecAtt": user.customSecurityAttributes });
db[upName] = Object.assign(db[upName], flattendAttributes);
}

db[upName] = customizer.ModifyLDAPUser(db[upName], user);
}
Expand Down
2 changes: 1 addition & 1 deletion src/graph.fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function addFilter(val) { return (val === undefined || val === null) ? '' : "&$f

// Default settings
fetch.apiConfig = {
uri: `${config.GRAPH_ENDPOINT}/${config.GRAPH_API_VERSION}/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled${addFilter(config.GRAPH_FILTER_USERS)}`,
uri: `${config.GRAPH_ENDPOINT}/${config.GRAPH_API_VERSION}/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled,customSecurityAttributes${addFilter(config.GRAPH_FILTER_USERS)}`,
gri: `${config.GRAPH_ENDPOINT}/${config.GRAPH_API_VERSION}/groups?$count=true${addFilter(config.GRAPH_FILTER_GROUPS)}`,
mri: `${config.GRAPH_ENDPOINT}/${config.GRAPH_API_VERSION}/groups/{id}/members`,
};
Expand Down
76 changes: 76 additions & 0 deletions src/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,80 @@ helper.ldap_now_2_date = function (strDate) {
return new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, 0));
};



/**
* check for subarrays in further flatten function
* @param {arr} array array to check for subarrays
* @returns boolean
*/
helper.hasSubObjects = function (arr) {
var returni = false;
//(Array.isArray(ob[i]) && ob[i].length == 1 &&
for (var i in arr) {
//if (!arr.hasOwnProperty(i)) continue;
if ((typeof arr[i]) === 'object' && arr[i] !== null) return true;
}
return returni;
};

/**
* convert the json object from azure to a simpler structure
* ignore @odata attributes and skip them
* @param {ob} object object to flatten
* @returns object
*/

helper.flattenObjectAndIgnoreOdata = function (ob) {
var returni = {};

for (var i in ob) {
// skip odata attributes
if (i.indexOf('@odata') > -1) continue;

// skip empty arrays and objects
if (Array.isArray(ob[i]) && ob[i].length == 0) {
returni[i] = null;
continue;
}
if ((typeof ob[i]) === 'object' && ob[i] !== null && Object.keys(ob[i]).length == 0) {
returni[i] = null;
continue;
}

// keep arrays
if ((typeof ob[i]) === 'object' && ob[i] !== null && Array.isArray(ob[i])) {
returni[i] = ob[i];
continue;
}

// keep non-objects
if ((typeof ob[i]) !== 'object' && ob[i] !== null && !Array.isArray(ob[i])) {
returni[i] = ob[i];
continue;
}

// each sub multi-array has to be flatten
// single items or items without deeper items are also okay, so an attribute can hav multiple values
if ((typeof ob[i]) === 'object' && ob[i] !== null && helper.hasSubObjects(ob[i])) {
var flatObject = helper.flattenObjectAndIgnoreOdata(ob[i]);
for (var x in flatObject) {
//if (!flatObject.hasOwnProperty(x)) continue;
//if (x.indexOf('@odata') > -1) continue;
returni[i + '_' + x] = flatObject[x];
}
continue;
}

if ((typeof ob[i]) === 'object' && ob[i] !== null && !helper.hasSubObjects(ob[i]) && !Array.isArray(ob[i])) {
for (var x in ob[i]) {
returni[i + '_' + x] = ob[i][x];
}
}

}

return returni;
};

module.exports = helper;
4 changes: 2 additions & 2 deletions tests/graph.fetch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Graph API Fetch without access token', () => {

it('should not throw errors and be set as wished', async () => {
expect(fetch.apiConfig.gri).toBe("https://graph.microsoft.com/v1.0/groups?$count=true");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled,customSecurityAttributes");
expect(fetch.apiConfig.mri).toBe("https://graph.microsoft.com/v1.0/groups/{id}/members");
});

Expand All @@ -44,7 +44,7 @@ describe('Graph API Fetch with access token', () => {
it('should not throw errors and be set as wished', async () => {

expect(fetch.apiConfig.gri).toBe("https://graph.microsoft.com/v1.0/groups?$count=true");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled,customSecurityAttributes");
expect(fetch.apiConfig.mri).toBe("https://graph.microsoft.com/v1.0/groups/{id}/members");
expect(async () => {
await fetch.initAccessToken();
Expand Down
4 changes: 2 additions & 2 deletions tests/graph.fetchProxy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ describe('Graph API Fetch with proxy', () => {
it('should fail', async () => {

expect(fetch.apiConfig.gri).toBe("https://graph.microsoft.com/v1.0/groups?$count=true&$filter=SecurityEnabled%20eq%20true");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled,customSecurityAttributes");
expect(fetch.apiConfig.mri).toBe("https://graph.microsoft.com/v1.0/groups/{id}/members");

});

it('should not throw errors and be set as wished', async () => {
expect(fetch.apiConfig.gri).toBe("https://graph.microsoft.com/v1.0/groups?$count=true&$filter=SecurityEnabled%20eq%20true");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled");
expect(fetch.apiConfig.uri).toBe("https://graph.microsoft.com/v1.0/users?$count=true&$select=businessPhones,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,identities,userType,externalUserState,accountEnabled,customSecurityAttributes");
expect(fetch.apiConfig.mri).toBe("https://graph.microsoft.com/v1.0/groups/{id}/members");
});

Expand Down
Loading

0 comments on commit 7864ff4

Please sign in to comment.