[fix] [permissions] Fix role assignment at sign-up (#11045)

In [a previous PR](https://github.com/twentyhq/twenty/pull/11023) I
fixed the issue where users where re-assigned the default role when
signin up using SSO.
The "fix" actually introduced another error, calling
`assignRoleToUserWorkspace` only when a user is signing up to twenty,
forgetting about the case where an existing twenty user signs up to a
different workspace. They should still be assigned the role in that
case.

To fix this and improve clarity, I moved assignRoleToUserWorkspace to
addUserToWorkspace and renamed addUserToWorkspace to
addUserToWorkspaceIfUserNotInWorkspace since this is at each sign in but
does nothing if user is already in the workspace.

I think ideally we should refactor this part to improve readability and
understandability, maybe with different flows for each case: signIn and
signUp, to twenty or to a workspace
This commit is contained in:
Marie
2025-03-20 11:37:26 +01:00
committed by GitHub
parent 4b34aa60b1
commit 8b7188e85b
8 changed files with 99 additions and 137 deletions
@@ -9,8 +9,8 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { ExistingUserOrNewUser } from 'src/engine/core-modules/auth/types/signInUp.type';
@@ -101,7 +101,8 @@ describe('AuthService', () => {
useValue: {
checkUserWorkspaceExists:
userWorkspaceServiceCheckUserWorkspaceExistsMock,
addUserToWorkspace: userWorkspaceAddUserToWorkspaceMock,
addUserToWorkspaceIfUserNotInWorkspace:
userWorkspaceAddUserToWorkspaceMock,
},
},
{
@@ -37,8 +37,8 @@ import {
UserNotExists,
} from 'src/engine/core-modules/auth/dto/user-exists.entity';
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import {
@@ -106,7 +106,10 @@ export class AuthService {
workspacePersonalInviteToken: invitation.value,
email: user.email,
});
await this.userWorkspaceService.addUserToWorkspace(user, workspace);
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
user,
workspace,
);
return;
}
@@ -44,8 +44,6 @@ describe('SignInUpService', () => {
let userWorkspaceService: UserWorkspaceService;
let environmentService: EnvironmentService;
let domainManagerService: DomainManagerService;
let userRoleService: UserRoleService;
let featureFlagService: FeatureFlagService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -83,7 +81,7 @@ describe('SignInUpService', () => {
{
provide: UserWorkspaceService,
useValue: {
addUserToWorkspace: jest.fn(),
addUserToWorkspaceIfUserNotInWorkspace: jest.fn(),
checkUserWorkspaceExists: jest.fn(),
create: jest.fn(),
},
@@ -148,8 +146,6 @@ describe('SignInUpService', () => {
environmentService = module.get<EnvironmentService>(EnvironmentService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
userRoleService = module.get<UserRoleService>(UserRoleService);
featureFlagService = module.get<FeatureFlagService>(FeatureFlagService);
});
it('should handle signInUp with valid personal invitation', async () => {
@@ -179,10 +175,9 @@ describe('SignInUpService', () => {
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
.mockResolvedValue(undefined);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
jest
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
.mockResolvedValue(undefined);
const result = await service.signInUp(params);
@@ -200,6 +195,9 @@ describe('SignInUpService', () => {
(params.workspace as Workspace).id,
'test@example.com',
);
expect(
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
).toHaveBeenCalled();
});
it('should handle signInUp on existing workspace without invitation', async () => {
@@ -217,16 +215,17 @@ describe('SignInUpService', () => {
},
};
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
jest
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
.mockResolvedValue(undefined);
const result = await service.signInUp(params);
expect(result.workspace).toEqual(params.workspace);
expect(result.user).toBeDefined();
expect(userWorkspaceService.addUserToWorkspace).toHaveBeenCalled();
expect(
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
).toHaveBeenCalled();
});
it('should handle signUp on new workspace for a new user', async () => {
@@ -294,10 +293,9 @@ describe('SignInUpService', () => {
};
jest.spyOn(environmentService, 'get').mockReturnValue(false);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
jest
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
.mockResolvedValue(undefined);
jest
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
.mockResolvedValue({} as UserWorkspace);
@@ -306,7 +304,9 @@ describe('SignInUpService', () => {
expect(result.workspace).toEqual(params.workspace);
expect(result.user).toBeDefined();
expect(userWorkspaceService.addUserToWorkspace).toHaveBeenCalled();
expect(
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
).toHaveBeenCalled();
});
it('should throw - handle signUp on workspace in pending state', async () => {
@@ -365,81 +365,4 @@ describe('SignInUpService', () => {
expect(WorkspaceRepository.create).toHaveBeenCalled();
expect(WorkspaceRepository.save).toHaveBeenCalled();
});
it('should not assign default role when permissions are enabled and user exists', async () => {
const params: SignInUpBaseParams &
ExistingUserOrPartialUserWithPicture &
AuthProviderWithPasswordType = {
workspace: {
id: 'workspaceId',
defaultRoleId: 'defaultRoleId',
activationStatus: WorkspaceActivationStatus.ACTIVE,
} as Workspace,
authParams: { provider: 'password', password: 'validPassword' },
userData: {
type: 'existingUser',
existingUser: { email: 'test@example.com' } as User,
},
};
const mockUserWorkspace = { id: 'userWorkspaceId' };
jest.spyOn(featureFlagService, 'isFeatureEnabled').mockResolvedValue(true);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: mockUserWorkspace as UserWorkspace,
});
await service.signInUp(params);
expect(params.workspace).toBeDefined();
expect(userRoleService.assignRoleToUserWorkspace).not.toHaveBeenCalled();
});
it('should assign default role when permissions are enabled and user does not exist', async () => {
const params: SignInUpBaseParams &
ExistingUserOrPartialUserWithPicture &
AuthProviderWithPasswordType = {
workspace: {
id: 'workspaceId',
defaultRoleId: 'defaultRoleId',
activationStatus: WorkspaceActivationStatus.ACTIVE,
} as Workspace,
authParams: { provider: 'password', password: 'validPassword' },
userData: {
type: 'newUserWithPicture',
newUserWithPicture: {
email: 'newuser@example.com',
picture: 'pictureUrl',
},
},
};
const mockUserWorkspace = { id: 'userWorkspaceId' };
jest.spyOn(featureFlagService, 'isFeatureEnabled').mockResolvedValue(true);
jest.spyOn(fileUploadService, 'uploadImage').mockResolvedValue({
id: '',
mimeType: '',
paths: ['path/to/image'],
});
jest.spyOn(UserRepository, 'create').mockReturnValue({} as User);
jest
.spyOn(UserRepository, 'save')
.mockResolvedValue({ id: 'newUserId' } as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: mockUserWorkspace as UserWorkspace,
});
await service.signInUp(params);
expect(params.workspace).toBeDefined();
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
workspaceId: params.workspace!.id,
userWorkspaceId: mockUserWorkspace.id,
roleId: params.workspace!.defaultRoleId,
});
});
});
@@ -226,37 +226,45 @@ export class SignInUpService {
) {
await this.throwIfWorkspaceIsNotReadyForSignInUp(params.workspace, params);
const currentUser =
params.userData.type === 'newUserWithPicture'
? await this.saveNewUser(
params.userData.newUserWithPicture,
params.workspace.id,
{ canAccessFullAdminPanel: false, canImpersonate: false },
)
: params.userData.existingUser;
const isNewUser = params.userData.type === 'newUserWithPicture';
const { user: updatedUser, userWorkspace } =
await this.userWorkspaceService.addUserToWorkspace(
currentUser,
if (isNewUser) {
const userData = params.userData as {
type: 'newUserWithPicture';
newUserWithPicture: PartialUserWithPicture;
};
const user = await this.saveNewUser(
userData.newUserWithPicture,
params.workspace.id,
{
canAccessFullAdminPanel: false,
canImpersonate: false,
},
);
await this.activateOnboardingForUser(user, params.workspace);
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
user,
params.workspace,
);
const user = Object.assign(currentUser, updatedUser);
const isSignUp = params.userData.type === 'newUserWithPicture';
if (isSignUp) {
await this.activateOnboardingForUser(user, params.workspace);
if (params.workspace.defaultRoleId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId: params.workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: params.workspace.defaultRoleId,
});
}
return user;
}
const userData = params.userData as {
type: 'existingUser';
existingUser: User;
};
const user = userData.existingUser;
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
user,
params.workspace,
);
return user;
}