Skip to content

Commit

Permalink
Merge pull request #100 from ahaenggli/dev
Browse files Browse the repository at this point in the history
Add Devices with Group Membership from DEV
  • Loading branch information
ahaenggli authored Jan 25, 2025
2 parents 7864ff4 + 6235bbb commit 8779faa
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 7 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] (in 'dev')

### Added

- Devices with group membership if env var `LDAP_GETDEVICES` is set to `true`.
Note: The `Device.Read.All` permission is additionally needed in the registered app in Entra.

## [2.0.3] - 2024-12-28

### Changed
Expand All @@ -18,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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

- handling missing .cache dir if startet directly in npm
Expand Down
1 change: 1 addition & 0 deletions customizer/customizer_DSM7_IDs_string2int.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ customizer.ModifyAzureUsers = function (azureusers) {
};
*/


module.exports = customizer;
3 changes: 3 additions & 0 deletions customizer/customizer_add_customSecurityAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,7 @@ customizer.ModifyLDAPGlobal = function (all) {
customizer.ModifyAzureGroups = function (azuregroups) { return azuregroups; };
customizer.ModifyAzureUsers = function (azureusers) { return azureusers; };

customizer.ModifyAzureDevices = function (azuredevices) {return azuredevices;};
customizer.ModifyLDAPDevice = function (ldapdevice, azuredevice) { return ldapdevice; };

module.exports = customizer;
3 changes: 2 additions & 1 deletion docs/content/installation/create-azuread-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ Register a new application in your [Microsoft Entra Admin Center](https://entra.
## Set permissions

- Set the following Microsoft Graph API Application permissions:
For type `Application` allow `User.Read.All` and `Group.Read.All`.\
For type `Application` allow `User.Read.All`, `Group.Read.All`.\
For type `Delegated` allow `User.Read`.\
Optionally: Allow `Device.Read.All` for type `Application` if you also want to load devices.\
![Entra Permissions](../entra_permissions.png)

- Click "Grant admin consent". The status should be "Granted for".\
Expand Down
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const allConfigs = {

GRAPH_FILTER_USERS: { format: "String", required: false, default: null, transform: "TRIM" },
GRAPH_FILTER_GROUPS: { format: "String", required: false, default: null, transform: "TRIM" },
GRAPH_FILTER_DEVICES: { format: "String", required: false, default: null, transform: "TRIM" },
GRAPH_IGNORE_MFA_ERRORS: { format: "Boolean", required: false, default: true },

LDAP_SYNC_TIME: { format: "Integer", required: false, default: 30 /* minutes */ },
Expand All @@ -57,6 +58,11 @@ const allConfigs = {
LDAP_USERSDN: { format: "String", required: true, default: () => "cn=users," + config.LDAP_BASEDN, transform: nonWhiteSpaceLowerCase, validate: validateDN },
LDAP_USERSGROUPSBASEDN: { format: "String", required: true, default: () => "cn=users," + config.LDAP_GROUPSDN, transform: nonWhiteSpaceLowerCase, validate: validateDN },

// Devices
LDAP_GETDEVICES: { format: "Boolean", default: false },
LDAP_DEVICESDN: { format: "String", required: true, default: () => "cn=devices," + config.LDAP_BASEDN, transform: nonWhiteSpaceLowerCase, validate: validateDN },
LDAP_DEVICESGROUPSBASEDN: { format: "String", required: true, default: () => "cn=devices," + config.LDAP_GROUPSDN, transform: nonWhiteSpaceLowerCase, validate: validateDN },

LDAP_USERRDN: { format: "String", required: true, default: "uid", transform: nonWhiteSpaceLowerCase },
LDAP_DATAFILE: { format: "String", required: true, default: "./.cache/azure.json", transform: "TRIM" },

Expand Down
13 changes: 13 additions & 0 deletions src/customizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ customizer.ModifyLDAPUser = function (ldapuser, azureuser) {
return ldapuser;
};

// ** modify fetched devices from Azure, e.g to delete some of them or som attributes so they are not processed further ** /
customizer.ModifyAzureDevices = function (azuredevices) {
if (typeof customizer_script.ModifyAzureDevices !== "undefined") azuredevices = customizer_script.ModifyAzureDevices(azuredevices);

return azuredevices;
};

// ** modify a single ldap devicer entry, e.g. add more attributes from azure, assign a different default group, ... ** /
customizer.ModifyLDAPDevice = function (ldapdevice, azuredevice) {
if (typeof customizer_script.ModifyLDAPDevice !== "undefined") ldapdevice = customizer_script.ModifyLDAPDevice(ldapdevice, azuredevice);

return ldapdevice;
};
// ** modify some overall attributes/entries ** /
customizer.ModifyLDAPGlobal = function (all) {
if (typeof customizer_script.ModifyLDAPGlobal !== "undefined") all = customizer_script.ModifyLDAPGlobal(all);
Expand Down
126 changes: 123 additions & 3 deletions src/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ function mergeDnUsers(db) {
"modifyTimestamp": helper.ldap_now() + "Z",
};
}
/**
* Create and/or merge the LDAP entry for Devices
* @param {Object} db existing db to merge
*/
function mergeDnDevices(db) {
if (!config.LDAP_GETDEVICES) return;
renameEntryByUUID(db, 'ecc6dc2f-6816-4e50-bd6d-9e6e59593fab', config.LDAP_DEVICESDN);

db[config.LDAP_DEVICESDN] = {
// default values
"objectClass": "organizationalRole",
"cn": config.LDAP_DEVICESDN.replace("," + config.LDAP_BASEDN, '').replace('cn=', ''),
"entryDN": config.LDAP_DEVICESDN,
"entryUUID": "ecc6dc2f-6816-4e50-bd6d-9e6e59593fab",
"structuralObjectClass": "organizationalRole",
"hasSubordinates": "TRUE",
"subschemaSubentry": "cn=subschema",
"createTimestamp": helper.ldap_now() + "Z",
"entryCSN": helper.ldap_now() + ".000000Z#000000#000#000000",
"modifyTimestamp": helper.ldap_now() + "Z",

// merge existing values
...db[config.LDAP_DEVICESDN],

// overwrite values from before
"cn": config.LDAP_DEVICESDN.replace("," + config.LDAP_BASEDN, '').replace('cn=', ''),
"entryDN": config.LDAP_DEVICESDN,
"entryCSN": helper.ldap_now() + ".000000Z#000000#000#000000",
"modifyTimestamp": helper.ldap_now() + "Z",
};
}

/**
* Create and/or merge the LDAP entry for Groups
Expand Down Expand Up @@ -381,6 +412,7 @@ async function refreshDBentries() {
mergeDnBase(newDbEntries); // domain
mergeDnSambaDomainName(newDbEntries); // samba
mergeDnUsers(newDbEntries); // users
mergeDnDevices(newDbEntries); // devices
mergeDnGroups(newDbEntries); // groups
mergeDnUserDefaultGroup(newDbEntries); // default group for all users
await mergeAzureEntries(newDbEntries); // load and append users and groups from azure
Expand Down Expand Up @@ -414,6 +446,7 @@ async function mergeAzureGroupEntries(db) {

db['tmp_user_to_groups'] = [];
db['tmp_nested_groups'] = [];
db['tmp_device_to_groups'] = [];

for (let i = 0, len = groups.length; i < len; i++) {
let group = groups[i];
Expand Down Expand Up @@ -498,6 +531,11 @@ async function mergeAzureGroupEntries(db) {
if (!db['tmp_nested_groups'][member.id].includes(gpName))
db['tmp_nested_groups'][member.id].push(gpName);
}
if (member['@odata.type'] == '#microsoft.graph.device' && config.LDAP_GETDEVICES) {
db['tmp_device_to_groups'][member.id] = db['tmp_device_to_groups'][member.id] || [];
if (!db['tmp_device_to_groups'][member.id].includes(gpName))
db['tmp_device_to_groups'][member.id].push(gpName);
}
}
}

Expand Down Expand Up @@ -638,13 +676,12 @@ async function mergeAzureUserEntries(db) {
helper.log("database.js", "no groups found for user", upName);
db['tmp_user_to_groups'][user.id] = [];
}

// add default `users`-group
db['tmp_user_to_groups'][user.id].push(config.LDAP_USERSGROUPSBASEDN);

for (let j = 0, jlen = db['tmp_user_to_groups'][user.id].length; j < jlen; j++) {
let g = db['tmp_user_to_groups'][user.id][j];

if (!db[g].member.includes(upName))
db[g].member.push(upName);

Expand Down Expand Up @@ -757,14 +794,97 @@ async function mergeAzureUserEntries(db) {
}

/**
* Create and/or merge the LDAP entries for Azure Users and Groups
* Create and/or merge the LDAP entries for Azure Devices
* @param {Object} db existing db to merge
*/
async function mergeAzureDeviceEntries(db) {
if (config.LDAP_GETDEVICES) {
helper.log("database.js", "mergeAzureDevicesEntries", "try fetching the devices");
const devices = await fetch.getDevices();
//const groups = await fetch.getGroups();

if (devices.length > 0) {
helper.SaveJSONtoFile(devices, './.cache/devices.json');
helper.log("database.js", "devices.json saved.");
}

for (let i = 0, len = devices.length; i < len; i++) {
let device = devices[i];
let deviceDisplayName = device.displayName; //.replace(/\s/g, '');
let deviceDisplayNameClean = removeSpecialChars(deviceDisplayName);

if (deviceDisplayName !== deviceDisplayNameClean) {
helper.warn("database.js", 'device names may not contain any special chars. We are using ', deviceDisplayNameClean, 'instead of', deviceDisplayName);
}

let devName = "cn=" + deviceDisplayNameClean + "," + config.LDAP_DEVICESDN;
devName = devName.toLowerCase();

renameEntryByUUID(db, device.id, devName);

if (typeof db['tmp_device_to_groups'][device.id] === 'undefined' || !db['tmp_device_to_groups'][device.id]) {
helper.log("database.js", "no groups found for user", devName);
db['tmp_device_to_groups'][device.id] = [];
}

for (let j = 0, jlen = db['tmp_device_to_groups'][device.id].length; j < jlen; j++) {
let g = db['tmp_device_to_groups'][device.id][j];
let gp = Object.values(db).find(x => x.entryDN.includes(g) && x.objectClass.includes('posixGroup')).entryDN;
//helper.log("database.js", "gp", gp);
if (gp) {
//helper.log("database.js", "instance of gp", gp);
if (!db[gp].member.includes(devName))
db[gp].member.push(devName);
}
}

db[devName] = {
// default values
"objectClass": [
"top",
"device",
"extensibleObject"
],
"cn": deviceDisplayNameClean.toLowerCase(),
"displayName": deviceDisplayName,
"entryDN": devName,
"memberOf": db['tmp_device_to_groups'][device.id],
"entryUUID": device.id,
"structuralObjectClass": "device",
"hasSubordinates": "FALSE",
"subschemaSubentry": "cn=subschema",
"createTimestamp": helper.ldap_now() + "Z",
"entryCSN": helper.ldap_now() + ".000000Z#000000#000#000000",
"modifyTimestamp": helper.ldap_now() + "Z",

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

// overwrite values from before
"cn": deviceDisplayNameClean.toLowerCase(),
"entryDN": devName,
"displayName": deviceDisplayName,
"memberOf": db['tmp_device_to_groups'][device.id],
"entryCSN": helper.ldap_now() + ".000000Z#000000#000#000000",
"modifyTimestamp": helper.ldap_now() + "Z",
};

db[devName] = customizer.ModifyLDAPDevice(db[devName], device);
}
}
delete db['tmp_device_to_groups'];
}

/**
* Create and/or merge the LDAP entries for Azure Devices, Users and Groups
* @param {Object} db existing db to merge
*/
async function mergeAzureEntries(db) {
try {
await fetch.initAccessToken();
await mergeAzureGroupEntries(db);
await mergeAzureUserEntries(db);
await mergeAzureDeviceEntries(db);
} catch (error) {
helper.error('database.js', 'mergeAzureEntries', error);
}
Expand Down
15 changes: 15 additions & 0 deletions src/graph.fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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,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`,
dri: `${config.GRAPH_ENDPOINT}/${config.GRAPH_API_VERSION}/devices?$count=true${addFilter(config.GRAPH_FILTER_DEVICES)}`,
};

// Settings can be overwritten by a customizer
Expand Down Expand Up @@ -83,6 +84,20 @@ fetch.getUsers = async function () {
return users;
};

/**
* Fetches all devices from Graph API
* @async
* @returns {Promise<Array<object>>} A promise that resolves to an array of device objects.
*/
fetch.getDevices = async function() {
let devices = await fetch.callApi(fetch.apiConfig.dri, accessToken);
if (devices.length === 0) {
helper.warn("graph.fetch.js", "getDevices()", "no devices found");
}
devices = customizer.ModifyAzureDevices(devices);
return devices;
};

/**
* Calls the endpoint with authorization bearer token.
* @async
Expand Down
1 change: 1 addition & 0 deletions tests/Test3.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ AZURE_APP_ID = "none"
AZURE_APP_SECRET= "none"
AZURE_TENANTID= "none"
LDAP_DEBUG=true
LDAP_GETDEVICES=true
LDAP_PORT=65537
LDAP_SYNC_TIME=0
LDAP_DOMAIN=domain.tld
Expand Down
7 changes: 7 additions & 0 deletions tests/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ describe('config tests', () => {
expect(config.LDAP_USERRDN).toBe("uid");

});
test('LDAP devices configs', () => {
// Run your test here
expect(config.LDAP_GETDEVICES).toBe(exDebug);
expect(config.LDAP_DEVICESDN).toBe(`cn=devices,${exDN}`);
expect(config.LDAP_DEVICESGROUPSBASEDN).toBe(`cn=devices,cn=groups,${exDN}`);

});

test('VARS VALIDATED', () => {
expect(config.VARS_VALIDATED).toBe(exVali);
Expand Down
10 changes: 8 additions & 2 deletions tests/customizer/customizer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ describe('customizer tests', () => {
expect(customizer.ModifyLDAPGroup(data, {})).toStrictEqual(data);
expect(customizer.ModifyAzureUsers(data, {})).toStrictEqual(data);
expect(customizer.ModifyLDAPUser(data, {})).toStrictEqual(data);
expect(customizer.ModifyAzureDevices(data, {})).toStrictEqual(data);
expect(customizer.ModifyLDAPDevice(data, {})).toStrictEqual(data);
expect(customizer.ModifyLDAPGlobal(data)).toStrictEqual(data);

});
Expand Down Expand Up @@ -86,18 +88,22 @@ describe('customizer tests #2', () => {

const user = { "gidNumber": "123", "uidNumber": "456", "creatorsName": "none" };
const group = { "gidNumber": "123", "creatorsName": "none" };
const all = { "user": user, "group": group, "samba": { "sambaDomainName": "SAMBA" } };
const device = { "displayName": "Pixel 7", "creatorsName": "none" };
const all = { "user": user, "group": group, "samba": { "sambaDomainName": "SAMBA" }, "device": device };

expect(customizer.ModifyLDAPGroup(group, {})).toStrictEqual(group);
expect(customizer.ModifyLDAPUser(user, {})).toStrictEqual(user);
expect(customizer.ModifyLDAPDevice(device, {})).toStrictEqual(device);
expect(customizer.ModifyLDAPGlobal(all)).toStrictEqual(all);

expect(customizer.ModifyLDAPGroup(group, {})).toMatchObject(group);
expect(customizer.ModifyLDAPUser(user, {})).toMatchObject(user);
expect(customizer.ModifyLDAPDevice(device, {})).toMatchObject(device);
expect(customizer.ModifyLDAPGlobal(all)).toMatchObject(all);

expect(customizer.ModifyLDAPGroup(group, {}).gidNumber).toBe(123);
expect(customizer.ModifyLDAPUser(user, {}).uidNumber).toBe(456);
expect(customizer.ModifyLDAPDevice(device, {}).displayName).toBe("Pixel 7");
expect(customizer.ModifyLDAPGlobal(all).creatorsName).toBe(undefined);

expect(customizer.modifyGraphApiConfig({})).toStrictEqual({});
Expand All @@ -121,7 +127,7 @@ describe('customizer tests #2', () => {

/* copy/paste tests from customizer_add_customSecurityAttributes.test.js: START */

const apiConfig = { "uri": "uri", "gri": "gri", "mri": "mri" };
const apiConfig = { "uri": "uri", "gri": "gri", "mri": "mri", "dri":"dri" };
expect(customizer.modifyGraphApiConfig(apiConfig, "hello-wordl")).toStrictEqual(apiConfig);
expect(customizer.modifyGraphApiConfig(apiConfig, "hello-wordl")).toMatchObject(apiConfig);

Expand Down

0 comments on commit 8779faa

Please sign in to comment.