mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: permission matrix (PoC) (#6223)
## About the changes This is a rough initial version as a PoC for a permission matrix. This is only available after enabling the flag `userAccessUIEnabled` that is set to true by default in local development. The access was added to the users' admin page but could be embedded in different contexts (e.g. when assigning a role to a user): ![image](https://github.com/Unleash/unleash/assets/455064/3f541f46-99bb-409b-a0fe-13f5d3f9572a) This is how the matrix looks like ![screencapture-localhost-3000-admin-users-3-access-2024-02-13-12_15_44](https://github.com/Unleash/unleash/assets/455064/183deeb6-a0dc-470f-924c-f435c6196407) --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
4a81f0932f
commit
7a48fb57a6
122
frontend/src/component/admin/users/AccessMatrix/AccessMatrix.tsx
Normal file
122
frontend/src/component/admin/users/AccessMatrix/AccessMatrix.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
|
||||
import { PermissionsTable } from './PermissionsTable';
|
||||
import { styled, useMediaQuery } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import theme from 'themes/theme';
|
||||
import { StringParam, useQueryParams } from 'use-query-params';
|
||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||
import { AccessMatrixSelect } from './AccessMatrixSelect';
|
||||
import { useUserAccessMatrix } from 'hooks/api/getters/useUserAccessMatrix/useUserAccessMatrix';
|
||||
|
||||
const StyledActionsContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
gap: theme.spacing(1),
|
||||
maxWidth: 600,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTitle = styled('h2')(({ theme }) => ({
|
||||
margin: theme.spacing(2, 0),
|
||||
}));
|
||||
|
||||
export const AccessMatrix = () => {
|
||||
const id = useRequiredPathParam('id');
|
||||
const [query, setQuery] = useQueryParams({
|
||||
project: StringParam,
|
||||
environment: StringParam,
|
||||
});
|
||||
const { projects } = useProjects();
|
||||
const { environments } = useEnvironments();
|
||||
const { user, loading } = useUserInfo(id);
|
||||
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [project, setProject] = useState(query.project ?? '');
|
||||
const [environment, setEnvironment] = useState(
|
||||
query.environment ?? undefined,
|
||||
);
|
||||
|
||||
const { matrix, rootRole, projectRoles } = useUserAccessMatrix(
|
||||
id,
|
||||
project,
|
||||
environment,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(
|
||||
{
|
||||
project: project || undefined,
|
||||
environment,
|
||||
},
|
||||
'replace',
|
||||
);
|
||||
}, [project, environment]);
|
||||
|
||||
const AccessActions = (
|
||||
<StyledActionsContainer>
|
||||
<AccessMatrixSelect
|
||||
label='Project'
|
||||
options={projects}
|
||||
getOptionLabel={(option) => option?.name ?? ''}
|
||||
value={projects.find(({ id }) => id === project)}
|
||||
setValue={(value) => setProject(value?.id ?? '')}
|
||||
/>
|
||||
<AccessMatrixSelect
|
||||
label='Environment'
|
||||
options={environments}
|
||||
getOptionLabel={(option) =>
|
||||
option?.name.concat(
|
||||
!option.enabled ? ' - deprecated' : '',
|
||||
) ?? ''
|
||||
}
|
||||
value={environments.find(({ name }) => name === environment)}
|
||||
setValue={(value) => setEnvironment(value?.name)}
|
||||
/>
|
||||
</StyledActionsContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Access for ${user.name ?? user.username}`}
|
||||
actions={
|
||||
<ConditionallyRender
|
||||
condition={!isSmallScreen}
|
||||
show={AccessActions}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={isSmallScreen}
|
||||
show={AccessActions}
|
||||
/>
|
||||
</PageHeader>
|
||||
}
|
||||
>
|
||||
<StyledTitle>
|
||||
Root permissions for role {rootRole?.name}
|
||||
</StyledTitle>
|
||||
<PermissionsTable permissions={matrix?.root ?? []} />
|
||||
<StyledTitle>
|
||||
Project permissions for project {project} with project roles [
|
||||
{projectRoles?.map((role: any) => role.name).join(', ')}]
|
||||
</StyledTitle>
|
||||
<PermissionsTable permissions={matrix?.project ?? []} />
|
||||
<StyledTitle>
|
||||
Environment permissions for environment {environment}
|
||||
</StyledTitle>
|
||||
<PermissionsTable permissions={matrix?.environment ?? []} />
|
||||
</PageContent>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Autocomplete, AutocompleteProps, TextField } from '@mui/material';
|
||||
|
||||
interface IAccessMatrixSelectProps<T>
|
||||
extends Partial<AutocompleteProps<T, false, false, false>> {
|
||||
label: string;
|
||||
options: T[];
|
||||
value: T;
|
||||
setValue: (role: T | null) => void;
|
||||
}
|
||||
|
||||
export const AccessMatrixSelect = <T,>({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
setValue,
|
||||
...rest
|
||||
}: IAccessMatrixSelectProps<T>) => (
|
||||
<Autocomplete
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(_, value) => setValue(value)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={label} fullWidth />
|
||||
)}
|
||||
size='small'
|
||||
fullWidth
|
||||
{...rest}
|
||||
/>
|
||||
);
|
@ -0,0 +1,80 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useFlexLayout, useSortBy, useTable } from 'react-table';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||
import { Check, Close } from '@mui/icons-material';
|
||||
import { Box } from '@mui/material';
|
||||
import { IMatrixPermission } from 'interfaces/permissions';
|
||||
|
||||
export const PermissionsTable = ({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: IMatrixPermission[];
|
||||
}) => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Permission',
|
||||
accessor: 'name',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'displayName',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
Header: 'Has permission',
|
||||
accessor: 'hasPermission',
|
||||
Cell: ({ value }: { value: boolean }) => (
|
||||
<IconCell
|
||||
icon={
|
||||
value ? (
|
||||
<Check color='success' />
|
||||
) : (
|
||||
<Close color='error' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[permissions],
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
sortBy: [{ id: 'name', desc: true }],
|
||||
};
|
||||
|
||||
const { headerGroups, rows, prepareRow } = useTable(
|
||||
{
|
||||
columns: columns as any,
|
||||
data: permissions ?? [],
|
||||
initialState,
|
||||
sortTypes,
|
||||
},
|
||||
useSortBy,
|
||||
useFlexLayout,
|
||||
);
|
||||
|
||||
const parentRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
return (
|
||||
<Box sx={{ maxHeight: 500, overflow: 'auto' }} ref={parentRef}>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<TablePlaceholder>No permissions found.</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -6,6 +6,7 @@ import { Route, Routes } from 'react-router-dom';
|
||||
import EditUser from './EditUser/EditUser';
|
||||
import NotFound from 'component/common/NotFound/NotFound';
|
||||
import { InactiveUsersList } from './InactiveUsersList/InactiveUsersList';
|
||||
import { AccessMatrix } from './AccessMatrix/AccessMatrix';
|
||||
|
||||
export const UsersAdmin = () => (
|
||||
<div>
|
||||
@ -21,6 +22,7 @@ export const UsersAdmin = () => (
|
||||
}
|
||||
/>
|
||||
<Route path=':id/edit' element={<EditUser />} />
|
||||
<Route path=':id/access' element={<AccessMatrix />} />
|
||||
<Route path='inactive' element={<InactiveUsersList />} />
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Delete, Edit, Lock, LockReset } from '@mui/icons-material';
|
||||
import { Delete, Edit, Key, Lock, LockReset } from '@mui/icons-material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { VFC } from 'react';
|
||||
@ -11,6 +12,7 @@ const StyledBox = styled(Box)(() => ({
|
||||
|
||||
interface IUsersActionsCellProps {
|
||||
onEdit: (event: React.SyntheticEvent) => void;
|
||||
onViewAccess?: (event: React.SyntheticEvent) => void;
|
||||
onChangePassword: (event: React.SyntheticEvent) => void;
|
||||
onResetPassword: (event: React.SyntheticEvent) => void;
|
||||
onDelete: (event: React.SyntheticEvent) => void;
|
||||
@ -18,6 +20,7 @@ interface IUsersActionsCellProps {
|
||||
|
||||
export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
|
||||
onEdit,
|
||||
onViewAccess,
|
||||
onChangePassword,
|
||||
onResetPassword,
|
||||
onDelete,
|
||||
@ -34,6 +37,23 @@ export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(onViewAccess)}
|
||||
show={
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
onClick={onViewAccess!}
|
||||
permission={ADMIN}
|
||||
tooltipProps={{
|
||||
title: 'Access matrix',
|
||||
}}
|
||||
>
|
||||
<Key />
|
||||
</PermissionIconButton>
|
||||
}
|
||||
/>
|
||||
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
onClick={onChangePassword}
|
||||
|
@ -34,8 +34,8 @@ import { UserLimitWarning } from './UserLimitWarning/UserLimitWarning';
|
||||
import { RoleCell } from 'component/common/Table/cells/RoleCell/RoleCell';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { Download } from '@mui/icons-material';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { StyledUsersLinkDiv } from '../Users.styles';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const UsersList = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -52,7 +52,7 @@ const UsersList = () => {
|
||||
}>({
|
||||
open: false,
|
||||
});
|
||||
const { isEnterprise } = useUiConfig();
|
||||
const userAccessUIEnabled = useUiFlag('userAccessUIEnabled');
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
@ -195,12 +195,21 @@ const UsersList = () => {
|
||||
onEdit={() => {
|
||||
navigate(`/admin/users/${user.id}/edit`);
|
||||
}}
|
||||
onViewAccess={
|
||||
userAccessUIEnabled
|
||||
? () => {
|
||||
navigate(
|
||||
`/admin/users/${user.id}/access`,
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onChangePassword={openPwDialog(user)}
|
||||
onResetPassword={openResetPwDialog(user)}
|
||||
onDelete={openDelDialog(user)}
|
||||
/>
|
||||
),
|
||||
width: 200,
|
||||
width: userAccessUIEnabled ? 240 : 200,
|
||||
disableSortBy: true,
|
||||
},
|
||||
// Always hidden -- for search
|
||||
@ -216,7 +225,7 @@ const UsersList = () => {
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
[roles, navigate, isBillingUsers],
|
||||
[roles, navigate, isBillingUsers, userAccessUIEnabled],
|
||||
);
|
||||
|
||||
const initialState = useMemo(() => {
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { useMemo } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import useSWR from 'swr';
|
||||
import { IRole } from 'interfaces/role';
|
||||
import { IUser } from 'interfaces/user';
|
||||
import { IMatrixPermission } from 'interfaces/permissions';
|
||||
|
||||
interface IUserAccessMatrix {
|
||||
root: IMatrixPermission[];
|
||||
project: IMatrixPermission[];
|
||||
environment: IMatrixPermission[];
|
||||
}
|
||||
|
||||
interface IUserAccessMatrixResponse {
|
||||
matrix: IUserAccessMatrix;
|
||||
projectRoles: IRole[];
|
||||
rootRole: IRole;
|
||||
user: IUser;
|
||||
}
|
||||
|
||||
interface IUserAccessMatrixOutput extends Partial<IUserAccessMatrixResponse> {
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const useUserAccessMatrix = (
|
||||
id: string,
|
||||
project?: string,
|
||||
environment?: string,
|
||||
): IUserAccessMatrixOutput => {
|
||||
const queryParams = `${project ? `?project=${project}` : ''}${
|
||||
environment ? `${project ? '&' : '?'}environment=${environment}` : ''
|
||||
}`;
|
||||
const url = `api/admin/user-admin/${id}/permissions${queryParams}`;
|
||||
|
||||
const { data, error, mutate } = useSWR<IUserAccessMatrixResponse>(
|
||||
formatApiPath(url),
|
||||
fetcher,
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
matrix: data?.matrix,
|
||||
projectRoles: data?.projectRoles,
|
||||
rootRole: data?.rootRole,
|
||||
user: data?.user,
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
error,
|
||||
}),
|
||||
[data, error, mutate],
|
||||
);
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('User access matrix'))
|
||||
.then((res) => res.json());
|
||||
};
|
@ -36,3 +36,7 @@ export interface IPermissionCategory {
|
||||
type: PermissionType;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export interface IMatrixPermission extends IPermission {
|
||||
hasPermission: boolean;
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ export type UiFlags = {
|
||||
displayUpgradeEdgeBanner?: boolean;
|
||||
showInactiveUsers?: boolean;
|
||||
featureSearchFeedbackPosting?: boolean;
|
||||
userAccessUIEnabled?: boolean;
|
||||
sdkReporting?: boolean;
|
||||
};
|
||||
|
||||
|
@ -138,6 +138,7 @@ exports[`should create default config 1`] = `
|
||||
"strictSchemaValidation": false,
|
||||
"stripClientHeadersOn304": false,
|
||||
"useMemoizedActiveTokens": false,
|
||||
"userAccessUIEnabled": false,
|
||||
},
|
||||
"externalResolver": {
|
||||
"getVariant": [Function],
|
||||
|
@ -56,6 +56,7 @@ import {
|
||||
createUserResponseSchema,
|
||||
CreateUserResponseSchema,
|
||||
} from '../../openapi/spec/create-user-response-schema';
|
||||
import { IRoleWithPermissions } from '../../types/stores/access-store';
|
||||
|
||||
export default class UserAdminController extends Controller {
|
||||
private flagResolver: IFlagResolver;
|
||||
@ -247,6 +248,44 @@ export default class UserAdminController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:id/permissions',
|
||||
permission: ADMIN,
|
||||
handler: this.getPermissions,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Auth'],
|
||||
operationId: 'getUserPermissions',
|
||||
summary: 'Returns the list of permissions for the user',
|
||||
description:
|
||||
'Gets a list of permissions for a user, additional project and environment can be specified.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'project',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'environment',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: emptyResponse, // TODO define schema
|
||||
...getStandardResponses(401, 403, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/admin-count',
|
||||
@ -636,4 +675,45 @@ export default class UserAdminController extends Controller {
|
||||
adminCount,
|
||||
);
|
||||
}
|
||||
|
||||
async getPermissions(
|
||||
req: IAuthRequest<
|
||||
{ id: number },
|
||||
unknown,
|
||||
unknown,
|
||||
{ project?: string; environment?: string }
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { project, environment } = req.query;
|
||||
const user = await this.userService.getUser(req.params.id);
|
||||
const rootRole = await this.accessService.getRootRoleForUser(user.id);
|
||||
let projectRoles: IRoleWithPermissions[] = [];
|
||||
if (project) {
|
||||
const projectRoleIds =
|
||||
await this.accessService.getProjectRolesForUser(
|
||||
project,
|
||||
user.id,
|
||||
);
|
||||
|
||||
projectRoles = await Promise.all(
|
||||
projectRoleIds.map((roleId) =>
|
||||
this.accessService.getRole(roleId),
|
||||
),
|
||||
);
|
||||
}
|
||||
const matrix = await this.accessService.permissionsMatrixForUser(
|
||||
user,
|
||||
project,
|
||||
environment,
|
||||
);
|
||||
|
||||
// TODO add response validation based on the schema
|
||||
res.status(200).json({
|
||||
matrix,
|
||||
user,
|
||||
rootRole,
|
||||
projectRoles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,14 @@ const PROJECT_ADMIN = [
|
||||
export type IdPermissionRef = Pick<IPermission, 'id' | 'environment'>;
|
||||
export type NamePermissionRef = Pick<IPermission, 'name' | 'environment'>;
|
||||
export type PermissionRef = IdPermissionRef | NamePermissionRef;
|
||||
type MatrixPermission = IPermission & {
|
||||
hasPermission: boolean;
|
||||
};
|
||||
type PermissionMatrix = {
|
||||
root: MatrixPermission[];
|
||||
project: MatrixPermission[];
|
||||
environment: MatrixPermission[];
|
||||
};
|
||||
|
||||
type APIUser = Pick<IUser, 'id' | 'permissions'> & { isAPI: true };
|
||||
type NonAPIUser = Pick<IUser, 'id'> & { isAPI?: false };
|
||||
@ -138,6 +146,32 @@ export class AccessService {
|
||||
this.eventService = eventService;
|
||||
}
|
||||
|
||||
private meetsAllPermissions(
|
||||
userP: IUserPermission[],
|
||||
permissionsArray: string[],
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
) {
|
||||
return userP
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.project ||
|
||||
p.project === projectId ||
|
||||
p.project === ALL_PROJECTS,
|
||||
)
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.environment ||
|
||||
p.environment === environment ||
|
||||
p.environment === ALL_ENVS,
|
||||
)
|
||||
.some(
|
||||
(p) =>
|
||||
permissionsArray.includes(p.permission) ||
|
||||
p.permission === ADMIN,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if a user has access to the requested resource
|
||||
*
|
||||
@ -166,24 +200,12 @@ export class AccessService {
|
||||
|
||||
try {
|
||||
const userP = await this.getPermissionsForUser(user);
|
||||
return userP
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.project ||
|
||||
p.project === projectId ||
|
||||
p.project === ALL_PROJECTS,
|
||||
)
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.environment ||
|
||||
p.environment === environment ||
|
||||
p.environment === ALL_ENVS,
|
||||
)
|
||||
.some(
|
||||
(p) =>
|
||||
permissionsArray.includes(p.permission) ||
|
||||
p.permission === ADMIN,
|
||||
);
|
||||
return this.meetsAllPermissions(
|
||||
userP,
|
||||
permissionsArray,
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Error checking ${permissionLogInfo}, userId=${user.id} projectId=${projectId}`,
|
||||
@ -193,6 +215,48 @@ export class AccessService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a user against all available permissions.
|
||||
* Provided a project, project permissions will be checked against that project.
|
||||
* Provided an environment, environment permissions will be checked against that environment (and project).
|
||||
*/
|
||||
async permissionsMatrixForUser(
|
||||
user: APIUser | NonAPIUser,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<PermissionMatrix> {
|
||||
const permissions = await this.getPermissions();
|
||||
const userP = await this.getPermissionsForUser(user);
|
||||
const matrix: PermissionMatrix = {
|
||||
root: permissions.root.map((p) => ({
|
||||
...p,
|
||||
hasPermission: this.meetsAllPermissions(userP, [p.name]),
|
||||
})),
|
||||
project: permissions.project.map((p) => ({
|
||||
...p,
|
||||
hasPermission: this.meetsAllPermissions(
|
||||
userP,
|
||||
[p.name],
|
||||
projectId,
|
||||
),
|
||||
})),
|
||||
environment:
|
||||
permissions.environments
|
||||
.find((ep) => ep.name === environment)
|
||||
?.permissions.map((p) => ({
|
||||
...p,
|
||||
hasPermission: this.meetsAllPermissions(
|
||||
userP,
|
||||
[p.name],
|
||||
projectId,
|
||||
environment,
|
||||
),
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
async getPermissionsForUser(
|
||||
user: APIUser | NonAPIUser,
|
||||
): Promise<IUserPermission[]> {
|
||||
|
@ -49,6 +49,7 @@ export type IFlagKey =
|
||||
| 'inMemoryScheduledChangeRequests'
|
||||
| 'collectTrafficDataUsage'
|
||||
| 'useMemoizedActiveTokens'
|
||||
| 'userAccessUIEnabled'
|
||||
| 'sdkReporting';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
@ -242,6 +243,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_COLLECT_TRAFFIC_DATA_USAGE,
|
||||
false,
|
||||
),
|
||||
userAccessUIEnabled: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_USER_ACCESS_UI_ENABLED,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -49,6 +49,7 @@ process.nextTick(async () => {
|
||||
featureSearchFeedbackPosting: true,
|
||||
extendedUsageMetricsUI: true,
|
||||
executiveDashboard: true,
|
||||
userAccessUIEnabled: true,
|
||||
sdkReporting: true,
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user