Skip to content

Commit 9d96614

Browse files
authored
Merge pull request #972 from kriswest/909-ldap-user-group-confirmation
feat: support direct querying of AD group membership via LDAP
2 parents 72d43e4 + 6a3146d commit 9d96614

File tree

6 files changed

+767
-79
lines changed

6 files changed

+767
-79
lines changed

config.schema.json

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,32 @@
1010
"sessionMaxAgeHours": { "type": "number" },
1111
"api": {
1212
"description": "Third party APIs",
13-
"type": "object"
13+
"type": "object",
14+
"properties": {
15+
"ls": {
16+
"type": "object",
17+
"description": "Configuration used in conjunction with ActiveDirectory auth, which relates to a REST API used to check user group membership, as opposed to direct querying via LDAP.<br />If this configuration is set direct querying of group membership via LDAP will be disabled.",
18+
"properties": {
19+
"userInADGroup": {
20+
"type": "string",
21+
"description": "URL template for a GET request that confirms a user's membership of a specific group. Should respond with a non-empty 200 status if the user is a member of the group, an empty response or non-200 status indicates that the user is not a group member. If set, this URL will be queried and direct queries via LDAP will be disabled. The template should contain the following string placeholders, which will be replaced to produce the final URL:<ul><li>\"&lt;domain&gt;\": AD domain,</li><li>\"&lt;name&gt;\": The group name to check membership of.</li><li>\"&lt;id&gt;\": The username to check group membership for.</li></ul>",
22+
"examples": [
23+
"https://somedomain.com/some/path/checkUserGroups?domain=<domain>&name=<name>&id=<id>"
24+
]
25+
}
26+
}
27+
},
28+
"github": {
29+
"type": "object",
30+
"properties": {
31+
"baseUrl": {
32+
"type": "string",
33+
"format": "uri",
34+
"examples": ["https://api.github.com"]
35+
}
36+
}
37+
}
38+
}
1439
},
1540
"commitConfig": {
1641
"description": "Enforce rules and patterns on commits including e-mail and message",
@@ -169,12 +194,98 @@
169194
},
170195
"authentication": {
171196
"type": "object",
172-
"properties": {
173-
"type": { "type": "string" },
174-
"enabled": { "type": "boolean" },
175-
"options": { "type": "object" }
176-
},
177-
"required": ["type", "enabled"]
197+
"description": "Configuration for an authentication source",
198+
"oneOf": [
199+
{
200+
"title": "Local Auth Config",
201+
"description": "Configuration for the use of the local database as the authentication source.",
202+
"properties": {
203+
"type": { "type": "string", "const": "local" },
204+
"enabled": { "type": "boolean" }
205+
},
206+
"required": ["type", "enabled"]
207+
},
208+
{
209+
"title": "Active Directory Auth Config",
210+
"description": "Configuration for Active Directory authentication.",
211+
"properties": {
212+
"type": { "type": "string", "const": "ActiveDirectory" },
213+
"enabled": { "type": "boolean" },
214+
"adminGroup": {
215+
"type": "string",
216+
"description": "Group that indicates that a user is an admin"
217+
},
218+
"userGroup": {
219+
"type": "string",
220+
"description": "Group that indicates that a user should be able to login to the Git Proxy UI and can work as a reviewer"
221+
},
222+
"domain": { "type": "string", "description": "Active Directory domain" },
223+
"adConfig": {
224+
"type": "object",
225+
"description": "Additional Active Directory configuration supporting LDAP connection which can be used to confirm group membership. For the full set of available options see the activedirectory 2 NPM module docs at https://www.npmjs.com/package/activedirectory2#activedirectoryoptions <br /><br />Please note that if the Third Party APIs config `api.ls.userInADGroup` is set then the REST API it represents is used in preference to direct querying of group memebership via LDAP.",
226+
"properties": {
227+
"url": {
228+
"type": "string",
229+
"description": "Active Directory server to connect to, e.g. `ldap://ad.example.com`."
230+
},
231+
"baseDN": {
232+
"type": "string",
233+
"description": "The root DN from which all searches will be performed, e.g. `dc=example,dc=com`."
234+
},
235+
"username": {
236+
"type": "string",
237+
"description": "An account name capable of performing the operations desired."
238+
},
239+
"password": {
240+
"type": "string",
241+
"description": "Password for the given `username`."
242+
}
243+
},
244+
"required": ["url", "baseDN", "username", "password"]
245+
}
246+
},
247+
"required": ["type", "enabled", "adminGroup", "userGroup", "domain"]
248+
},
249+
{
250+
"title": "Open ID Connect Auth Config",
251+
"description": "Configuration for Open ID Connect authentication.",
252+
"properties": {
253+
"type": { "type": "string", "const": "openidconnect" },
254+
"enabled": { "type": "boolean" },
255+
"oidcConfig": {
256+
"type": "object",
257+
"description": "Additional OIDC configuration.",
258+
"properties": {
259+
"issuer": { "type": "string" },
260+
"clientID": { "type": "string" },
261+
"clientSecret": { "type": "string" },
262+
"callbackURL": { "type": "string" },
263+
"scope": { "type": "string" }
264+
},
265+
"required": ["issuer", "clientID", "clientSecret", "callbackURL", "scope"]
266+
}
267+
},
268+
"required": ["type", "enabled", "oidcConfig"]
269+
},
270+
{
271+
"title": "JWT Auth Config",
272+
"description": "Configuration for JWT authentication.",
273+
"properties": {
274+
"type": { "type": "string", "const": "jwt" },
275+
"enabled": { "type": "boolean" },
276+
"jwtConfig": {
277+
"type": "object",
278+
"description": "Additional JWT configuration.",
279+
"properties": {
280+
"clientID": { "type": "string" },
281+
"authorityURL": { "type": "string" }
282+
},
283+
"required": ["clientID", "authorityURL"]
284+
}
285+
},
286+
"required": ["type", "enabled", "jwtConfig"]
287+
}
288+
]
178289
},
179290
"routeAuthRule": {
180291
"type": "object",

proxy.config.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@
5151
"adConfig": {
5252
"url": "",
5353
"baseDN": "",
54-
"searchBase": ""
54+
"searchBase": "",
55+
"username": "",
56+
"password": ""
5557
}
5658
},
5759
{

src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const getDatabase = () => {
8787
}
8888
}
8989

90-
throw Error('No database cofigured!');
90+
throw Error('No database configured!');
9191
};
9292

9393
/**

src/service/passport/activeDirectory.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,35 @@ const configure = (passport) => {
3131
profile.id = profile.username;
3232
req.user = profile;
3333

34-
// First check to see if the user is in the usergroups
35-
const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup);
36-
37-
if (!isUser) {
38-
const message = `User it not a member of ${userGroup}`;
34+
console.log(
35+
`passport.activeDirectory: resolved login ${
36+
profile._json.userPrincipalName
37+
}, profile=${JSON.stringify(profile)}`,
38+
);
39+
// First check to see if the user is in the AD user group
40+
try {
41+
const isUser = await ldaphelper.isUserInAdGroup(req, profile, ad, domain, userGroup);
42+
if (!isUser) {
43+
const message = `User it not a member of ${userGroup}`;
44+
return done(message, null);
45+
}
46+
} catch (e) {
47+
const message = `An error occurred while checking if the user is a member of the user group: ${JSON.stringify(e)}`;
3948
return done(message, null);
4049
}
41-
50+
4251
// Now check if the user is an admin
43-
const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup);
52+
let isAdmin = false;
53+
try {
54+
isAdmin = await ldaphelper.isUserInAdGroup(req, profile, ad, domain, adminGroup);
55+
56+
} catch (e) {
57+
const message = `An error occurred while checking if the user is a member of the admin group: ${JSON.stringify(e)}`;
58+
console.error(message, e); // don't return an error for this case as you may still be a user
59+
}
4460

4561
profile.admin = isAdmin;
62+
console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`);
4663

4764
const user = {
4865
username: profile.username,
@@ -70,6 +87,7 @@ const configure = (passport) => {
7087
passport.deserializeUser(function (user, done) {
7188
done(null, user);
7289
});
90+
passport.type = "ActiveDirectory";
7391

7492
return passport;
7593
};

src/service/passport/ldaphelper.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
1-
const axios = require('axios');
21
const thirdpartyApiConfig = require('../../config').getAPIs();
3-
const client = axios.create({
4-
responseType: 'json',
5-
headers: {
6-
'content-type': 'application/json',
7-
},
8-
});
2+
const axios = require('axios');
3+
4+
const isUserInAdGroup = (req, profile, ad, domain, name) => {
5+
// determine, via config, if we're using HTTP or AD directly
6+
if (thirdpartyApiConfig?.ls?.userInADGroup) {
7+
return isUserInAdGroupViaHttp(profile.username, domain, name);
8+
} else {
9+
return isUserInAdGroupViaAD(req, profile, ad, domain, name);
10+
}
11+
};
912

10-
const isUserInAdGroup = (id, domain, name) => {
13+
const isUserInAdGroupViaAD = (req, profile, ad, domain, name) => {
14+
return new Promise((resolve, reject) => {
15+
ad.isUserMemberOf(profile.username, name, function (err, isMember) {
16+
if (err) {
17+
const msg = 'ERROR isUserMemberOf: ' + JSON.stringify(err);
18+
reject(msg);
19+
} else {
20+
console.log(profile.username + ' isMemberOf ' + name + ': ' + isMember);
21+
resolve(isMember);
22+
}
23+
});
24+
});
25+
};
26+
27+
const isUserInAdGroupViaHttp = (id, domain, name) => {
1128
const url = String(thirdpartyApiConfig.ls.userInADGroup)
1229
.replace('<domain>', domain)
1330
.replace('<name>', name)
1431
.replace('<id>', id);
1532

33+
const client = axios.create({
34+
responseType: 'json',
35+
headers: {
36+
'content-type': 'application/json',
37+
},
38+
});
39+
1640
console.log(`checking if user is in group ${url}`);
1741
return client
1842
.get(url)

0 commit comments

Comments
 (0)