Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ BUILD_DEPLOY_IMAGE_TAG ?= edge
# UI_IMAGE_REPO and UI_IMAGE_TAG are an easy way to override the UI image used
# only works for installations where INSTALL_STABLE_CORE=false
UI_IMAGE_REPO = uselagoon/ui
UI_IMAGE_TAG = main
UI_IMAGE_TAG = pr-340

# The two variables below are an easy way to override the insights-handler image used in the local stack lagoon-core
# only works for installations where ENABLE_INSIGHTS=true and INSTALL_STABLE_CORE=false
Expand Down
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