@@ -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
4251interface 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 ) ;
0 commit comments