Initial implementation for supporitng Active Directory organizations
Signed-off-by: Mathias Åhsberg <mathias.ahsberg@resurs.se>
This commit is contained in:
@@ -17,6 +17,11 @@
|
||||
import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs';
|
||||
import { BindConfig } from './config';
|
||||
import { errorString } from './util';
|
||||
import {
|
||||
ActiveDirectoryVendor,
|
||||
DefaultLdapVendor,
|
||||
LdapVendor,
|
||||
} from './vendors';
|
||||
|
||||
/**
|
||||
* Basic wrapper for the ldapjs library.
|
||||
@@ -24,6 +29,8 @@ import { errorString } from './util';
|
||||
* Helps out with promisifying calls, paging, binding etc.
|
||||
*/
|
||||
export class LdapClient {
|
||||
private vendor: Promise<LdapVendor> | undefined;
|
||||
|
||||
static async create(target: string, bind?: BindConfig): Promise<LdapClient> {
|
||||
const client = ldap.createClient({ url: target });
|
||||
if (!bind) {
|
||||
@@ -88,4 +95,44 @@ export class LdapClient {
|
||||
throw new Error(`LDAP search at ${dn} failed, ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Server Vendor.
|
||||
* Currently only detects Microsoft Active Directory Servers.
|
||||
*
|
||||
* @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor
|
||||
*/
|
||||
async getVendor(): Promise<LdapVendor> {
|
||||
if (this.vendor) {
|
||||
return this.vendor;
|
||||
}
|
||||
this.vendor = this.getRootDSE()
|
||||
.then(root => {
|
||||
if (root && root.raw?.forestFunctionality) {
|
||||
return ActiveDirectoryVendor;
|
||||
}
|
||||
return DefaultLdapVendor;
|
||||
})
|
||||
.catch(err => {
|
||||
this.vendor = undefined;
|
||||
throw err;
|
||||
});
|
||||
return this.vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Root DSE.
|
||||
*
|
||||
* @see https://ldapwiki.com/wiki/RootDSE
|
||||
*/
|
||||
async getRootDSE(): Promise<SearchEntry | undefined> {
|
||||
const result = await this.search('', {
|
||||
scope: 'base',
|
||||
filter: '(objectclass=*)',
|
||||
} as SearchOptions);
|
||||
if (result && result.length === 1) {
|
||||
return result[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,11 @@ import {
|
||||
LDAP_RDN_ANNOTATION,
|
||||
LDAP_UUID_ANNOTATION,
|
||||
} from './constants';
|
||||
|
||||
// NOTE(freben): These attribute names are assumed to be the same across all
|
||||
// compliant LDAP implementations
|
||||
const DN_ATTRIBUTE = 'entryDN';
|
||||
const UUID_ATTRIBUTE = 'entryUUID';
|
||||
import { LdapVendor } from './vendors';
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
|
||||
/**
|
||||
* Reads groups out of an LDAP provider.
|
||||
* Reads users out of an LDAP provider.
|
||||
*
|
||||
* @param client The LDAP client
|
||||
* @param config The user data configuration
|
||||
@@ -44,6 +41,7 @@ export async function readLdapUsers(
|
||||
userMemberOf: Map<string, Set<string>>; // DN -> DN or UUID of groups
|
||||
}> {
|
||||
const { dn, options, set, map } = config;
|
||||
const vendor = await client.getVendor();
|
||||
|
||||
const entries = await client.search(dn, options);
|
||||
|
||||
@@ -51,13 +49,6 @@ export async function readLdapUsers(
|
||||
const userMemberOf: Map<string, Set<string>> = new Map();
|
||||
|
||||
for (const entry of entries) {
|
||||
const attributes = new Map(
|
||||
entry.attributes.map(attr => {
|
||||
const data = attr.json;
|
||||
return [data.type, data.vals];
|
||||
}),
|
||||
);
|
||||
|
||||
const entity: UserEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
@@ -77,32 +68,32 @@ export async function readLdapUsers(
|
||||
}
|
||||
}
|
||||
|
||||
mapStringAttr(attributes, map.name, v => {
|
||||
mapStringAttr(entry, vendor, map.name, v => {
|
||||
entity.metadata.name = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.description, v => {
|
||||
mapStringAttr(entry, vendor, map.description, v => {
|
||||
entity.metadata.description = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.rdn, v => {
|
||||
mapStringAttr(entry, vendor, map.rdn, v => {
|
||||
entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, UUID_ATTRIBUTE, v => {
|
||||
mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => {
|
||||
entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, DN_ATTRIBUTE, v => {
|
||||
mapStringAttr(entry, vendor, vendor.dnAttributeName, v => {
|
||||
entity.metadata.annotations![LDAP_DN_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.displayName, v => {
|
||||
mapStringAttr(entry, vendor, map.displayName, v => {
|
||||
entity.spec.profile!.displayName = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.email, v => {
|
||||
mapStringAttr(entry, vendor, map.email, v => {
|
||||
entity.spec.profile!.email = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.picture, v => {
|
||||
mapStringAttr(entry, vendor, map.picture, v => {
|
||||
entity.spec.profile!.picture = v;
|
||||
});
|
||||
|
||||
mapReferencesAttr(attributes, map.memberOf, (myDn, vs) => {
|
||||
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(userMemberOf, myDn, vs);
|
||||
});
|
||||
|
||||
@@ -127,6 +118,8 @@ export async function readLdapGroups(
|
||||
groupMember: Map<string, Set<string>>; // DN -> DN or UUID of groups & users
|
||||
}> {
|
||||
const { dn, options, set, map } = config;
|
||||
const vendor = await client.getVendor();
|
||||
|
||||
const entries = await client.search(dn, options);
|
||||
|
||||
const groups: GroupEntity[] = [];
|
||||
@@ -134,13 +127,6 @@ export async function readLdapGroups(
|
||||
const groupMember: Map<string, Set<string>> = new Map();
|
||||
|
||||
for (const entry of entries) {
|
||||
const attributes = new Map(
|
||||
entry.attributes.map(attr => {
|
||||
const data = attr.json;
|
||||
return [data.type, data.vals];
|
||||
}),
|
||||
);
|
||||
|
||||
const entity: GroupEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
@@ -161,38 +147,38 @@ export async function readLdapGroups(
|
||||
}
|
||||
}
|
||||
|
||||
mapStringAttr(attributes, map.name, v => {
|
||||
mapStringAttr(entry, vendor, map.name, v => {
|
||||
entity.metadata.name = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.description, v => {
|
||||
mapStringAttr(entry, vendor, map.description, v => {
|
||||
entity.metadata.description = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.rdn, v => {
|
||||
mapStringAttr(entry, vendor, map.rdn, v => {
|
||||
entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, UUID_ATTRIBUTE, v => {
|
||||
mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => {
|
||||
entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, DN_ATTRIBUTE, v => {
|
||||
mapStringAttr(entry, vendor, vendor.dnAttributeName, v => {
|
||||
entity.metadata.annotations![LDAP_DN_ANNOTATION] = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.type, v => {
|
||||
mapStringAttr(entry, vendor, map.type, v => {
|
||||
entity.spec.type = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.displayName, v => {
|
||||
mapStringAttr(entry, vendor, map.displayName, v => {
|
||||
entity.spec.profile!.displayName = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.email, v => {
|
||||
mapStringAttr(entry, vendor, map.email, v => {
|
||||
entity.spec.profile!.email = v;
|
||||
});
|
||||
mapStringAttr(attributes, map.picture, v => {
|
||||
mapStringAttr(entry, vendor, map.picture, v => {
|
||||
entity.spec.profile!.picture = v;
|
||||
});
|
||||
|
||||
mapReferencesAttr(attributes, map.memberOf, (myDn, vs) => {
|
||||
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(groupMemberOf, myDn, vs);
|
||||
});
|
||||
mapReferencesAttr(attributes, map.members, (myDn, vs) => {
|
||||
mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => {
|
||||
ensureItems(groupMember, myDn, vs);
|
||||
});
|
||||
|
||||
@@ -244,12 +230,13 @@ export async function readLdapOrg(
|
||||
|
||||
// Maps a single-valued attribute to a consumer
|
||||
function mapStringAttr(
|
||||
attributes: Map<string, string[]>,
|
||||
entry: SearchEntry,
|
||||
vendor: LdapVendor,
|
||||
attributeName: string | undefined,
|
||||
setter: (value: string) => void,
|
||||
) {
|
||||
if (attributeName) {
|
||||
const values = attributes.get(attributeName);
|
||||
const values = vendor.decodeStringAttribute(entry, attributeName);
|
||||
if (values && values.length === 1) {
|
||||
setter(values[0]);
|
||||
}
|
||||
@@ -258,13 +245,14 @@ function mapStringAttr(
|
||||
|
||||
// Maps a multi-valued attribute of references to other objects, to a consumer
|
||||
function mapReferencesAttr(
|
||||
attributes: Map<string, string[]>,
|
||||
entry: SearchEntry,
|
||||
vendor: LdapVendor,
|
||||
attributeName: string | undefined,
|
||||
setter: (sourceDn: string, targets: string[]) => void,
|
||||
) {
|
||||
if (attributeName) {
|
||||
const values = attributes.get(attributeName);
|
||||
const dn = attributes.get(DN_ATTRIBUTE);
|
||||
const values = vendor.decodeStringAttribute(entry, attributeName);
|
||||
const dn = vendor.decodeStringAttribute(entry, vendor.dnAttributeName);
|
||||
if (values && dn && dn.length === 1) {
|
||||
setter(dn[0], values);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
|
||||
/**
|
||||
* An LDAP Vendor handles unique nuances between different vendors.
|
||||
*/
|
||||
export type LdapVendor = {
|
||||
/**
|
||||
* The attribute name that holds the distinguished name (DN) for an entry.
|
||||
*/
|
||||
dnAttributeName: string;
|
||||
/**
|
||||
* The attribute name that holds a universal unique identifier for an entry.
|
||||
*/
|
||||
uuidAttributeName: string;
|
||||
/**
|
||||
* Decode ldap entry values for a given attribute name to their string representation.
|
||||
*
|
||||
* @param entry The ldap entry
|
||||
* @param name The attribute to decode
|
||||
*/
|
||||
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
|
||||
};
|
||||
|
||||
export const DefaultLdapVendor: LdapVendor = {
|
||||
dnAttributeName: 'entryDN',
|
||||
uuidAttributeName: 'entryUUID',
|
||||
decodeStringAttribute: (entry, name) => {
|
||||
return decode(entry, name, value => {
|
||||
return value.toString();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const ActiveDirectoryVendor: LdapVendor = {
|
||||
dnAttributeName: 'distinguishedName',
|
||||
uuidAttributeName: 'objectGUID',
|
||||
decodeStringAttribute: (entry, name) => {
|
||||
const decoder = (value: string | Buffer) => {
|
||||
if (name === ActiveDirectoryVendor.uuidAttributeName) {
|
||||
return formatGUID(value);
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
return decode(entry, name, decoder);
|
||||
},
|
||||
};
|
||||
|
||||
// Decode an attribute to a consumer
|
||||
function decode(
|
||||
entry: SearchEntry,
|
||||
attributeName: string,
|
||||
decoder: (value: string | Buffer) => string,
|
||||
): string[] {
|
||||
const values = entry.raw[attributeName];
|
||||
if (Array.isArray(values)) {
|
||||
return (values as Buffer[]).map(v => {
|
||||
return decoder(v);
|
||||
});
|
||||
} else if (values) {
|
||||
return [decoder(values)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Formats a Microsoft Active Directory binary-encoded uuid to a readable string
|
||||
// See https://github.com/ldapjs/node-ldapjs/issues/297#issuecomment-137765214
|
||||
function formatGUID(objectGUID: string | Buffer | Buffer[]): string {
|
||||
let data: Buffer | Buffer[];
|
||||
if (typeof objectGUID === 'string') {
|
||||
data = new Buffer(objectGUID, 'binary');
|
||||
} else {
|
||||
data = objectGUID;
|
||||
}
|
||||
// GUID_FORMAT_D
|
||||
let template = '{3}{2}{1}{0}-{5}{4}-{7}{6}-{8}{9}-{10}{11}{12}{13}{14}{15}';
|
||||
|
||||
// check each byte
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
// @ts-ignore
|
||||
let dataStr = data[i].toString(16);
|
||||
dataStr = data[i] >= 16 ? dataStr : `0${dataStr}`;
|
||||
|
||||
// insert that character into the template
|
||||
template = template.replace(new RegExp(`\\{${i}\\}`, 'g'), dataStr);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
Reference in New Issue
Block a user