Skip to content

Commit 8cf49c5

Browse files
committed
feat: show is user has 2fa or is federated user in api
1 parent adad5ab commit 8cf49c5

File tree

3 files changed

+106
-16
lines changed

3 files changed

+106
-16
lines changed

services/api/src/models/group.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,32 @@ export const Group = (clients: {
362362
}
363363
};
364364

365+
// load all keycloak groups using pagination
366+
const loadAllKeycloakGroups = async (): Promise<KeycloakLagoonGroup[]> => {
367+
let groups = [];
368+
let first = 0;
369+
let pageSize = 200;
370+
let fetchedGroups;
371+
while (true) {
372+
fetchedGroups = await keycloakAdminClient.groups.find({
373+
briefRepresentation: false,
374+
first: first,
375+
max: pageSize
376+
});
377+
groups = groups.concat(fetchedGroups)
378+
first += pageSize;
379+
if (fetchedGroups.length < pageSize) {
380+
break;
381+
}
382+
}
383+
return groups;
384+
};
385+
365386
const loadAllGroups = async (): Promise<KeycloakLagoonGroup[]> => {
366387
// briefRepresentation pulls all the group information from keycloak
367388
// including the attributes this means we don't need to iterate over all the
368389
// groups one by one anymore to get the full group information
369-
const keycloakGroups = await keycloakAdminClient.groups.find({
370-
briefRepresentation: false,
371-
});
390+
const keycloakGroups = await loadAllKeycloakGroups();
372391

373392
let fullGroups: KeycloakLagoonGroup[] = [];
374393
for (const group of keycloakGroups) {

services/api/src/models/user.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ export interface User {
3737
organizationRole?: string;
3838
platformRoles?: [string];
3939
emailNotifications?: IEmailNotifications;
40+
totp?: boolean;
41+
has2faEnabled?: boolean;
42+
isFederatedUser?: boolean;
43+
}
44+
45+
46+
interface FederatedIdentity {
47+
hasIdentities?: boolean;
48+
gitlabId?: string;
4049
}
4150

4251
interface UserEdit {
@@ -166,18 +175,46 @@ export const User = (clients: {
166175
)(user.attributes)
167176
)(users);
168177

169-
const fetchGitlabId = async (user: User): Promise<string> => {
178+
179+
const fetchUserTwoFactor = async (user: User): Promise<boolean> => {
180+
const credTypes = await keycloakAdminClient.users.getCredentials({
181+
id: user.id
182+
});
183+
let hasTwoFactor = false
184+
for(const cred of credTypes) {
185+
switch (cred.type.replace(/"/g, "")) {
186+
case "webauthn":
187+
case "otp":
188+
hasTwoFactor = true
189+
break;
190+
default:
191+
break;
192+
}
193+
}
194+
return hasTwoFactor;
195+
};
196+
197+
198+
const fetchFederatedIdentities = async (user: User): Promise<FederatedIdentity> => {
199+
let userFed: FederatedIdentity = {
200+
hasIdentities: false,
201+
gitlabId: null,
202+
};
170203
const identities = await keycloakAdminClient.users.listFederatedIdentities({
171204
id: user.id
172205
});
173206

207+
if (identities.length > 0) {
208+
userFed.hasIdentities = true;
209+
}
210+
174211
const gitlabIdentity = R.find(
175212
R.propEq('identityProvider', 'gitlab'),
176213
identities
177214
);
178-
179215
// @ts-ignore
180-
return R.defaultTo(undefined, R.prop('userId', gitlabIdentity));
216+
userFed.gitlabId = R.defaultTo(undefined, R.prop('userId', gitlabIdentity))
217+
return userFed;
181218
};
182219

183220
const transformKeycloakUsers = async (
@@ -206,9 +243,21 @@ export const User = (clients: {
206243
if (userdate.length) {
207244
user.lastAccessed = userdate[0].lastAccessed
208245
}
246+
user.has2faEnabled = await fetchUserTwoFactor(user)
247+
let userFed = await fetchFederatedIdentities(user);
248+
if (userFed.hasIdentities) {
249+
// if a federated user sets up 2fa or passkeys in their account after they have logged in
250+
// it will never be used unless the user unlinks themselves from the identity provider
251+
// if that happens, then the user will have no federated user and their 2fa status will show correctly
252+
// if a user is federated though, then their 2fa status is managed/enforced in the external provider
253+
// and lagoon has no way to verify if 2fa is enabled or enforced in that provider so just set that the user
254+
// is a federated user
255+
user.has2faEnabled = false
256+
user.isFederatedUser = userFed.hasIdentities
257+
}
209258
usersWithGitlabIdFetch.push({
210259
...user,
211-
gitlabId: await fetchGitlabId(user)
260+
gitlabId: userFed.gitlabId
212261
});
213262
}
214263

@@ -300,6 +349,7 @@ export const User = (clients: {
300349
// from various people with issues with this
301350
email = email.toLocaleLowerCase();
302351
const keycloakUsers = await keycloakAdminClient.users.find({
352+
briefRepresentation: false,
303353
email
304354
});
305355

@@ -332,6 +382,27 @@ export const User = (clients: {
332382
throw new Error('You must provide a user id or email');
333383
};
334384

385+
// load all keycloak users using pagination
386+
const loadAllKeycloakUsers = async (): Promise<User[]> => {
387+
let users = [];
388+
let first = 0;
389+
let pageSize = 200;
390+
let fetchedUsers;
391+
while (true) {
392+
fetchedUsers = await keycloakAdminClient.users.find({
393+
briefRepresentation: false,
394+
first: first,
395+
max: pageSize
396+
});
397+
users = users.concat(fetchedUsers)
398+
first += pageSize;
399+
if (fetchedUsers.length < pageSize) {
400+
break;
401+
}
402+
}
403+
return users;
404+
};
405+
335406
// used to list onwers of organizations
336407
const loadUsersByOrganizationId = async (organizationId: number): Promise<User[]> => {
337408
const ownerFilter = attribute => {
@@ -365,7 +436,7 @@ export const User = (clients: {
365436
return false;
366437
};
367438

368-
const keycloakUsers = await keycloakAdminClient.users.find({briefRepresentation: false, max: -1});
439+
let keycloakUsers = await loadAllKeycloakUsers()
369440

370441
let filteredOwners = filterUsersByAttribute(keycloakUsers, ownerFilter);
371442
let filteredAdmins = filterUsersByAttribute(keycloakUsers, adminFilter);
@@ -389,13 +460,9 @@ export const User = (clients: {
389460
};
390461

391462
const loadAllUsers = async (): Promise<User[]> => {
392-
const keycloakUsers = await keycloakAdminClient.users.find({
393-
max: -1
394-
});
395-
396-
const users = await transformKeycloakUsers(keycloakUsers);
397-
398-
return users;
463+
let users = await loadAllKeycloakUsers()
464+
const keycloakUsers = await transformKeycloakUsers(users)
465+
return keycloakUsers;
399466
};
400467

401468
const loadAllPlatformUsers = async (): Promise<User[]> => {
@@ -804,7 +871,7 @@ const getAllProjectsIdsForUser = async (
804871
}
805872
}
806873

807-
// Purge the Group cache for all groups the user belongs to,
874+
// Purge the Group cache for all groups the user belongs to,
808875
// so it does not have out of date information.
809876
const GroupModel = Group(clients);
810877
const userGroups = await getAllGroupsForUser(userInput.id);

services/api/src/typeDefs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,8 @@ const typeDefs = gql`
436436
created: String
437437
lastAccessed: String
438438
emailNotifications: UserEmailNotification
439+
has2faEnabled: Boolean
440+
isFederatedUser: Boolean
439441
}
440442
441443
enum PlatformRole {
@@ -1066,6 +1068,8 @@ const typeDefs = gql`
10661068
admin: Boolean @deprecated(reason: "use organizationRole")
10671069
owner: Boolean @deprecated(reason: "use organizationRole")
10681070
comment: String
1071+
has2faEnabled: Boolean
1072+
isFederatedUser: Boolean
10691073
groupRoles: [GroupRoleInterface]
10701074
organizationRole: OrganizationRole
10711075
}

0 commit comments

Comments
 (0)