Skip to content

Commit 79ee5e9

Browse files
fix: user name in ad synchronization (#1621)
1 parent 093a366 commit 79ee5e9

File tree

7 files changed

+62
-41
lines changed

7 files changed

+62
-41
lines changed
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
import { faker } from '@faker-js/faker';
22
import { buildTestFactory } from './generic-factory.mock';
3-
import { AzureUserDTO } from 'src/modules/azure/dto/azure-user.dto';
3+
import { AzureUserSyncDTO } from 'src/modules/azure/dto/azure-user.dto';
44

5-
const mockUserData = (): AzureUserDTO => {
5+
const mockUserData = (): AzureUserSyncDTO => {
66
//xGeeks AD style, the '.' is mandatory for some tests
7-
const firstName = faker.person.firstName();
8-
const lastName = faker.person.lastName();
9-
const mail = firstName[0].toLowerCase() + '.' + lastName.toLowerCase() + '@xgeeks.com';
7+
const firstName = `${faker.person.firstName()} ${faker.person.middleName()}`;
8+
const lastName = `${faker.person.middleName()} ${faker.person.lastName()}`;
9+
const mail = `${firstName[0].toLowerCase()}.${lastName.split(' ').at(-1).toLowerCase()}@xgeeks.com`;
1010

1111
return {
1212
id: faker.string.uuid(),
13-
displayName: firstName + ' ' + lastName,
13+
displayName: firstName.split(' ')[0] + ' ' + lastName.split(' ').at(-1),
1414
mail: mail,
1515
userPrincipalName: mail,
1616
createdDateTime: faker.date.past({ years: 5 }),
1717
accountEnabled: faker.datatype.boolean(),
1818
deletedDateTime: faker.datatype.boolean() ? faker.date.recent({ days: 1 }) : null,
19-
employeeLeaveDateTime: faker.datatype.boolean() ? faker.date.recent({ days: 1 }) : null
19+
employeeLeaveDateTime: faker.datatype.boolean() ? faker.date.recent({ days: 1 }) : null,
20+
givenName: firstName,
21+
surName: lastName
2022
};
2123
};
2224

23-
export const AzureUserFactory = buildTestFactory<AzureUserDTO>(() => {
25+
export const AzureUserFactory = buildTestFactory<AzureUserSyncDTO>(() => {
2426
return mockUserData();
2527
});

backend/src/modules/azure/dto/azure-user.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ export type AzureUserDTO = {
88
deletedDateTime: Date | null;
99
employeeLeaveDateTime: Date | null;
1010
};
11+
12+
export type AzureUserSyncDTO = AzureUserDTO & {
13+
givenName: string;
14+
surName: string;
15+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { AzureUserDTO } from '../../dto/azure-user.dto';
1+
import { AzureUserDTO, AzureUserSyncDTO } from '../../dto/azure-user.dto';
22

33
export interface AuthAzureServiceInterface {
44
getUserFromAzure(email: string): Promise<AzureUserDTO | undefined>;
55
fetchUserPhoto(userId: string): Promise<any>;
6-
getADUsers(): Promise<Array<AzureUserDTO>>;
6+
getADUsers(): Promise<Array<AzureUserSyncDTO>>;
77
}

backend/src/modules/azure/schedules/synchronize-ad-users.cron.azure.use-case.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,22 @@ describe('SynchronizeAdUsersCronUseCase', () => {
116116
employeeLeaveDateTime: null,
117117
deletedDateTime: null
118118
});
119+
const userWithInvalidName = AzureUserFactory.create({
120+
employeeLeaveDateTime: null,
121+
deletedDateTime: null
122+
});
123+
124+
//Invalidate last name - represents a user only with first name
125+
userWithInvalidName.surName = '';
126+
userWithInvalidName.displayName = userWithInvalidName.displayName.split(' ')[0];
127+
119128
const azureUserDto: CreateUserAzureDto = {
120129
email: userNotInApp.mail,
121130
firstName: userNotInApp.displayName.split(' ')[0],
122-
lastName: userNotInApp.displayName.split(' ')[-1],
131+
lastName: userNotInApp.displayName.split(' ').at(-1),
123132
providerAccountCreatedAt: userNotInApp.createdDateTime
124133
};
125-
const finalADUsers = [userNotInApp, ...usersAD];
134+
const finalADUsers = [userNotInApp, userWithInvalidName, ...usersAD];
126135
const userNotInAD = UserFactory.create({
127136
isDeleted: false,
128137
@@ -149,7 +158,7 @@ describe('SynchronizeAdUsersCronUseCase', () => {
149158
await synchronizeADUsers.execute();
150159
expect(getTeamByNameUseCase.execute).toHaveBeenCalledWith('xgeeks');
151160
expect(deleteUserMock.execute).toHaveBeenCalledWith(userNotInAD._id);
152-
expect(createUserServiceMock.createMany).toHaveBeenCalledWith([azureUserDto]);
161+
expect(createUserServiceMock.createMany).toHaveBeenCalledWith([azureUserDto]); //userWithInvalidName isn't called here because of the name rules
153162
expect(addAndRemoveTeamUserUseCase.execute).toHaveBeenCalledWith(updateUsersTeam);
154163
});
155164
});

backend/src/modules/azure/schedules/synchronize-ad-users.cron.azure.use-case.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'src/modules/users/constants';
1111
import { UseCase } from 'src/libs/interfaces/use-case.interface';
1212
import User from 'src/modules/users/entities/user.schema';
13-
import { AzureUserDTO } from '../dto/azure-user.dto';
13+
import { AzureUserDTO, AzureUserSyncDTO } from '../dto/azure-user.dto';
1414
import { CreateUserServiceInterface } from 'src/modules/users/interfaces/services/create.user.service.interface';
1515
import { GET_TEAM_BY_NAME_USE_CASE } from 'src/modules/teams/constants';
1616
import Team from 'src/modules/teams/entities/team.schema';
@@ -61,27 +61,26 @@ export class SynchronizeADUsersCronUseCase implements SynchronizeADUsersCronUseC
6161
throw new Error('Azure AD users list is empty.');
6262
}
6363

64-
const usersApp = await this.getAllUsersIncludeDeletedUseCase.execute();
65-
66-
if (!usersApp.length) {
67-
throw new Error('Split app users list is empty.');
68-
}
69-
70-
let usersAppFiltered = usersApp;
64+
let usersApp = await this.getAllUsersIncludeDeletedUseCase.execute();
7165

7266
if (userEmailDomain) {
7367
const emailDomain = '@' + userEmailDomain;
74-
usersAppFiltered = usersAppFiltered.filter(
75-
(user) => user.email && user.email.endsWith(emailDomain)
76-
);
68+
usersApp = usersApp.filter((user) => user.email && user.email.endsWith(emailDomain));
7769
}
7870

7971
const today = new Date();
72+
8073
//Filter out users that don't have a '.' in the beggining of the email
8174
let usersADFiltered = usersADAll.filter((u) =>
8275
/[a-z]+\.[a-zA-Z0-9]+@/.test(u.userPrincipalName)
8376
);
8477

78+
//Filter out users that don't have at least first and last name
79+
usersADFiltered = usersADFiltered.filter(
80+
(u: AzureUserSyncDTO) =>
81+
!(u.displayName.split(' ').length < 2 && (!u.givenName || !u.surName))
82+
);
83+
8584
//Filter out users that have a deletedDateTime bigger than 'today'
8685
usersADFiltered = usersADFiltered.filter((u) =>
8786
'deletedDateTime' in u ? u.deletedDateTime === null || u.deletedDateTime >= today : true
@@ -94,8 +93,8 @@ export class SynchronizeADUsersCronUseCase implements SynchronizeADUsersCronUseC
9493
: true
9594
);
9695

97-
await this.removeUsersFromApp(usersADFiltered, usersAppFiltered);
98-
await this.addUsersToApp(usersADFiltered, usersAppFiltered, team);
96+
await this.removeUsersFromApp(usersADFiltered, usersApp);
97+
await this.addUsersToApp(usersADFiltered, usersApp, team);
9998

10099
this.logger.log('Synchronization of users between App and AD runned successfully.');
101100
} catch (err) {
@@ -125,7 +124,7 @@ export class SynchronizeADUsersCronUseCase implements SynchronizeADUsersCronUseC
125124
}
126125
}
127126
private async addUsersToApp(
128-
usersADFiltered: Array<AzureUserDTO>,
127+
usersADFiltered: Array<AzureUserSyncDTO>,
129128
usersApp: Array<User>,
130129
team: Team
131130
) {
@@ -137,16 +136,20 @@ export class SynchronizeADUsersCronUseCase implements SynchronizeADUsersCronUseC
137136
);
138137

139138
try {
140-
const usersToCreate: Array<CreateUserAzureDto> = notIntersectedUsers.map((user) => {
141-
const splittedName = user.displayName.split(' ');
142-
143-
return {
144-
email: user.mail ?? user.userPrincipalName,
145-
firstName: splittedName[0],
146-
lastName: splittedName[-1],
147-
providerAccountCreatedAt: user.createdDateTime
148-
};
149-
});
139+
const usersToCreate: Array<CreateUserAzureDto> = notIntersectedUsers
140+
.map((user) => {
141+
const splittedName = user.displayName.split(' ');
142+
const firstName = user.givenName?.split(' ')[0] ?? splittedName[0];
143+
const lastName = user.surName?.split(' ').at(-1) ?? splittedName.at(-1);
144+
145+
return {
146+
email: user.mail ?? user.userPrincipalName,
147+
firstName,
148+
lastName,
149+
providerAccountCreatedAt: user.createdDateTime
150+
};
151+
})
152+
.filter((u) => u.firstName && u.lastName);
150153

151154
const createdUsers = await this.createUserService.createMany(usersToCreate);
152155

backend/src/modules/azure/services/auth.azure.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ConfidentialClientApplication } from '@azure/msal-node';
44
import { Client, PageCollection } from '@microsoft/microsoft-graph-client';
55
import { ConfigService } from '@nestjs/config';
66
import { AZURE_AUTHORITY, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET } from 'src/libs/constants/azure';
7-
import { AzureUserDTO } from '../dto/azure-user.dto';
7+
import { AzureUserDTO, AzureUserSyncDTO } from '../dto/azure-user.dto';
88

99
export type AzureDecodedUser = {
1010
unique_name: string;
@@ -71,7 +71,7 @@ export default class AuthAzureService implements AuthAzureServiceInterface {
7171
return this.graphClient.api(`/users/${userId}/photos/240x240/$value`).get();
7272
}
7373

74-
async getADUsers(): Promise<Array<AzureUserDTO>> {
74+
async getADUsers(): Promise<Array<AzureUserSyncDTO>> {
7575
let response: PageCollection = await this.graphClient
7676
.api('/users')
7777
.header('ConsistencyLevel', 'eventual')
@@ -85,7 +85,9 @@ export default class AuthAzureService implements AuthAzureServiceInterface {
8585
'createdDateTime',
8686
'accountEnabled',
8787
'deletedDateTime',
88-
'employeeLeaveDateTime'
88+
'employeeLeaveDateTime',
89+
'givenName',
90+
'surname'
8991
])
9092
.get();
9193

backend/src/modules/users/utils/sortings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import User from '../entities/user.schema';
33

44
export const sortAlphabetically = (a: User, b: User) => {
55
if (a.firstName.toLowerCase() === b.firstName.toLowerCase()) {
6-
return a.lastName.toLowerCase() < b.lastName.toLowerCase() ? -1 : 1;
6+
return a.lastName?.toLowerCase() < b.lastName?.toLowerCase() ? -1 : 1;
77
}
88

99
return a.firstName.toLowerCase() < b.firstName.toLowerCase() ? -1 : 1;

0 commit comments

Comments
 (0)