Skip to content

Commit

Permalink
feat: list all root roles in SSO config (#5887)
Browse files Browse the repository at this point in the history
Lists all root roles in SSO config, including custom root roles.


![image](https://github.com/Unleash/unleash/assets/14320932/30114169-4184-4a22-9671-c7041b750d1c)
  • Loading branch information
nunogois committed Jan 15, 2024
1 parent 9d83929 commit 0ba37e8
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 31 deletions.
48 changes: 24 additions & 24 deletions frontend/src/component/admin/auth/AutoCreateForm/AutoCreateForm.tsx
Expand Up @@ -3,40 +3,49 @@ import {
FormControl,
FormControlLabel,
Grid,
InputLabel,
MenuItem,
Select,
Switch,
TextField,
SelectChangeEvent,
} from '@mui/material';
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import { IRole } from 'interfaces/role';

interface IAutoCreateFormProps {
data?: {
enabled: boolean;
autoCreate: boolean;
defaultRootRole?: string;
defaultRootRoleId?: number;
emailDomains?: string;
};
setValue: (name: string, value: string | boolean) => void;
setValue: (
name: string,
value: string | boolean | number | undefined,
) => void;
}

export const AutoCreateForm = ({
data = { enabled: false, autoCreate: false },
setValue,
}: IAutoCreateFormProps) => {
const { roles } = useRoles();

const updateAutoCreate = () => {
setValue('autoCreate', !data.autoCreate);
};

const updateDefaultRootRole = (evt: SelectChangeEvent) => {
setValue('defaultRootRole', evt.target.value);
const updateDefaultRootRoleId = (role: IRole | null) => {
setValue('defaultRootRoleId', role?.id);
};

const updateField = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.name, e.target.value);
};

const roleIdToRole = (rootRoleId: number | undefined): IRole | null => {
return roles.find((role: IRole) => role.id === rootRoleId) || null;
};

return (
<Fragment>
<Grid container spacing={3} mb={2}>
Expand Down Expand Up @@ -69,24 +78,15 @@ export const AutoCreateForm = ({
</p>
</Grid>
<Grid item md={6}>
<FormControl style={{ minWidth: '200px' }}>
<InputLabel id='defaultRootRole-label'>
Default Role
</InputLabel>
<Select
label='Default Role'
labelId='defaultRootRole-label'
id='defaultRootRole'
name='defaultRootRole'
<FormControl style={{ width: '400px' }}>
<RoleSelect
roles={roles}
value={roleIdToRole(data.defaultRootRoleId)}
setValue={updateDefaultRootRoleId}
disabled={!data.autoCreate || !data.enabled}
value={data.defaultRootRole || 'Editor'}
onChange={updateDefaultRootRole}
>
{/*consider these from API or constants. */}
<MenuItem value='Viewer'>Viewer</MenuItem>
<MenuItem value='Editor'>Editor</MenuItem>
<MenuItem value='Admin'>Admin</MenuItem>
</Select>
required
hideDescription
/>
</FormControl>
</Grid>
</Grid>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx
Expand Up @@ -60,7 +60,10 @@ export const OidcAuth = () => {
setData({ ...data, enableSingleSignOut: !data.enableSingleSignOut });
};

const setValue = (name: string, value: string | boolean) => {
const setValue = (
name: string,
value: string | boolean | number | undefined,
) => {
setData({
...data,
[name]: value,
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx
Expand Up @@ -51,7 +51,10 @@ export const SamlAuth = () => {
setData({ ...data, enabled: !data.enabled });
};

const setValue = (name: string, value: string | boolean) => {
const setValue = (
name: string,
value: string | boolean | number | undefined,
) => {
setData({
...data,
[name]: value,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/component/common/RoleSelect/RoleSelect.tsx
Expand Up @@ -23,13 +23,15 @@ interface IRoleSelectProps
value: IRole | null;
setValue: (role: IRole | null) => void;
required?: boolean;
hideDescription?: boolean;
}

export const RoleSelect = ({
roles,
value,
setValue,
required,
hideDescription,
...rest
}: IRoleSelectProps) => {
const renderRoleOption = (
Expand Down Expand Up @@ -60,7 +62,7 @@ export const RoleSelect = ({
{...rest}
/>
<ConditionallyRender
condition={Boolean(value)}
condition={Boolean(value) && !hideDescription}
show={() => (
<RoleDescription sx={{ marginTop: 1 }} roleId={value!.id} />
)}
Expand Down
7 changes: 4 additions & 3 deletions src/lib/db/access-store.ts
Expand Up @@ -12,12 +12,13 @@ import {
IUserRole,
IUserWithProjectRoles,
} from '../types/stores/access-store';
import { IPermission, IUserAccessOverview, RoleType } from '../types/model';
import { IPermission, IUserAccessOverview } from '../types/model';
import NotFoundError from '../error/notfound-error';
import {
ENVIRONMENT_PERMISSION_TYPE,
PROJECT_ROLE_TYPES,
ROOT_PERMISSION_TYPE,
ROOT_ROLE_TYPES,
} from '../util/constants';
import { Db } from './db';
import {
Expand Down Expand Up @@ -407,8 +408,8 @@ export class AccessStore implements IAccessStore {
.select(['id', 'name', 'type', 'description'])
.from<IRole[]>(T.ROLES)
.innerJoin(`${T.ROLE_USER} as ru`, 'ru.role_id', 'id')
.where('ru.user_id', '=', userId)
.andWhere('type', '=', RoleType.ROOT)
.whereIn('type', ROOT_ROLE_TYPES)
.andWhere('ru.user_id', '=', userId)
.first();
}

Expand Down
55 changes: 54 additions & 1 deletion src/test/e2e/services/user-service.e2e.test.ts
Expand Up @@ -18,30 +18,34 @@ import { BadDataError } from '../../../lib/error';
import PasswordMismatch from '../../../lib/error/password-mismatch';
import { EventService } from '../../../lib/services';
import {
CREATE_ADDON,
IUnleashStores,
IUserStore,
USER_CREATED,
USER_DELETED,
USER_UPDATED,
} from '../../../lib/types';
import { CUSTOM_ROOT_ROLE_TYPE } from '../../../lib/util';

let db: ITestDb;
let stores: IUnleashStores;
let userService: UserService;
let userStore: IUserStore;
let adminRole: IRole;
let viewerRole: IRole;
let customRole: IRole;
let sessionService: SessionService;
let settingService: SettingService;
let eventService: EventService;
let accessService: AccessService;

beforeAll(async () => {
db = await dbInit('user_service_serial', getLogger);
stores = db.stores;
const config = createTestConfig();
eventService = new EventService(stores, config);
const groupService = new GroupService(stores, config, eventService);
const accessService = new AccessService(
accessService = new AccessService(
stores,
config,
groupService,
Expand All @@ -64,6 +68,17 @@ beforeAll(async () => {
const rootRoles = await accessService.getRootRoles();
adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN)!;
viewerRole = rootRoles.find((r) => r.name === RoleName.VIEWER)!;
customRole = await accessService.createRole({
name: 'Custom role',
type: CUSTOM_ROOT_ROLE_TYPE,
description: 'A custom role',
permissions: [
{
name: CREATE_ADDON,
},
],
createdByUserId: 1,
});
});

afterAll(async () => {
Expand Down Expand Up @@ -401,3 +416,41 @@ test('should throw if autoCreate is false via SSO', async () => {
}),
).rejects.toThrow(new NotFoundError('No user found'));
});

test('should support a root role id when logging in and creating user via SSO', async () => {
const name = 'root-role-id';
const email = `${name}@test.com`;
const user = await userService.loginUserSSO({
email,
rootRole: viewerRole.id,
name,
autoCreate: true,
});

const userWithRole = await userService.getUser(user.id);
expect(user.email).toBe(email);
expect(user.name).toBe(name);
expect(userWithRole.name).toBe(name);
expect(userWithRole.rootRole).toBe(viewerRole.id);
});

test('should support a custom root role id when logging in and creating user via SSO', async () => {
const name = 'custom-root-role-id';
const email = `${name}@test.com`;
const user = await userService.loginUserSSO({
email,
rootRole: customRole.id,
name,
autoCreate: true,
});

const userWithRole = await userService.getUser(user.id);
expect(user.email).toBe(email);
expect(user.name).toBe(name);
expect(userWithRole.name).toBe(name);
expect(userWithRole.rootRole).toBe(customRole.id);

const permissions = await accessService.getPermissionsForUser(user);
expect(permissions).toHaveLength(1);
expect(permissions[0].permission).toBe(CREATE_ADDON);
});

0 comments on commit 0ba37e8

Please sign in to comment.