mirror of
https://github.com/portainer/portainer.git
synced 2025-08-10 00:05:24 +02:00
refactor(app): create access-control-form react component [EE-2332] (#6346)
* refactor(app): create access-control-form react component [EE-2332] fix [EE-2332] * chore(tests): setup msw for async tests and stories chore(sb): add msw support for storybook * refactor(access-control): move loading into component * fix(app): fix users and teams selector stories * chore(access-control): write test for validation
This commit is contained in:
parent
8dbb802fb1
commit
ecd0eb6170
41 changed files with 1841 additions and 72 deletions
|
@ -21,7 +21,7 @@ export function BoxSelector<T extends number | string>({
|
|||
onChange,
|
||||
}: Props<T>) {
|
||||
return (
|
||||
<div className={clsx('boxselector_wrapper', styles.root)}>
|
||||
<div className={clsx('boxselector_wrapper', styles.root)} role="radiogroup">
|
||||
{options.map((option) => (
|
||||
<BoxSelectorItem
|
||||
key={option.id}
|
||||
|
@ -43,7 +43,7 @@ export function buildOption<T extends number | string>(
|
|||
label: string,
|
||||
description: string,
|
||||
value: T,
|
||||
feature: FeatureId
|
||||
feature?: FeatureId
|
||||
): BoxSelectorOption<T> {
|
||||
return { id, icon, label, description, value, feature };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { TeamViewModel } from '@/portainer/models/team';
|
||||
|
||||
export function createMockTeam(id: number, name: string): TeamViewModel {
|
||||
return {
|
||||
Id: id,
|
||||
Name: name,
|
||||
Checked: false,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { TeamsSelector } from './TeamsSelector';
|
||||
import { createMockTeam } from './TeamsSelector.mocks';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/TeamsSelector',
|
||||
component: TeamsSelector,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export { Example };
|
||||
|
||||
function Example() {
|
||||
const [selectedTeams, setSelectedTeams] = useState([1]);
|
||||
|
||||
const teams = [createMockTeam(1, 'team1'), createMockTeam(2, 'team2')];
|
||||
|
||||
return (
|
||||
<TeamsSelector
|
||||
value={selectedTeams}
|
||||
onChange={setSelectedTeams}
|
||||
teams={teams}
|
||||
placeholder="Select one or more teams"
|
||||
/>
|
||||
);
|
||||
}
|
38
app/portainer/components/TeamsSelector/TeamsSelector.tsx
Normal file
38
app/portainer/components/TeamsSelector/TeamsSelector.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Select from 'react-select';
|
||||
|
||||
import { Team, TeamId } from '@/portainer/teams/types';
|
||||
|
||||
interface Props {
|
||||
value: TeamId[];
|
||||
onChange(value: TeamId[]): void;
|
||||
teams: Team[];
|
||||
dataCy?: string;
|
||||
inputId?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function TeamsSelector({
|
||||
value,
|
||||
onChange,
|
||||
teams,
|
||||
dataCy,
|
||||
inputId,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(team) => team.Name}
|
||||
getOptionValue={(team) => String(team.Id)}
|
||||
options={teams}
|
||||
value={teams.filter((team) => value.includes(team.Id))}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(selectedTeams) =>
|
||||
onChange(selectedTeams.map((team) => team.Id))
|
||||
}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
1
app/portainer/components/TeamsSelector/index.ts
Normal file
1
app/portainer/components/TeamsSelector/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { TeamsSelector } from './TeamsSelector';
|
|
@ -11,8 +11,10 @@ const meta: Meta = {
|
|||
|
||||
export default meta;
|
||||
|
||||
export function Example() {
|
||||
const [selectedUsers, setSelectedUsers] = useState([10]);
|
||||
export { Example };
|
||||
|
||||
function Example() {
|
||||
const [selectedUsers, setSelectedUsers] = useState([1]);
|
||||
|
||||
const users = [createMockUser(1, 'user1'), createMockUser(2, 'user2')];
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Select from 'react-select';
|
||||
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
type UserId = number;
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
interface Props {
|
||||
value: UserId[];
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { AccessControlForm } from './AccessControlForm';
|
||||
import { AccessControlFormData } from './model';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/AccessControlForm',
|
||||
component: AccessControlForm,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
enum Role {
|
||||
Admin = 1,
|
||||
User,
|
||||
}
|
||||
|
||||
const testQueryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
interface Args {
|
||||
userRole: Role;
|
||||
}
|
||||
|
||||
function Template({ userRole }: Args) {
|
||||
const defaults = new AccessControlFormData();
|
||||
defaults.ownership =
|
||||
userRole === Role.Admin
|
||||
? ResourceControlOwnership.ADMINISTRATORS
|
||||
: ResourceControlOwnership.PRIVATE;
|
||||
|
||||
const [value, setValue] = useState(defaults);
|
||||
|
||||
const userProviderState = useMemo(
|
||||
() => ({ user: new UserViewModel({ Role: userRole }) }),
|
||||
[userRole]
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
<UserContext.Provider value={userProviderState}>
|
||||
<AccessControlForm values={value} onChange={setValue} />
|
||||
</UserContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export const AdminAccessControl: Story<Args> = Template.bind({});
|
||||
AdminAccessControl.args = {
|
||||
userRole: Role.Admin,
|
||||
};
|
||||
|
||||
export const NonAdminAccessControl: Story<Args> = Template.bind({});
|
||||
NonAdminAccessControl.args = {
|
||||
userRole: Role.User,
|
||||
};
|
|
@ -0,0 +1,320 @@
|
|||
import { server, rest } from '@/setup-tests/server';
|
||||
import { ResourceControlOwnership as RCO } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import {
|
||||
renderWithQueryClient,
|
||||
within,
|
||||
waitFor,
|
||||
} from '@/react-tools/test-utils';
|
||||
import { Team } from '@/portainer/teams/types';
|
||||
import { ResourceControlViewModel } from '@/portainer/models/resourceControl/resourceControl';
|
||||
import { createMockTeams } from '@/react-tools/test-mocks';
|
||||
|
||||
import { AccessControlForm } from './AccessControlForm';
|
||||
import { AccessControlFormData } from './model';
|
||||
|
||||
test('renders correctly', async () => {
|
||||
const values: AccessControlFormData = new AccessControlFormData();
|
||||
|
||||
const { findByText } = await renderComponent(values);
|
||||
|
||||
expect(await findByText('Access control')).toBeVisible();
|
||||
});
|
||||
|
||||
test('when AccessControlEnabled is true, ownership selector should be visible', async () => {
|
||||
const values = new AccessControlFormData();
|
||||
|
||||
const { queryByRole } = await renderComponent(values);
|
||||
|
||||
expect(queryByRole('radiogroup')).toBeVisible();
|
||||
});
|
||||
|
||||
test('when AccessControlEnabled is false, ownership selector should be hidden', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
accessControlEnabled: false,
|
||||
};
|
||||
|
||||
const { queryByRole } = await renderComponent(values);
|
||||
|
||||
expect(queryByRole('radiogroup')).toBeNull();
|
||||
});
|
||||
|
||||
test('when hideTitle is true, title should be hidden', async () => {
|
||||
const values = new AccessControlFormData();
|
||||
|
||||
const { queryByRole } = await renderComponent(values, jest.fn(), {
|
||||
hideTitle: true,
|
||||
});
|
||||
|
||||
expect(queryByRole('Access control')).toBeNull();
|
||||
});
|
||||
|
||||
test('when isAdmin and AccessControlEnabled, ownership selector should admin and restricted options', async () => {
|
||||
const values = new AccessControlFormData();
|
||||
|
||||
const { findByRole } = await renderComponent(values, jest.fn(), {
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
expect(await selectorQueries.findByLabelText(/Administrator/)).toBeVisible();
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('when isAdmin, AccessControlEnabled and admin ownership is selected, no extra options are visible', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
ownership: RCO.ADMINISTRATORS,
|
||||
};
|
||||
|
||||
const { findByRole, queryByLabelText } = await renderComponent(
|
||||
values,
|
||||
jest.fn(),
|
||||
{
|
||||
isAdmin: true,
|
||||
}
|
||||
);
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(await selectorQueries.findByLabelText(/Administrator/)).toBeChecked();
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).not.toBeChecked();
|
||||
|
||||
expect(queryByLabelText('extra-options')).toBeNull();
|
||||
});
|
||||
|
||||
test('when isAdmin, AccessControlEnabled and restricted ownership is selected, show team and users selectors', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
ownership: RCO.RESTRICTED,
|
||||
};
|
||||
|
||||
const { findByRole, findByLabelText } = await renderComponent(
|
||||
values,
|
||||
jest.fn(),
|
||||
{
|
||||
isAdmin: true,
|
||||
}
|
||||
);
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(
|
||||
await selectorQueries.findByLabelText(/Administrator/)
|
||||
).not.toBeChecked();
|
||||
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).toBeChecked();
|
||||
|
||||
const extraOptions = await findByLabelText('extra-options');
|
||||
expect(extraOptions).toBeVisible();
|
||||
|
||||
if (!extraOptions) {
|
||||
throw new Error('extra options section is missing');
|
||||
}
|
||||
|
||||
const extraQueries = within(extraOptions);
|
||||
expect(await extraQueries.findByText(/Authorized users/)).toBeVisible();
|
||||
expect(await extraQueries.findByText(/Authorized teams/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('when user is not an admin and access control is enabled and no teams, should have only private option', async () => {
|
||||
const values = new AccessControlFormData();
|
||||
|
||||
const { findByRole } = await renderComponent(values, jest.fn(), {
|
||||
teams: [],
|
||||
isAdmin: false,
|
||||
});
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(selectorQueries.queryByLabelText(/Private/)).toBeVisible();
|
||||
expect(selectorQueries.queryByLabelText(/Restricted/)).toBeNull();
|
||||
});
|
||||
|
||||
test('when user is not an admin and access control is enabled and there is 1 team, should have private and restricted options', async () => {
|
||||
const values = new AccessControlFormData();
|
||||
|
||||
const { findByRole } = await renderComponent(values, jest.fn(), {
|
||||
teams: createMockTeams(1),
|
||||
isAdmin: false,
|
||||
});
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(await selectorQueries.findByLabelText(/Private/)).toBeVisible();
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('when user is not an admin, access control is enabled, there are more then 1 team and ownership is restricted, team selector should be visible', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
ownership: RCO.RESTRICTED,
|
||||
};
|
||||
|
||||
const { findByRole, findByLabelText } = await renderComponent(
|
||||
values,
|
||||
jest.fn()
|
||||
);
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(await selectorQueries.findByLabelText(/Private/)).toBeVisible();
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).toBeVisible();
|
||||
|
||||
const extraOptions = await findByLabelText('extra-options');
|
||||
expect(extraOptions).toBeVisible();
|
||||
|
||||
if (!extraOptions) {
|
||||
throw new Error('extra options section is missing');
|
||||
}
|
||||
|
||||
const extraQueries = within(extraOptions);
|
||||
expect(extraQueries.queryByLabelText(/Authorized teams/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('when user is not an admin, access control is enabled, there is 1 team and ownership is restricted, team selector not should be visible', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
ownership: RCO.RESTRICTED,
|
||||
};
|
||||
|
||||
const { findByRole, findByLabelText } = await renderComponent(
|
||||
values,
|
||||
jest.fn(),
|
||||
{
|
||||
teams: createMockTeams(1),
|
||||
isAdmin: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const selectorQueries = within(ownershipSelector);
|
||||
|
||||
expect(await selectorQueries.findByLabelText(/Private/)).toBeVisible();
|
||||
expect(await selectorQueries.findByLabelText(/Restricted/)).toBeVisible();
|
||||
|
||||
const extraOptions = await findByLabelText('extra-options');
|
||||
expect(extraOptions).toBeVisible();
|
||||
|
||||
if (!extraOptions) {
|
||||
throw new Error('extra options section is missing');
|
||||
}
|
||||
|
||||
const extraQueries = within(extraOptions);
|
||||
expect(extraQueries.queryByText(/Authorized teams/)).toBeNull();
|
||||
});
|
||||
|
||||
test('when user is not an admin, access control is enabled, and ownership is restricted, user selector not should be visible', async () => {
|
||||
const values: AccessControlFormData = {
|
||||
...new AccessControlFormData(),
|
||||
ownership: RCO.RESTRICTED,
|
||||
};
|
||||
|
||||
const { findByRole, findByLabelText } = await renderComponent(
|
||||
values,
|
||||
jest.fn(),
|
||||
{
|
||||
isAdmin: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ownershipSelector = await findByRole('radiogroup');
|
||||
|
||||
expect(ownershipSelector).toBeVisible();
|
||||
if (!ownershipSelector) {
|
||||
throw new Error('selector is missing');
|
||||
}
|
||||
|
||||
const extraOptions = await findByLabelText('extra-options');
|
||||
expect(extraOptions).toBeVisible();
|
||||
|
||||
if (!extraOptions) {
|
||||
throw new Error('extra options section is missing');
|
||||
}
|
||||
const extraQueries = within(extraOptions);
|
||||
|
||||
expect(extraQueries.queryByText(/Authorized users/)).toBeNull();
|
||||
});
|
||||
|
||||
interface AdditionalProps {
|
||||
teams?: Team[];
|
||||
users?: UserViewModel[];
|
||||
isAdmin?: boolean;
|
||||
hideTitle?: boolean;
|
||||
resourceControl?: ResourceControlViewModel;
|
||||
}
|
||||
|
||||
async function renderComponent(
|
||||
values: AccessControlFormData,
|
||||
onChange = jest.fn(),
|
||||
{ isAdmin = false, hideTitle = false, teams, users }: AdditionalProps = {}
|
||||
) {
|
||||
const user = new UserViewModel({ Username: 'user', Role: isAdmin ? 1 : 2 });
|
||||
const state = { user };
|
||||
|
||||
if (teams) {
|
||||
server.use(rest.get('/api/teams', (req, res, ctx) => res(ctx.json(teams))));
|
||||
}
|
||||
|
||||
if (users) {
|
||||
server.use(rest.get('/api/users', (req, res, ctx) => res(ctx.json(users))));
|
||||
}
|
||||
|
||||
const renderResult = renderWithQueryClient(
|
||||
<UserContext.Provider value={state}>
|
||||
<AccessControlForm
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
hideTitle={hideTitle}
|
||||
/>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await renderResult.findByLabelText(/Enable access control/)
|
||||
).toBeVisible()
|
||||
);
|
||||
|
||||
return renderResult;
|
||||
}
|
155
app/portainer/components/accessControlForm/AccessControlForm.tsx
Normal file
155
app/portainer/components/accessControlForm/AccessControlForm.tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
import _ from 'lodash';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { ResourceControlOwnership as RCO } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
import { BoxSelector, buildOption } from '@/portainer/components/BoxSelector';
|
||||
import { ownershipIcon } from '@/portainer/filters/filters';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { Team } from '@/portainer/teams/types';
|
||||
import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
|
||||
|
||||
import { AccessControlFormData } from './model';
|
||||
import { UsersField } from './UsersField';
|
||||
import { TeamsField } from './TeamsField';
|
||||
import { useLoadState } from './useLoadState';
|
||||
|
||||
export interface Props {
|
||||
values: AccessControlFormData;
|
||||
onChange(values: AccessControlFormData): void;
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
export function AccessControlForm({ values, onChange, hideTitle }: Props) {
|
||||
const { users, teams, isLoading } = useLoadState();
|
||||
|
||||
const { user } = useUser();
|
||||
const isAdmin = user?.Role === 1;
|
||||
|
||||
const options = useOptions(isAdmin, teams);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(partialValues: Partial<typeof values>) => {
|
||||
onChange({ ...values, ...partialValues });
|
||||
},
|
||||
|
||||
[values, onChange]
|
||||
);
|
||||
|
||||
if (isLoading || !teams || !users) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideTitle && <FormSectionTitle>Access control</FormSectionTitle>}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
checked={values.accessControlEnabled}
|
||||
name="ownership"
|
||||
label="Enable access control"
|
||||
tooltip="When enabled, you can restrict the access and management of this resource."
|
||||
onChange={(accessControlEnabled) =>
|
||||
handleChange({ accessControlEnabled })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{values.accessControlEnabled && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<BoxSelector
|
||||
radioName="access-control"
|
||||
value={values.ownership}
|
||||
options={options}
|
||||
onChange={(ownership) => handleChange({ ownership })}
|
||||
/>
|
||||
</div>
|
||||
{values.ownership === RCO.RESTRICTED && (
|
||||
<div aria-label="extra-options">
|
||||
{isAdmin && (
|
||||
<UsersField
|
||||
users={users}
|
||||
onChange={(authorizedUsers) =>
|
||||
handleChange({ authorizedUsers })
|
||||
}
|
||||
value={values.authorizedUsers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isAdmin || teams.length > 1) && (
|
||||
<TeamsField
|
||||
teams={teams}
|
||||
overrideTooltip={
|
||||
!isAdmin && teams.length > 1
|
||||
? 'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.'
|
||||
: undefined
|
||||
}
|
||||
onChange={(authorizedTeams) =>
|
||||
handleChange({ authorizedTeams })
|
||||
}
|
||||
value={values.authorizedTeams}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useOptions(isAdmin: boolean, teams?: Team[]) {
|
||||
const [options, setOptions] = useState<Array<BoxSelectorOption<RCO>>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(isAdmin ? adminOptions() : nonAdminOptions(teams));
|
||||
}, [isAdmin, teams]);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function adminOptions() {
|
||||
return [
|
||||
buildOption(
|
||||
'access_administrators',
|
||||
ownershipIcon('administrators'),
|
||||
'Administrators',
|
||||
'I want to restrict the management of this resource to administrators only',
|
||||
RCO.ADMINISTRATORS
|
||||
),
|
||||
buildOption(
|
||||
'access_restricted',
|
||||
ownershipIcon('restricted'),
|
||||
'Restricted',
|
||||
'I want to restrict the management of this resource to a set of users and/or teams',
|
||||
RCO.RESTRICTED
|
||||
),
|
||||
];
|
||||
}
|
||||
function nonAdminOptions(teams?: Team[]) {
|
||||
return _.compact([
|
||||
buildOption(
|
||||
'access_private',
|
||||
ownershipIcon('private'),
|
||||
'Private',
|
||||
'I want to this resource to be manageable by myself only',
|
||||
RCO.PRIVATE
|
||||
),
|
||||
teams &&
|
||||
teams.length > 0 &&
|
||||
buildOption(
|
||||
'access_restricted',
|
||||
ownershipIcon('restricted'),
|
||||
'Restricted',
|
||||
teams.length === 1
|
||||
? `I want any member of my team (${teams[0].Name}) to be able to manage this resource`
|
||||
: 'I want to restrict the management of this resource to one or more of my teams',
|
||||
RCO.RESTRICTED
|
||||
),
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
|
||||
import { validationSchema } from './AccessControlForm.validation';
|
||||
|
||||
test('when access control is disabled, should be valid', async () => {
|
||||
const schema = validationSchema(true);
|
||||
const object = { accessControlEnabled: false };
|
||||
|
||||
await expect(
|
||||
schema.validate(object, { strict: true })
|
||||
).resolves.toStrictEqual(object);
|
||||
});
|
||||
|
||||
test('when only access control is enabled, should be invalid', async () => {
|
||||
const schema = validationSchema(true);
|
||||
|
||||
await expect(
|
||||
schema.validate({ accessControlEnabled: true }, { strict: true })
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('when access control is enabled and ownership not restricted, should be valid', async () => {
|
||||
const schema = validationSchema(true);
|
||||
[
|
||||
ResourceControlOwnership.ADMINISTRATORS,
|
||||
ResourceControlOwnership.PRIVATE,
|
||||
ResourceControlOwnership.PUBLIC,
|
||||
].forEach(async (ownership) => {
|
||||
const object = { accessControlEnabled: false, ownership };
|
||||
|
||||
await expect(
|
||||
schema.validate(object, { strict: true })
|
||||
).resolves.toStrictEqual(object);
|
||||
});
|
||||
});
|
||||
|
||||
test('when access control is enabled, ownership is restricted and no teams or users, should be invalid', async () => {
|
||||
[true, false].forEach(async (isAdmin) => {
|
||||
const schema = validationSchema(isAdmin);
|
||||
|
||||
await expect(
|
||||
schema.validate(
|
||||
{
|
||||
accessControlEnabled: true,
|
||||
ownership: ResourceControlOwnership.RESTRICTED,
|
||||
},
|
||||
{ strict: true }
|
||||
)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('when access control is enabled, ownership is restricted, user is admin but no users, should be valid', async () => {
|
||||
const schema = validationSchema(true);
|
||||
|
||||
await expect(
|
||||
schema.validate(
|
||||
{
|
||||
accessControlEnabled: true,
|
||||
ownership: ResourceControlOwnership.RESTRICTED,
|
||||
authorizedTeams: [1],
|
||||
},
|
||||
{ strict: true }
|
||||
)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('when access control is enabled, ownership is restricted, user is admin with teams and users, should be valid', async () => {
|
||||
const schema = validationSchema(false);
|
||||
|
||||
const object = {
|
||||
accessControlEnabled: true,
|
||||
ownership: ResourceControlOwnership.RESTRICTED,
|
||||
authorizedTeams: [1],
|
||||
authorizedUsers: [1],
|
||||
};
|
||||
|
||||
await expect(
|
||||
schema.validate(object, { strict: true })
|
||||
).resolves.toStrictEqual(object);
|
||||
});
|
||||
|
||||
test('when access control is enabled, ownership is restricted, user is not admin with teams, should be valid', async () => {
|
||||
const schema = validationSchema(false);
|
||||
|
||||
const object = {
|
||||
accessControlEnabled: true,
|
||||
ownership: ResourceControlOwnership.RESTRICTED,
|
||||
authorizedTeams: [1],
|
||||
};
|
||||
|
||||
await expect(
|
||||
schema.validate(object, { strict: true })
|
||||
).resolves.toStrictEqual(object);
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import { object, string, array, number, bool } from 'yup';
|
||||
|
||||
import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
|
||||
export function validationSchema(isAdmin: boolean) {
|
||||
return object().shape({
|
||||
accessControlEnabled: bool(),
|
||||
ownership: string()
|
||||
.oneOf(Object.values(ResourceControlOwnership))
|
||||
.when('accessControlEnabled', {
|
||||
is: true,
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
authorizedUsers: array(number()).when(
|
||||
['accessControlEnabled', 'ownership'],
|
||||
{
|
||||
is: (
|
||||
accessControlEnabled: boolean,
|
||||
ownership: ResourceControlOwnership
|
||||
) =>
|
||||
isAdmin &&
|
||||
accessControlEnabled &&
|
||||
ownership === ResourceControlOwnership.RESTRICTED,
|
||||
then: (schema) =>
|
||||
schema.required('You must specify at least one user.'),
|
||||
}
|
||||
),
|
||||
authorizedTeams: array(number()).when(
|
||||
['accessControlEnabled', 'ownership'],
|
||||
{
|
||||
is: (
|
||||
accessControlEnabled: boolean,
|
||||
ownership: ResourceControlOwnership
|
||||
) =>
|
||||
accessControlEnabled &&
|
||||
ownership === ResourceControlOwnership.RESTRICTED,
|
||||
then: (schema) => schema.required('You must specify at least one team'),
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
40
app/portainer/components/accessControlForm/TeamsField.tsx
Normal file
40
app/portainer/components/accessControlForm/TeamsField.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { TeamsSelector } from '@/portainer/components/TeamsSelector';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
import { Team } from '@/portainer/teams/types';
|
||||
|
||||
interface Props {
|
||||
teams: Team[];
|
||||
value: number[];
|
||||
overrideTooltip?: string;
|
||||
onChange(value: number[]): void;
|
||||
}
|
||||
|
||||
export function TeamsField({ teams, value, overrideTooltip, onChange }: Props) {
|
||||
return (
|
||||
<FormControl
|
||||
label="Authorized teams"
|
||||
tooltip={
|
||||
teams.length > 0
|
||||
? overrideTooltip ||
|
||||
'You can select which team(s) will be able to manage this resource.'
|
||||
: undefined
|
||||
}
|
||||
inputId="teams-selector"
|
||||
>
|
||||
{teams.length > 0 ? (
|
||||
<TeamsSelector
|
||||
teams={teams}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
inputId="teams-selector"
|
||||
/>
|
||||
) : (
|
||||
<span className="small text-muted">
|
||||
You have not yet created any teams. Head over to the
|
||||
<Link to="portainer.teams">Teams view</Link> to manage teams.
|
||||
</span>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
38
app/portainer/components/accessControlForm/UsersField.tsx
Normal file
38
app/portainer/components/accessControlForm/UsersField.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { UsersSelector } from '@/portainer/components/UsersSelector';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
|
||||
interface Props {
|
||||
users: UserViewModel[];
|
||||
value: number[];
|
||||
onChange(value: number[]): void;
|
||||
}
|
||||
|
||||
export function UsersField({ users, value, onChange }: Props) {
|
||||
return (
|
||||
<FormControl
|
||||
label="Authorized users"
|
||||
tooltip={
|
||||
users.length > 0
|
||||
? 'You can select which user(s) will be able to manage this resource.'
|
||||
: undefined
|
||||
}
|
||||
inputId="users-selector"
|
||||
>
|
||||
{users.length > 0 ? (
|
||||
<UsersSelector
|
||||
users={users}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
inputId="users-selector"
|
||||
/>
|
||||
) : (
|
||||
<span className="small text-muted">
|
||||
You have not yet created any users. Head over to the
|
||||
<Link to="portainer.users">Users view</Link> to manage users.
|
||||
</span>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`when access control is enabled, ownership is restricted and no teams or users, should be invalid 1`] = `"You must specify at least one team"`;
|
||||
|
||||
exports[`when access control is enabled, ownership is restricted and no teams or users, should be invalid 2`] = `"You must specify at least one team"`;
|
||||
|
||||
exports[`when access control is enabled, ownership is restricted and no teams or users, should be valid 1`] = `"You must specify at least one team"`;
|
||||
|
||||
exports[`when access control is enabled, ownership is restricted and no teams or users, should be valid 2`] = `"You must specify at least one team"`;
|
||||
|
||||
exports[`when access control is enabled, ownership is restricted, user is admin but no users, should be valid 1`] = `"You must specify at least one user."`;
|
||||
|
||||
exports[`when only access control is enabled, should be invalid 1`] = `"ownership is a required field"`;
|
1
app/portainer/components/accessControlForm/index.ts
Normal file
1
app/portainer/components/accessControlForm/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AccessControlForm } from './AccessControlForm';
|
61
app/portainer/components/accessControlForm/model.test.ts
Normal file
61
app/portainer/components/accessControlForm/model.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { ResourceControlOwnership as RCO } from '@/portainer/models/resourceControl/resourceControlOwnership';
|
||||
import {
|
||||
ResourceControlType,
|
||||
ResourceControlViewModel,
|
||||
} from '@/portainer/models/resourceControl/resourceControl';
|
||||
|
||||
import { parseFromResourceControl } from './model';
|
||||
|
||||
test('when resource control supplied, if user is not admin, will change ownership to rc ownership', () => {
|
||||
[RCO.ADMINISTRATORS, RCO.RESTRICTED, RCO.PUBLIC, RCO.PRIVATE].forEach(
|
||||
(ownership) => {
|
||||
const resourceControl = buildResourceControl(ownership);
|
||||
|
||||
const actual = parseFromResourceControl(false, resourceControl.Ownership);
|
||||
expect(actual.ownership).toBe(resourceControl.Ownership);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('when resource control supplied and user is admin, if resource ownership is not private , will change ownership to rc ownership', () => {
|
||||
[RCO.ADMINISTRATORS, RCO.RESTRICTED, RCO.PUBLIC].forEach((ownership) => {
|
||||
const resourceControl = buildResourceControl(ownership);
|
||||
|
||||
const actual = parseFromResourceControl(true, resourceControl.Ownership);
|
||||
expect(actual.ownership).toBe(resourceControl.Ownership);
|
||||
});
|
||||
});
|
||||
|
||||
test('when resource control supplied, if ownership is public, will disabled access control', () => {
|
||||
const resourceControl = buildResourceControl(RCO.PUBLIC);
|
||||
|
||||
const actual = parseFromResourceControl(false, resourceControl.Ownership);
|
||||
|
||||
expect(actual.accessControlEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test('when isAdmin and resource control not supplied, ownership should be set to Administrator', () => {
|
||||
const actual = parseFromResourceControl(true);
|
||||
|
||||
expect(actual.ownership).toBe(RCO.ADMINISTRATORS);
|
||||
});
|
||||
|
||||
test('when resource control supplied, if user is admin and resource ownership is private, will change ownership to restricted', () => {
|
||||
const resourceControl = buildResourceControl(RCO.PRIVATE);
|
||||
|
||||
const actual = parseFromResourceControl(true, resourceControl.Ownership);
|
||||
expect(actual.ownership).toBe(RCO.RESTRICTED);
|
||||
});
|
||||
|
||||
function buildResourceControl(ownership: RCO): ResourceControlViewModel {
|
||||
return {
|
||||
UserAccesses: [],
|
||||
TeamAccesses: [],
|
||||
Ownership: ownership,
|
||||
Id: 1,
|
||||
Public: false,
|
||||
ResourceId: 1,
|
||||
System: false,
|
||||
Type: ResourceControlType.Config,
|
||||
};
|
||||
}
|
40
app/portainer/components/accessControlForm/model.ts
Normal file
40
app/portainer/components/accessControlForm/model.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ResourceControlOwnership } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||
|
||||
import { TeamId } from '@/portainer/teams/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
export class AccessControlFormData {
|
||||
accessControlEnabled = true;
|
||||
|
||||
ownership = ResourceControlOwnership.PRIVATE;
|
||||
|
||||
authorizedUsers: UserId[] = [];
|
||||
|
||||
authorizedTeams: TeamId[] = [];
|
||||
}
|
||||
|
||||
export function parseFromResourceControl(
|
||||
isAdmin: boolean,
|
||||
resourceControlOwnership?: ResourceControlOwnership
|
||||
): AccessControlFormData {
|
||||
const formData = new AccessControlFormData();
|
||||
|
||||
if (resourceControlOwnership) {
|
||||
let ownership = resourceControlOwnership;
|
||||
if (isAdmin && ownership === ResourceControlOwnership.PRIVATE) {
|
||||
ownership = ResourceControlOwnership.RESTRICTED;
|
||||
}
|
||||
|
||||
let accessControl = formData.accessControlEnabled;
|
||||
if (ownership === ResourceControlOwnership.PUBLIC) {
|
||||
accessControl = false;
|
||||
}
|
||||
|
||||
formData.ownership = ownership;
|
||||
formData.accessControlEnabled = accessControl;
|
||||
} else if (isAdmin) {
|
||||
formData.ownership = ResourceControlOwnership.ADMINISTRATORS;
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
<div>
|
||||
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title">
|
||||
Access control
|
||||
</div>
|
||||
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title"> Access control </div>
|
||||
<!-- access-control-switch -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
@ -9,18 +7,18 @@
|
|||
Enable access control
|
||||
<portainer-tooltip position="bottom" message="When enabled, you can restrict the access and management of this resource."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input name="ownership" type="checkbox" ng-model="$ctrl.formData.AccessControlEnabled" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px"> <input name="ownership" type="checkbox" ng-model="$ctrl.formData.AccessControlEnabled" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !access-control-switch -->
|
||||
<!-- restricted-access -->
|
||||
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled" style="margin-bottom: 0;">
|
||||
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div ng-if="$ctrl.isAdmin">
|
||||
<input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators" />
|
||||
<label for="access_administrators">
|
||||
<div class="boxselector_header">
|
||||
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Administrators
|
||||
</div>
|
||||
<p>I want to restrict the management of this resource to administrators only</p>
|
||||
|
@ -30,40 +28,34 @@
|
|||
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
|
||||
<label for="access_restricted">
|
||||
<div class="boxselector_header">
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Restricted
|
||||
</div>
|
||||
<p>
|
||||
I want to restrict the management of this resource to a set of users and/or teams
|
||||
</p>
|
||||
<p> I want to restrict the management of this resource to a set of users and/or teams </p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAdmin">
|
||||
<input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private" />
|
||||
<label for="access_private">
|
||||
<div class="boxselector_header">
|
||||
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Private
|
||||
</div>
|
||||
<p>
|
||||
I want to this resource to be manageable by myself only
|
||||
</p>
|
||||
<p> I want to this resource to be manageable by myself only </p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0">
|
||||
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
|
||||
<label for="access_restricted">
|
||||
<div class="boxselector_header">
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Restricted
|
||||
</div>
|
||||
<p ng-if="$ctrl.availableTeams.length === 1">
|
||||
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b
|
||||
>) to be able to manage this resource
|
||||
</p>
|
||||
<p ng-if="$ctrl.availableTeams.length > 1">
|
||||
I want to restrict the management of this resource to one or more of my teams
|
||||
</p>
|
||||
<p ng-if="$ctrl.availableTeams.length > 1"> I want to restrict the management of this resource to one or more of my teams </p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -88,7 +80,7 @@
|
|||
message="As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 20px;">
|
||||
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 20px">
|
||||
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
|
||||
</span>
|
||||
<span
|
||||
|
@ -102,7 +94,7 @@
|
|||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
||||
style="margin-left: 20px;"
|
||||
style="margin-left: 20px"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -119,7 +111,7 @@
|
|||
message="You can select which user(s) will be able to manage this resource."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 20px;">
|
||||
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 20px">
|
||||
You have not yet created any users. Head over to the <a ui-sref="portainer.users">Users view</a> to manage users.
|
||||
</span>
|
||||
<span
|
||||
|
@ -133,7 +125,7 @@
|
|||
helper-elements="filter"
|
||||
search-property="Username"
|
||||
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
|
||||
style="margin-left: 20px;"
|
||||
style="margin-left: 20px"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||
|
||||
/**
|
||||
* @deprecated use only for angularjs components. For react components use ./model.ts
|
||||
*/
|
||||
export function AccessControlFormData() {
|
||||
this.AccessControlEnabled = true;
|
||||
this.Ownership = RCO.PRIVATE;
|
||||
|
|
45
app/portainer/components/accessControlForm/useLoadState.ts
Normal file
45
app/portainer/components/accessControlForm/useLoadState.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { getTeams } from '@/portainer/teams/teams.service';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { getUsers } from '@/portainer/services/api/userService';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
export function useLoadState() {
|
||||
const { teams, isLoading: isLoadingTeams } = useTeams();
|
||||
|
||||
const { users, isLoading: isLoadingUsers } = useUsers();
|
||||
|
||||
return { teams, users, isLoading: isLoadingTeams || isLoadingUsers };
|
||||
}
|
||||
|
||||
function useTeams() {
|
||||
const { isError, error, isLoading, data } = useQuery('teams', () =>
|
||||
getTeams()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error('Failure', error as Error, 'Failed retrieving teams');
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return { isLoading, teams: data };
|
||||
}
|
||||
|
||||
function useUsers() {
|
||||
const { isError, error, isLoading, data } = useQuery<
|
||||
unknown,
|
||||
unknown,
|
||||
UserViewModel[]
|
||||
>('users', () => getUsers());
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error('Failure', error as Error, 'Failed retrieving users');
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return { isLoading, users: data };
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import { ResourceControlOwnership as RCO } from './resourceControlOwnership';
|
||||
|
||||
export function ResourceControlViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Type = data.Type;
|
||||
this.ResourceId = data.ResourceId;
|
||||
this.UserAccesses = data.UserAccesses;
|
||||
this.TeamAccesses = data.TeamAccesses;
|
||||
this.Public = data.Public;
|
||||
this.System = data.System;
|
||||
this.Ownership = determineOwnership(this);
|
||||
}
|
||||
|
||||
function determineOwnership(resourceControl) {
|
||||
if (resourceControl.Public) {
|
||||
return RCO.PUBLIC;
|
||||
} else if (resourceControl.UserAccesses.length === 1 && resourceControl.TeamAccesses.length === 0) {
|
||||
return RCO.PRIVATE;
|
||||
} else if (resourceControl.UserAccesses.length > 1 || resourceControl.TeamAccesses.length > 0) {
|
||||
return RCO.RESTRICTED;
|
||||
} else {
|
||||
return RCO.ADMINISTRATORS;
|
||||
}
|
||||
}
|
84
app/portainer/models/resourceControl/resourceControl.ts
Normal file
84
app/portainer/models/resourceControl/resourceControl.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { ResourceControlOwnership as RCO } from './resourceControlOwnership';
|
||||
|
||||
export enum ResourceControlType {
|
||||
// Container represents a resource control associated to a Docker container
|
||||
Container = 1,
|
||||
// Service represents a resource control associated to a Docker service
|
||||
Service,
|
||||
// Volume represents a resource control associated to a Docker volume
|
||||
Volume,
|
||||
// Network represents a resource control associated to a Docker network
|
||||
Network,
|
||||
// Secret represents a resource control associated to a Docker secret
|
||||
Secret,
|
||||
// Stack represents a resource control associated to a stack composed of Docker services
|
||||
Stack,
|
||||
// Config represents a resource control associated to a Docker config
|
||||
Config,
|
||||
// CustomTemplate represents a resource control associated to a custom template
|
||||
CustomTemplate,
|
||||
// ContainerGroup represents a resource control associated to an Azure container group
|
||||
ContainerGroup,
|
||||
}
|
||||
|
||||
export interface ResourceControlResponse {
|
||||
Id: number;
|
||||
Type: ResourceControlType;
|
||||
ResourceId: string | number;
|
||||
UserAccesses: unknown[];
|
||||
TeamAccesses: unknown[];
|
||||
Public: boolean;
|
||||
AdministratorsOnly: boolean;
|
||||
System: boolean;
|
||||
}
|
||||
|
||||
export class ResourceControlViewModel {
|
||||
Id: number;
|
||||
|
||||
Type: ResourceControlType;
|
||||
|
||||
ResourceId: string | number;
|
||||
|
||||
UserAccesses: unknown[];
|
||||
|
||||
TeamAccesses: unknown[];
|
||||
|
||||
Public: boolean;
|
||||
|
||||
System: boolean;
|
||||
|
||||
Ownership: RCO;
|
||||
|
||||
constructor(data: ResourceControlResponse) {
|
||||
this.Id = data.Id;
|
||||
this.Type = data.Type;
|
||||
this.ResourceId = data.ResourceId;
|
||||
this.UserAccesses = data.UserAccesses;
|
||||
this.TeamAccesses = data.TeamAccesses;
|
||||
this.Public = data.Public;
|
||||
this.System = data.System;
|
||||
this.Ownership = determineOwnership(this);
|
||||
}
|
||||
}
|
||||
|
||||
function determineOwnership(resourceControl: ResourceControlViewModel) {
|
||||
if (resourceControl.Public) {
|
||||
return RCO.PUBLIC;
|
||||
}
|
||||
|
||||
if (
|
||||
resourceControl.UserAccesses.length === 1 &&
|
||||
resourceControl.TeamAccesses.length === 0
|
||||
) {
|
||||
return RCO.PRIVATE;
|
||||
}
|
||||
|
||||
if (
|
||||
resourceControl.UserAccesses.length > 1 ||
|
||||
resourceControl.TeamAccesses.length > 0
|
||||
) {
|
||||
return RCO.RESTRICTED;
|
||||
}
|
||||
|
||||
return RCO.ADMINISTRATORS;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export const ResourceControlOwnership = Object.freeze({
|
||||
PUBLIC: 'public',
|
||||
PRIVATE: 'private',
|
||||
RESTRICTED: 'restricted',
|
||||
ADMINISTRATORS: 'administrators',
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export enum ResourceControlOwnership {
|
||||
PUBLIC = 'public',
|
||||
PRIVATE = 'private',
|
||||
RESTRICTED = 'restricted',
|
||||
ADMINISTRATORS = 'administrators',
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
|
||||
import PortainerError from '../error';
|
||||
import { get as localStorageGet } from '../hooks/useLocalStorage';
|
||||
|
||||
import {
|
||||
|
@ -40,3 +41,17 @@ export function agentInterceptor(config: AxiosRequestConfig) {
|
|||
}
|
||||
|
||||
axiosApiInstance.interceptors.request.use(agentInterceptor);
|
||||
|
||||
export function parseAxiosError(err: Error, msg = '') {
|
||||
let resultErr = err;
|
||||
let resultMsg = msg;
|
||||
|
||||
if ('isAxiosError' in err) {
|
||||
const axiosError = err as AxiosError;
|
||||
resultErr = new Error(`${axiosError.response?.data.message}`);
|
||||
const msgDetails = axiosError.response?.data.details;
|
||||
resultMsg = msg ? `${msg}: ${msgDetails}` : msgDetails;
|
||||
}
|
||||
|
||||
return new PortainerError(resultMsg, resultErr);
|
||||
}
|
||||
|
|
22
app/portainer/teams/teams.service.ts
Normal file
22
app/portainer/teams/teams.service.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { Team, TeamId } from './types';
|
||||
|
||||
export async function getTeams() {
|
||||
try {
|
||||
const { data } = await axios.get<Team[]>(buildUrl());
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(id?: TeamId) {
|
||||
let url = '/teams';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
6
app/portainer/teams/types.ts
Normal file
6
app/portainer/teams/types.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type TeamId = number;
|
||||
|
||||
export interface Team {
|
||||
Id: TeamId;
|
||||
Name: string;
|
||||
}
|
1
app/portainer/users/types.ts
Normal file
1
app/portainer/users/types.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type UserId = number;
|
Loading…
Add table
Add a link
Reference in a new issue