Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions services/api/src/models/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,32 @@ export const Group = (clients: {
}
};

// load all keycloak groups using pagination
const loadAllKeycloakGroups = async (): Promise<KeycloakLagoonGroup[]> => {
let groups = [];
let first = 0;
let pageSize = 200;
let fetchedGroups;
while (true) {
fetchedGroups = await keycloakAdminClient.groups.find({
briefRepresentation: false,
first: first,
max: pageSize
});
groups = groups.concat(fetchedGroups)
first += pageSize;
if (fetchedGroups.length < pageSize) {
break;
}
}
return groups;
};

const loadAllGroups = async (): Promise<KeycloakLagoonGroup[]> => {
// briefRepresentation pulls all the group information from keycloak
// including the attributes this means we don't need to iterate over all the
// groups one by one anymore to get the full group information
const keycloakGroups = await keycloakAdminClient.groups.find({
briefRepresentation: false,
});
const keycloakGroups = await loadAllKeycloakGroups();

let fullGroups: KeycloakLagoonGroup[] = [];
for (const group of keycloakGroups) {
Expand Down
89 changes: 75 additions & 14 deletions services/api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export interface User {
organizationRole?: string;
platformRoles?: [string];
emailNotifications?: IEmailNotifications;
totp?: boolean;
has2faEnabled?: boolean;
isFederatedUser?: boolean;
}


interface FederatedIdentity {
hasIdentities?: boolean;
gitlabId?: string;
}

interface UserEdit {
Expand Down Expand Up @@ -76,6 +85,7 @@ export interface UserModel {
userLastAccessed: (userInput: User) => Promise<Boolean>;
transformKeycloakUsers: (keycloakUsers: UserRepresentation[]) => Promise<User[]>;
getFullUserDetails: (userInput: User) => Promise<Object>;
fetchUserTwoFactor: (user: User) => Promise<boolean>;
}

// these match the names of the roles created in keycloak
Expand Down Expand Up @@ -166,18 +176,46 @@ export const User = (clients: {
)(user.attributes)
)(users);

const fetchGitlabId = async (user: User): Promise<string> => {

const fetchUserTwoFactor = async (user: User): Promise<boolean> => {
const credTypes = await keycloakAdminClient.users.getCredentials({
id: user.id
});
let hasTwoFactor = false
for(const cred of credTypes) {
switch (cred.type.replace(/"/g, "")) {
case "webauthn":
case "otp":
hasTwoFactor = true
break;
default:
break;
}
}
return hasTwoFactor;
};


const fetchFederatedIdentities = async (user: User): Promise<FederatedIdentity> => {
let userFed: FederatedIdentity = {
hasIdentities: false,
gitlabId: null,
};
const identities = await keycloakAdminClient.users.listFederatedIdentities({
id: user.id
});

if (identities.length > 0) {
userFed.hasIdentities = true;
}

const gitlabIdentity = R.find(
R.propEq('identityProvider', 'gitlab'),
identities
);

// @ts-ignore
return R.defaultTo(undefined, R.prop('userId', gitlabIdentity));
userFed.gitlabId = R.defaultTo(undefined, R.prop('userId', gitlabIdentity))
return userFed;
};

const transformKeycloakUsers = async (
Expand Down Expand Up @@ -206,9 +244,13 @@ export const User = (clients: {
if (userdate.length) {
user.lastAccessed = userdate[0].lastAccessed
}
let userFed = await fetchFederatedIdentities(user);
if (userFed.hasIdentities) {
user.isFederatedUser = userFed.hasIdentities
}
usersWithGitlabIdFetch.push({
...user,
gitlabId: await fetchGitlabId(user)
gitlabId: userFed.gitlabId
});
}

Expand Down Expand Up @@ -300,6 +342,7 @@ export const User = (clients: {
// from various people with issues with this
email = email.toLocaleLowerCase();
const keycloakUsers = await keycloakAdminClient.users.find({
briefRepresentation: false,
email
});

Expand Down Expand Up @@ -332,6 +375,27 @@ export const User = (clients: {
throw new Error('You must provide a user id or email');
};

// load all keycloak users using pagination
const loadAllKeycloakUsers = async (): Promise<User[]> => {
let users = [];
let first = 0;
let pageSize = 200;
let fetchedUsers;
while (true) {
fetchedUsers = await keycloakAdminClient.users.find({
briefRepresentation: false,
first: first,
max: pageSize
});
users = users.concat(fetchedUsers)
first += pageSize;
if (fetchedUsers.length < pageSize) {
break;
}
}
return users;
};

// used to list onwers of organizations
const loadUsersByOrganizationId = async (organizationId: number): Promise<User[]> => {
const ownerFilter = attribute => {
Expand Down Expand Up @@ -365,7 +429,7 @@ export const User = (clients: {
return false;
};

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

let filteredOwners = filterUsersByAttribute(keycloakUsers, ownerFilter);
let filteredAdmins = filterUsersByAttribute(keycloakUsers, adminFilter);
Expand All @@ -389,13 +453,9 @@ export const User = (clients: {
};

const loadAllUsers = async (): Promise<User[]> => {
const keycloakUsers = await keycloakAdminClient.users.find({
max: -1
});

const users = await transformKeycloakUsers(keycloakUsers);

return users;
let users = await loadAllKeycloakUsers()
const keycloakUsers = await transformKeycloakUsers(users)
return keycloakUsers;
};

const loadAllPlatformUsers = async (): Promise<User[]> => {
Expand Down Expand Up @@ -804,7 +864,7 @@ const getAllProjectsIdsForUser = async (
}
}

// Purge the Group cache for all groups the user belongs to,
// Purge the Group cache for all groups the user belongs to,
// so it does not have out of date information.
const GroupModel = Group(clients);
const userGroups = await getAllGroupsForUser(userInput.id);
Expand Down Expand Up @@ -966,6 +1026,7 @@ const getAllProjectsIdsForUser = async (
deleteUser,
resetUserPassword,
transformKeycloakUsers,
getFullUserDetails
getFullUserDetails,
fetchUserTwoFactor,
};
};
3 changes: 3 additions & 0 deletions services/api/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const {
getAllPlatformUsers,
addPlatformRoleToUser,
removePlatformRoleFromUser,
getUser2fa,
} = require('./resources/user/resolvers');

const {
Expand Down Expand Up @@ -609,11 +610,13 @@ async function getResolvers() {
}
},
User: {
has2faEnabled: getUser2fa,
sshKeys: getUserSshKeys,
groups: getGroupsByUserId,
groupRoles: getGroupRolesByUserId,
},
OrgUser: {
has2faEnabled: getUser2fa,
groupRoles: getGroupRolesByUserIdAndOrganization,
},
Backup: {
Expand Down
18 changes: 16 additions & 2 deletions services/api/src/resources/user/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { Helpers as organizationHelpers } from '../organization/helpers';
import { Sql } from './sql';
import { AuditType } from '@lagoon/commons/dist/types';
import { AuditLog } from '../audit/types';
import { get } from 'http';
import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger';
import { send } from 'process';
import { UserModel, User } from '../../models/user';

export const getMe: ResolverFn = async (_root, args, { models, keycloakGrant: grant }) => {
Expand Down Expand Up @@ -786,3 +784,19 @@ async function getUserOrgEmailDetails(action, models: { UserModel: UserModel; },
return emailDetails;
}

export const getUser2fa: ResolverFn = async (
user,
_,
{ models }
) => {
if (user.isFederatedUser !== undefined && user.isFederatedUser === true ) {
// if a federated user sets up 2fa or passkeys in their account after they have logged in
// it will never be used unless the user unlinks themselves from the identity provider
// if that happens, then the user will have no federated user and their 2fa status will show correctly
// if a user is federated though, then their 2fa status is managed/enforced in the external provider
// and lagoon has no way to verify if 2fa is enabled or enforced in that provider so just set that the user
// is a federated user
return false
}
return models.UserModel.fetchUserTwoFactor(user)
};
4 changes: 4 additions & 0 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ const typeDefs = gql`
created: String
lastAccessed: String
emailNotifications: UserEmailNotification
has2faEnabled: Boolean
isFederatedUser: Boolean
}

enum PlatformRole {
Expand Down Expand Up @@ -1066,6 +1068,8 @@ const typeDefs = gql`
admin: Boolean @deprecated(reason: "use organizationRole")
owner: Boolean @deprecated(reason: "use organizationRole")
comment: String
has2faEnabled: Boolean
isFederatedUser: Boolean
groupRoles: [GroupRoleInterface]
organizationRole: OrganizationRole
}
Expand Down