Skip to content

Commit 1400d9b

Browse files
committed
feat: support direct querying of AD group membership via LDAP
1 parent 38791bd commit 1400d9b

File tree

11 files changed

+400
-107
lines changed

11 files changed

+400
-107
lines changed

.husky/commit-msg

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
3-
41
npx --no -- commitlint --edit ${1} && npm run lint

config.schema.json

Lines changed: 57 additions & 3 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",
@@ -103,10 +128,39 @@
103128
},
104129
"authentication": {
105130
"type": "object",
131+
"description": "Configuration for an authentication source",
106132
"properties": {
107-
"type": { "type": "string" },
133+
"type": { "type": "string", "enum": ["local", "ActiveDirectory", "OpenIdConnect"] },
108134
"enabled": { "type": "boolean" },
109-
"options": { "type": "object" }
135+
"adminGroup": {
136+
"type": "string",
137+
"description": "Group that indicates that a user is an admin"
138+
},
139+
"userGroup": {
140+
"type": "string",
141+
"description": "Group that indicates that a user should be able to login to the Git Proxy UI and can work as a reviewer"
142+
},
143+
"domain": { "type": "string", "description": "Active Directory domain" },
144+
"adConfig": {
145+
"type": "object",
146+
"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.",
147+
"properties": {
148+
"url": {
149+
"type": "string",
150+
"description": "Active Directory server to connect to, e.g. `ldap://ad.example.com`."
151+
},
152+
"baseDN": {
153+
"type": "string",
154+
"description": "The root DN from which all searches will be performed, e.g. `dc=example,dc=com`."
155+
},
156+
"username": {
157+
"type": "string",
158+
"description": "An account name capable of performing the operations desired."
159+
},
160+
"password": { "type": "string", "description": "Password for the given `username`." }
161+
},
162+
"required": ["url", "baseDN", "username", "password"]
163+
}
110164
},
111165
"required": ["type", "enabled"]
112166
}

proxy.config.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
"adConfig": {
4848
"url": "",
4949
"baseDN": "",
50-
"searchBase": ""
50+
"searchBase": "",
51+
"username": "",
52+
"password": ""
5153
}
5254
}
5355
],

src/config/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const getDatabase = () => {
6969
}
7070
}
7171

72-
throw Error('No database cofigured!');
72+
throw Error('No database configured!');
7373
};
7474

7575
// Gets the configured authentication method, defaults to local
@@ -85,7 +85,7 @@ export const getAuthentication = () => {
8585
}
8686
}
8787

88-
throw Error('No authentication cofigured!');
88+
throw Error('No authentication configured!');
8989
};
9090

9191
// Log configuration to console
@@ -170,7 +170,7 @@ export const getPlugins = () => {
170170
_plugins = _userSettings.plugins;
171171
}
172172
return _plugins;
173-
}
173+
};
174174

175175
export const getSSLKeyPath = () => {
176176
if (_userSettings && _userSettings.sslKeyPemPath) {

src/proxy/processors/push-action/checkCommitMessages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const exec = async (req: any, action: Action): Promise<Action> => {
7070
step.error = true;
7171
step.log(`The following commit messages are illegal: ${illegalMessages}`);
7272
step.setError(
73-
'\n\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\n\n',
73+
`\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\nThe following commit messages are illegal: ${illegalMessages}\n\n`,
7474
);
7575

7676
action.addStep(step);

src/service/passport/activeDirectory.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,27 @@ const configure = () => {
2929
profile._json.userPrincipalName
3030
}, profile=${JSON.stringify(profile)}`,
3131
);
32-
// First check to see if the user is in the usergroups
33-
const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup);
34-
35-
if (!isUser) {
36-
const message = `User it not a member of ${userGroup}`;
32+
// First check to see if the user is in the AD user group
33+
try {
34+
const isUser = await ldaphelper.isUserInAdGroup(req, profile, ad, domain, userGroup);
35+
if (!isUser) {
36+
const message = `User it not a member of ${userGroup}`;
37+
return done(message, null);
38+
}
39+
} catch (e) {
40+
const message = `An error occurred while checking if the user is a member of the user group: ${JSON.stringify(e)}`;
3741
return done(message, null);
3842
}
39-
43+
4044
// Now check if the user is an admin
41-
const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup);
45+
let isAdmin = false;
46+
try {
47+
isAdmin = await ldaphelper.isUserInAdGroup(req, profile, ad, domain, adminGroup);
48+
49+
} catch (e) {
50+
const message = `An error occurred while checking if the user is a member of the admin group: ${JSON.stringify(e)}`;
51+
console.error(message, e);
52+
}
4253

4354
profile.admin = isAdmin;
4455
console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`);
@@ -65,6 +76,7 @@ const configure = () => {
6576
passport.deserializeUser(function (user, done) {
6677
done(null, user);
6778
});
79+
passport.type = "ActiveDirectory";
6880

6981
return passport;
7082
};

src/service/passport/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const configure = async () => {
1919
_passport = await oidc.configure();
2020
break;
2121
default:
22-
throw Error(`uknown authentication type ${type}`);
22+
throw Error(`unknown authentication type ${type}`);
2323
}
2424
if (!_passport.type) {
2525
_passport.type = type;

src/service/passport/ldaphelper.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
1-
const axios = require('axios');
1+
const config = require('../../config').getAuthentication();
22
const thirdpartyApiConfig = require('../../config').getAPIs();
3-
const client = axios.create({
4-
responseType: 'json',
5-
headers: {
6-
'content-type': 'application/json',
7-
},
8-
});
3+
const axios = require('axios');
4+
5+
const isUserInAdGroup = (req, profile, ad, domain, name) => {
6+
// determine, via config, if we're using HTTP or AD directly
7+
if (thirdpartyApiConfig?.ls?.userInADGroup) {
8+
return isUserInAdGroupViaHttp(profile.username, domain, name);
9+
} else if (config.adConfig) {
10+
return isUserInAdGroupViaAD(req, profile, ad, domain, name);
11+
} else {
12+
console.error('Unable to check user groups as config is incomplete or unreadable');
13+
}
14+
};
15+
16+
const isUserInAdGroupViaAD = (req, profile, ad, domain, name) => {
17+
// TODO: Added for debugging , needs to be removed
18+
console.log(`checking if id: ${profile.username}, on domain: ${domain}, is a member of group name: ${name}`);
919

10-
const isUserInAdGroup = (id, domain, name) => {
20+
return new Promise((resolve, reject) => {
21+
ad.isUserMemberOf(profile.username, name, function (err, isMember) {
22+
if (err) {
23+
const msg = 'ERROR isUserMemberOf: ' + JSON.stringify(err);
24+
reject(msg);
25+
} else {
26+
console.log(profile.username + ' isMemberOf ' + name + ': ' + isMember);
27+
resolve(isMember);
28+
}
29+
});
30+
});
31+
};
32+
33+
const isUserInAdGroupViaHttp = (id, domain, name) => {
1134
const url = String(thirdpartyApiConfig.ls.userInADGroup)
1235
.replace('<domain>', domain)
1336
.replace('<name>', name)
1437
.replace('<id>', id);
1538

39+
const client = axios.create({
40+
responseType: 'json',
41+
headers: {
42+
'content-type': 'application/json',
43+
},
44+
});
45+
1646
console.log(`checking if user is in group ${url}`);
1747
return client
1848
.get(url)

test/addRepoTest.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('add new repo', async () => {
2828
await db.deleteUser('u1');
2929
await db.deleteUser('u2');
3030
await db.createUser('u1', 'abc', '[email protected]', 'test', true);
31-
await db.createUser('u2', 'abc', 'test@test.com', 'test', true);
31+
await db.createUser('u2', 'abc', 'test2@test.com', 'test', true);
3232
});
3333

3434
it('login', async function () {
@@ -187,5 +187,8 @@ describe('add new repo', async () => {
187187

188188
after(async function () {
189189
await service.httpServer.close();
190+
await db.deleteRepo('test-repo');
191+
await db.deleteUser('u1');
192+
await db.deleteUser('u2');
190193
});
191194
});

test/testUserCreation.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ describe('user creation', async () => {
5555
});
5656

5757
it('login as new user', async function () {
58-
// we don't know the users tempoary password - so force update a
59-
// pasword
58+
// we don't know the users temporary password - so force update a password
6059
const user = await db.findUser('login-test-user');
6160

6261
await bcrypt.hash('test1234', 10, async function (err, hash) {

0 commit comments

Comments
 (0)