diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index e72d064c6..0763e68a8 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -14,9 +14,9 @@ import ( type resourceControlCreatePayload struct { // ResourceID string `example:"617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" validate:"required"` - // Type of Docker resource. Valid values are: container, volume\ - // service, secret, config or stack - Type string `example:"container" validate:"required"` + // Type of Resource. Valid values are: 1 - container, 2 - service + // 3 - volume, 4 - network, 5 - secret, 6 - stack, 7 - config, 8 - custom template, 9 - azure-container-group + Type portainer.ResourceControlType `example:"1" validate:"required" enums:"1,2,3,4,5,6,7,8,9"` // Permit access to the associated resource to any user Public bool `example:"true"` // Permit access to resource only to admins @@ -39,8 +39,8 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error { return errors.New("invalid payload: invalid resource identifier") } - if govalidator.IsNull(payload.Type) { - return errors.New("invalid payload: invalid type") + if payload.Type <= 0 || payload.Type >= 10 { + return errors.New("invalid payload: Invalid type value. Value must be one of: 1 - container, 2 - service, 3 - volume, 4 - network, 5 - secret, 6 - stack, 7 - config, 8 - custom template, 9 - azure-container-group") } if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly { @@ -75,29 +75,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - var resourceControlType portainer.ResourceControlType - switch payload.Type { - case "container": - resourceControlType = portainer.ContainerResourceControl - case "container-group": - resourceControlType = portainer.ContainerGroupResourceControl - case "service": - resourceControlType = portainer.ServiceResourceControl - case "volume": - resourceControlType = portainer.VolumeResourceControl - case "network": - resourceControlType = portainer.NetworkResourceControl - case "secret": - resourceControlType = portainer.SecretResourceControl - case "stack": - resourceControlType = portainer.StackResourceControl - case "config": - resourceControlType = portainer.ConfigResourceControl - default: - return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", errInvalidResourceControlType} - } - - rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType) + rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, payload.Type) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err} } @@ -126,7 +104,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req resourceControl := portainer.ResourceControl{ ResourceID: payload.ResourceID, SubResourceIDs: payload.SubResourceIDs, - Type: resourceControlType, + Type: payload.Type, Public: payload.Public, AdministratorsOnly: payload.AdministratorsOnly, UserAccesses: userAccesses, diff --git a/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.tsx b/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.tsx index 0aa9aa0e2..3c2d723de 100644 --- a/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.tsx +++ b/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.tsx @@ -6,10 +6,10 @@ import { Input, Select } from '@/portainer/components/form-components/Input'; import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle'; import { LoadingButton } from '@/portainer/components/Button/LoadingButton'; import { InputListError } from '@/portainer/components/form-components/InputList/InputList'; -import { AccessControlForm } from '@/portainer/components/accessControlForm'; import { ContainerInstanceFormValues } from '@/azure/types'; import * as notifications from '@/portainer/services/notifications'; -import { isAdmin, useUser } from '@/portainer/hooks/useUser'; +import { useUser } from '@/portainer/hooks/useUser'; +import { AccessControlForm } from '@/portainer/access-control/AccessControlForm'; import { validationSchema } from './CreateContainerInstanceForm.validation'; import { PortMapping, PortsMappingField } from './PortsMappingField'; @@ -29,19 +29,14 @@ export function CreateContainerInstanceForm() { throw new Error('endpointId url param is required'); } - const { user } = useUser(); - const isUserAdmin = isAdmin(user); + const { isAdmin } = useUser(); const { initialValues, isLoading, providers, subscriptions, resourceGroups } = - useLoadFormState(environmentId, isUserAdmin); + useLoadFormState(environmentId, isAdmin); const router = useRouter(); - const { mutateAsync } = useCreateInstance( - resourceGroups, - environmentId, - user?.Id - ); + const { mutateAsync } = useCreateInstance(resourceGroups, environmentId); if (isLoading) { return null; @@ -50,7 +45,7 @@ export function CreateContainerInstanceForm() { return ( initialValues={initialValues} - validationSchema={() => validationSchema(isUserAdmin)} + validationSchema={() => validationSchema(isAdmin)} onSubmit={onSubmit} validateOnMount validateOnChange diff --git a/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.validation.ts b/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.validation.ts index 3ba8d29d3..ec2c7feab 100644 --- a/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.validation.ts +++ b/app/azure/ContainerInstances/CreateContainerInstanceForm/CreateContainerInstanceForm.validation.ts @@ -1,6 +1,6 @@ import { object, string, number, boolean } from 'yup'; -import { validationSchema as accessControlSchema } from '@/portainer/components/accessControlForm/AccessControlForm.validation'; +import { validationSchema as accessControlSchema } from '@/portainer/access-control/AccessControlForm/AccessControlForm.validation'; import { validationSchema as portsSchema } from './PortsMappingField.validation'; diff --git a/app/azure/ContainerInstances/CreateContainerInstanceForm/useCreateInstanceMutation.tsx b/app/azure/ContainerInstances/CreateContainerInstanceForm/useCreateInstanceMutation.tsx index 6df05517f..9fe68e766 100644 --- a/app/azure/ContainerInstances/CreateContainerInstanceForm/useCreateInstanceMutation.tsx +++ b/app/azure/ContainerInstances/CreateContainerInstanceForm/useCreateInstanceMutation.tsx @@ -8,8 +8,7 @@ import { ContainerInstanceFormValues, ResourceGroup, } from '@/azure/types'; -import { UserId } from '@/portainer/users/types'; -import { applyResourceControl } from '@/portainer/resource-control/resource-control.service'; +import { applyResourceControl } from '@/portainer/access-control/access-control.service'; import { getSubscriptionResourceGroups } from './utils'; @@ -17,8 +16,7 @@ export function useCreateInstance( resourceGroups: { [k: string]: ResourceGroup[]; }, - environmentId: EnvironmentId, - userId?: UserId + environmentId: EnvironmentId ) { const queryClient = useQueryClient(); return useMutation( @@ -47,13 +45,13 @@ export function useCreateInstance( }, { async onSuccess(containerGroup, values) { - if (!userId) { - throw new Error('missing user id'); + const resourceControl = containerGroup.Portainer?.ResourceControl; + if (!resourceControl) { + throw new PortainerError('resource control expected after creation'); } - const resourceControl = containerGroup.Portainer.ResourceControl; const accessControlData = values.accessControl; - await applyResourceControl(userId, accessControlData, resourceControl); + await applyResourceControl(accessControlData, resourceControl); queryClient.invalidateQueries(['azure', 'container-instances']); }, } diff --git a/app/azure/ContainerInstances/CreateContainerInstanceForm/useLoadFormState.ts b/app/azure/ContainerInstances/CreateContainerInstanceForm/useLoadFormState.ts index d805ab03d..60ab74a02 100644 --- a/app/azure/ContainerInstances/CreateContainerInstanceForm/useLoadFormState.ts +++ b/app/azure/ContainerInstances/CreateContainerInstanceForm/useLoadFormState.ts @@ -9,7 +9,7 @@ import { getResourceGroups } from '@/azure/services/resource-groups.service'; import { getSubscriptions } from '@/azure/services/subscription.service'; import { getContainerInstanceProvider } from '@/azure/services/provider.service'; import { ContainerInstanceFormValues, Subscription } from '@/azure/types'; -import { parseFromResourceControl } from '@/portainer/components/accessControlForm/model'; +import { parseAccessControlFormData } from '@/portainer/access-control/utils'; import { getSubscriptionLocations, @@ -58,7 +58,7 @@ export function useLoadFormState( cpu: 1, ports: [{ container: '80', host: '80', protocol: 'TCP' }], allocatePublicIP: true, - accessControl: parseFromResourceControl(isUserAdmin), + accessControl: parseAccessControlFormData(isUserAdmin), }; return { diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js index 907ed55fb..b30f49109 100644 --- a/app/azure/models/container_group.js +++ b/app/azure/models/container_group.js @@ -1,5 +1,5 @@ import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function ContainerGroupDefaultModel() { this.Location = ''; diff --git a/app/azure/types.ts b/app/azure/types.ts index 881bb870e..9fc078acd 100644 --- a/app/azure/types.ts +++ b/app/azure/types.ts @@ -1,5 +1,7 @@ -import { AccessControlFormData } from '@/portainer/components/accessControlForm/model'; -import { ResourceControlResponse } from '@/portainer/models/resourceControl/resourceControl'; +import { + AccessControlFormData, + ResourceControlResponse, +} from '@/portainer/access-control/types'; import { PortMapping } from './ContainerInstances/CreateContainerInstanceForm/PortsMappingField'; diff --git a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html index 4abbe7098..f78c8bc4a 100644 --- a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html +++ b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html @@ -123,8 +123,12 @@ - - - - + + diff --git a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetailsController.js b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetailsController.js index 769fea6ff..985fd2a57 100644 --- a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetailsController.js +++ b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetailsController.js @@ -1,3 +1,5 @@ +import { ResourceControlType } from '@/portainer/access-control/types'; + class ContainerInstanceDetailsController { /* @ngInject */ constructor($state, AzureService, ContainerGroupService, Notifications, ResourceGroupService, SubscriptionService) { @@ -7,9 +9,16 @@ class ContainerInstanceDetailsController { loading: false, }; + this.resourceType = ResourceControlType.ContainerGroup; + this.container = null; this.subscription = null; this.resourceGroup = null; + this.onUpdateSuccess = this.onUpdateSuccess.bind(this); + } + + onUpdateSuccess() { + this.$state.reload(); } async $onInit() { diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js index 30a944693..b9a1abead 100644 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js +++ b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js @@ -1,4 +1,4 @@ -import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership'; +import { ResourceControlOwnership as RCO } from '@/portainer/access-control/types'; angular.module('portainer.docker').directive('networkRowContent', [ function networkRowContent() { diff --git a/app/docker/containers/components/ContainersDatatable/columns/ownership.tsx b/app/docker/containers/components/ContainersDatatable/columns/ownership.tsx index 21b451552..6aca8f19b 100644 --- a/app/docker/containers/components/ContainersDatatable/columns/ownership.tsx +++ b/app/docker/containers/components/ContainersDatatable/columns/ownership.tsx @@ -2,8 +2,8 @@ import { Column } from 'react-table'; import clsx from 'clsx'; import { ownershipIcon } from '@/portainer/filters/filters'; -import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership'; import type { DockerContainer } from '@/docker/containers/types'; +import { ResourceControlOwnership } from '@/portainer/access-control/types'; export const ownership: Column = { Header: 'Ownership', diff --git a/app/docker/containers/types.ts b/app/docker/containers/types.ts index 006f2dac2..37b2caadd 100644 --- a/app/docker/containers/types.ts +++ b/app/docker/containers/types.ts @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from '@/portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export type DockerContainerStatus = | 'paused' diff --git a/app/docker/models/config.js b/app/docker/models/config.js index 0beb8f85c..687b1587d 100644 --- a/app/docker/models/config.js +++ b/app/docker/models/config.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; function b64DecodeUnicode(str) { try { diff --git a/app/docker/models/container.js b/app/docker/models/container.js index 4df081717..a9ef10ad4 100644 --- a/app/docker/models/container.js +++ b/app/docker/models/container.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function createStatus(statusText) { var status = _.toLower(statusText); diff --git a/app/docker/models/network.js b/app/docker/models/network.js index 32c145efa..d1f8f8ad5 100644 --- a/app/docker/models/network.js +++ b/app/docker/models/network.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function NetworkViewModel(data) { this.Id = data.Id; diff --git a/app/docker/models/secret.js b/app/docker/models/secret.js index ecca5f077..559721b58 100644 --- a/app/docker/models/secret.js +++ b/app/docker/models/secret.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function SecretViewModel(data) { this.Id = data.ID; diff --git a/app/docker/models/service.js b/app/docker/models/service.js index df72a282f..bc3ebd3f9 100644 --- a/app/docker/models/service.js +++ b/app/docker/models/service.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function ServiceViewModel(data, runningTasks, allTasks) { this.Model = data; diff --git a/app/docker/models/volume.js b/app/docker/models/volume.js index 82ebcd0ba..7a629435a 100644 --- a/app/docker/models/volume.js +++ b/app/docker/models/volume.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; export function VolumeViewModel(data) { this.Id = data.Name; diff --git a/app/docker/views/configs/edit/config.html b/app/docker/views/configs/edit/config.html index 591ae9b8a..07153ae59 100644 --- a/app/docker/views/configs/edit/config.html +++ b/app/docker/views/configs/edit/config.html @@ -59,7 +59,14 @@ - + +
diff --git a/app/docker/views/configs/edit/configController.js b/app/docker/views/configs/edit/configController.js index 516d65b69..05aabea7a 100644 --- a/app/docker/views/configs/edit/configController.js +++ b/app/docker/views/configs/edit/configController.js @@ -1,3 +1,5 @@ +import { ResourceControlType } from '@/portainer/access-control/types'; + angular.module('portainer.docker').controller('ConfigController', [ '$scope', '$transition$', @@ -5,6 +7,12 @@ angular.module('portainer.docker').controller('ConfigController', [ 'ConfigService', 'Notifications', function ($scope, $transition$, $state, ConfigService, Notifications) { + $scope.resourceType = ResourceControlType.Config; + + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + $scope.removeConfig = function removeConfig(configId) { ConfigService.remove(configId) .then(function success() { diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index c00d2ea69..cdcedcf04 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -152,7 +152,14 @@
- + +
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 070450011..9833cba64 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -3,6 +3,7 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt'; import { FeatureId } from 'Portainer/feature-flags/enums'; +import { ResourceControlType } from '@/portainer/access-control/types'; angular.module('portainer.docker').controller('ContainerController', [ '$q', @@ -45,6 +46,7 @@ angular.module('portainer.docker').controller('ContainerController', [ Authentication, endpoint ) { + $scope.resourceType = ResourceControlType.Container; $scope.endpoint = endpoint; $scope.isAdmin = Authentication.isAdmin(); $scope.activityTime = 0; @@ -71,6 +73,10 @@ angular.module('portainer.docker').controller('ContainerController', [ $scope.updateRestartPolicy = updateRestartPolicy; + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + var update = function () { var nodeName = $transition$.params().nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index db24386fe..c5dbb7bec 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -69,14 +69,15 @@
- - +
diff --git a/app/docker/views/networks/edit/networkController.js b/app/docker/views/networks/edit/networkController.js index 76d17b4bd..7229cd582 100644 --- a/app/docker/views/networks/edit/networkController.js +++ b/app/docker/views/networks/edit/networkController.js @@ -1,3 +1,4 @@ +import { ResourceControlType } from '@/portainer/access-control/types'; import DockerNetworkHelper from 'Docker/helpers/networkHelper'; angular.module('portainer.docker').controller('NetworkController', [ @@ -11,6 +12,12 @@ angular.module('portainer.docker').controller('NetworkController', [ 'HttpRequestHelper', 'NetworkHelper', function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) { + $scope.resourceType = ResourceControlType.Network; + + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + $scope.removeNetwork = function removeNetwork() { NetworkService.remove($transition$.params().id, $transition$.params().id) .then(function success() { diff --git a/app/docker/views/secrets/edit/secret.html b/app/docker/views/secrets/edit/secret.html index daa5bc912..dfacd3a2b 100644 --- a/app/docker/views/secrets/edit/secret.html +++ b/app/docker/views/secrets/edit/secret.html @@ -56,5 +56,12 @@
- + + diff --git a/app/docker/views/secrets/edit/secretController.js b/app/docker/views/secrets/edit/secretController.js index 69fa6077c..a28b873b0 100644 --- a/app/docker/views/secrets/edit/secretController.js +++ b/app/docker/views/secrets/edit/secretController.js @@ -1,3 +1,5 @@ +import { ResourceControlType } from '@/portainer/access-control/types'; + angular.module('portainer.docker').controller('SecretController', [ '$scope', '$transition$', @@ -5,6 +7,12 @@ angular.module('portainer.docker').controller('SecretController', [ 'SecretService', 'Notifications', function ($scope, $transition$, $state, SecretService, Notifications) { + $scope.resourceType = ResourceControlType.Secret; + + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + $scope.removeSecret = function removeSecret(secretId) { SecretService.remove(secretId) .then(function success() { diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index 41cc84db4..e88af91e1 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -198,7 +198,14 @@ - + +
diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index 8a3115190..b32f62a66 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -21,6 +21,7 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import * as envVarsUtils from '@/portainer/helpers/env-vars'; +import { ResourceControlType } from '@/portainer/access-control/types'; angular.module('portainer.docker').controller('ServiceController', [ '$q', @@ -87,6 +88,12 @@ angular.module('portainer.docker').controller('ServiceController', [ RegistryService, endpoint ) { + $scope.resourceType = ResourceControlType.Service; + + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + $scope.endpoint = endpoint; $scope.state = { diff --git a/app/docker/views/volumes/edit/volume.html b/app/docker/views/volumes/edit/volume.html index 619e18222..8716038de 100644 --- a/app/docker/views/volumes/edit/volume.html +++ b/app/docker/views/volumes/edit/volume.html @@ -52,7 +52,14 @@
- + +
diff --git a/app/docker/views/volumes/edit/volumeController.js b/app/docker/views/volumes/edit/volumeController.js index 49d26374e..749ffe9d2 100644 --- a/app/docker/views/volumes/edit/volumeController.js +++ b/app/docker/views/volumes/edit/volumeController.js @@ -1,3 +1,5 @@ +import { ResourceControlType } from '@/portainer/access-control/types'; + angular.module('portainer.docker').controller('VolumeController', [ '$scope', '$state', @@ -9,6 +11,12 @@ angular.module('portainer.docker').controller('VolumeController', [ 'Notifications', 'HttpRequestHelper', function ($scope, $state, $transition$, $q, ModalService, VolumeService, ContainerService, Notifications, HttpRequestHelper) { + $scope.resourceType = ResourceControlType.Volume; + + $scope.onUpdateResourceControlSuccess = function () { + $state.reload(); + }; + $scope.removeVolume = function removeVolume() { ModalService.confirmDeletion('Do you want to remove this volume?', (confirmed) => { if (confirmed) { diff --git a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js index 0dc04d1b6..3e25ab6c1 100644 --- a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js +++ b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js @@ -1,5 +1,5 @@ +import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel'; import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; -import { ResourceControlViewModel } from '@/portainer/models/resourceControl/resourceControl'; class KubeEditCustomTemplateViewController { /* @ngInject */ diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 1a9e4e8aa..028137979 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -8,6 +8,7 @@ import userActivityModule from './user-activity'; import servicesModule from './services'; import teamsModule from './teams'; import homeModule from './home'; +import { accessControlModule } from './access-control'; async function initAuthentication(authManager, Authentication, $rootScope, $state) { authManager.checkAuthOnRefresh(); @@ -36,6 +37,7 @@ angular 'portainer.shared.datatable', servicesModule, teamsModule, + accessControlModule, ]) .config([ '$stateRegistryProvider', diff --git a/app/portainer/components/accessControlForm/AccessControlForm.stories.tsx b/app/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx similarity index 75% rename from app/portainer/components/accessControlForm/AccessControlForm.stories.tsx rename to app/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx index 0899560d6..726fba1dc 100644 --- a/app/portainer/components/accessControlForm/AccessControlForm.stories.tsx +++ b/app/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx @@ -3,11 +3,11 @@ 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 { parseAccessControlFormData } from '../utils'; + import { AccessControlForm } from './AccessControlForm'; -import { AccessControlFormData } from './model'; const meta: Meta = { title: 'Components/AccessControlForm', @@ -30,11 +30,8 @@ interface Args { } function Template({ userRole }: Args) { - const defaults = new AccessControlFormData(); - defaults.ownership = - userRole === Role.Admin - ? ResourceControlOwnership.ADMINISTRATORS - : ResourceControlOwnership.PRIVATE; + const isAdmin = userRole === Role.Admin; + const defaults = parseAccessControlFormData(isAdmin); const [value, setValue] = useState(defaults); @@ -46,7 +43,7 @@ function Template({ userRole }: Args) { return ( - + ); diff --git a/app/portainer/components/accessControlForm/AccessControlForm.test.tsx b/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx similarity index 56% rename from app/portainer/components/accessControlForm/AccessControlForm.test.tsx rename to app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx index 5ba65ed2a..c37fd9360 100644 --- a/app/portainer/components/accessControlForm/AccessControlForm.test.tsx +++ b/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx @@ -1,36 +1,118 @@ 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 } from '@/react-tools/test-utils'; -import { Team } from '@/portainer/teams/types'; -import { ResourceControlViewModel } from '@/portainer/models/resourceControl/resourceControl'; +import { Team, TeamId } from '@/portainer/teams/types'; import { createMockTeams } from '@/react-tools/test-mocks'; +import { UserId } from '@/portainer/users/types'; + +import { ResourceControlOwnership, AccessControlFormData } from '../types'; +import { ResourceControlViewModel } from '../models/ResourceControlViewModel'; import { AccessControlForm } from './AccessControlForm'; -import { AccessControlFormData } from './model'; test('renders correctly', async () => { - const values: AccessControlFormData = new AccessControlFormData(); + const values = buildFormData(); 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(); +test.each([ + [ResourceControlOwnership.ADMINISTRATORS], + [ResourceControlOwnership.PRIVATE], + [ResourceControlOwnership.RESTRICTED], +])( + `when ownership is %s, ownership selector should be visible`, + async (ownership) => { + const values = buildFormData(ownership); - const { queryByRole } = await renderComponent(values); + const { findByRole, getByLabelText } = await renderComponent(values); + const accessSwitch = getByLabelText(/Enable access control/); - expect(queryByRole('radiogroup')).toBeVisible(); -}); + expect(accessSwitch).toBeEnabled(); -test('when AccessControlEnabled is false, ownership selector should be hidden', async () => { - const values: AccessControlFormData = { - ...new AccessControlFormData(), - accessControlEnabled: false, - }; + await expect(findByRole('radiogroup')).resolves.toBeVisible(); + } +); + +test.each([ + [ResourceControlOwnership.ADMINISTRATORS], + [ResourceControlOwnership.PRIVATE], + [ResourceControlOwnership.RESTRICTED], +])( + 'when isAdmin and ownership is %s, ownership selector should show admin and restricted options', + async (ownership) => { + const values = buildFormData(ownership); + + 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.each([ + [ResourceControlOwnership.ADMINISTRATORS], + [ResourceControlOwnership.PRIVATE], + [ResourceControlOwnership.RESTRICTED], +])( + `when user is not an admin and %s and no teams, should have only private option`, + async (ownership) => { + const values = buildFormData(ownership); + + 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.each([ + [ResourceControlOwnership.ADMINISTRATORS], + [ResourceControlOwnership.PRIVATE], + [ResourceControlOwnership.RESTRICTED], +])( + `when user is not an admin and %s and there is 1 team, should have private and restricted options`, + async (ownership) => { + const values = buildFormData(ownership); + + 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 ownership is public, ownership selector should be hidden', async () => { + const values = buildFormData(ResourceControlOwnership.PUBLIC); const { queryByRole } = await renderComponent(values); @@ -38,7 +120,7 @@ test('when AccessControlEnabled is false, ownership selector should be hidden', }); test('when hideTitle is true, title should be hidden', async () => { - const values = new AccessControlFormData(); + const values = buildFormData(); const { queryByRole } = await renderComponent(values, jest.fn(), { hideTitle: true, @@ -47,30 +129,8 @@ test('when hideTitle is true, title should be hidden', async () => { 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, - }; +test('when isAdmin and admin ownership is selected, no extra options are visible', async () => { + const values = buildFormData(ResourceControlOwnership.ADMINISTRATORS); const { findByRole, queryByLabelText } = await renderComponent( values, @@ -95,11 +155,8 @@ test('when isAdmin, AccessControlEnabled and admin ownership is selected, no ext 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, - }; +test('when isAdmin and restricted ownership is selected, show team and users selectors', async () => { + const values = buildFormData(ResourceControlOwnership.RESTRICTED); const { findByRole, findByLabelText } = await renderComponent( values, @@ -136,43 +193,8 @@ test('when isAdmin, AccessControlEnabled and restricted ownership is selected, s 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, - }; +test('when user is not an admin, there are more then 1 team and ownership is restricted, team selector should be visible', async () => { + const values = buildFormData(ResourceControlOwnership.RESTRICTED); const { findByRole, findByLabelText } = await renderComponent( values, @@ -202,11 +224,8 @@ test('when user is not an admin, access control is enabled, there are more then 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, - }; +test('when user is not an admin, there is 1 team and ownership is restricted, team selector not should be visible', async () => { + const values = buildFormData(ResourceControlOwnership.RESTRICTED); const { findByRole, findByLabelText } = await renderComponent( values, @@ -240,11 +259,8 @@ test('when user is not an admin, access control is enabled, there is 1 team and 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, - }; +test('when user is not an admin, and ownership is restricted, user selector not should be visible', async () => { + const values = buildFormData(ResourceControlOwnership.RESTRICTED); const { findByRole, findByLabelText } = await renderComponent( values, @@ -299,6 +315,7 @@ async function renderComponent( const renderResult = renderWithQueryClient( ; +} + +export function AccessControlForm({ + values, + onChange, + hideTitle, + formNamespace, + errors, +}: Props) { + const { isAdmin } = useUser(); + + const accessControlEnabled = + values.ownership !== ResourceControlOwnership.PUBLIC; + return ( + <> + {!hideTitle && Access control} + +
+
+ +
+
+ + {accessControlEnabled && ( + + )} + + ); + + function withNamespace(name: string) { + return formNamespace ? `${formNamespace}.${name}` : name; + } + + function handleToggleEnable(accessControlEnabled: boolean) { + let ownership = ResourceControlOwnership.PUBLIC; + if (accessControlEnabled) { + ownership = isAdmin + ? ResourceControlOwnership.ADMINISTRATORS + : ResourceControlOwnership.PRIVATE; + } + onChange({ ...values, ownership }); + } +} diff --git a/app/portainer/access-control/AccessControlForm/AccessControlForm.validation.test.ts b/app/portainer/access-control/AccessControlForm/AccessControlForm.validation.test.ts new file mode 100644 index 000000000..483c2e4fb --- /dev/null +++ b/app/portainer/access-control/AccessControlForm/AccessControlForm.validation.test.ts @@ -0,0 +1,82 @@ +import { ResourceControlOwnership } from '../types'; + +import { validationSchema } from './AccessControlForm.validation'; + +test('when ownership not restricted, should be valid', async () => { + const schema = validationSchema(true); + [ + ResourceControlOwnership.ADMINISTRATORS, + ResourceControlOwnership.PRIVATE, + ResourceControlOwnership.PUBLIC, + ].forEach(async (ownership) => { + const object = { ownership }; + + await expect( + schema.validate(object, { strict: true }) + ).resolves.toStrictEqual(object); + }); +}); + +test('when 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( + { + ownership: ResourceControlOwnership.RESTRICTED, + authorizedTeams: [], + authorizedUsers: [], + }, + { strict: true } + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); +}); + +test('when ownership is restricted, and the user is admin should have either teams or users', async () => { + const schema = validationSchema(true); + const teams = { + ownership: ResourceControlOwnership.RESTRICTED, + authorizedTeams: [1], + authorizedUsers: [], + }; + + await expect(schema.validate(teams, { strict: true })).resolves.toStrictEqual( + teams + ); + + const users = { + ownership: ResourceControlOwnership.RESTRICTED, + authorizedTeams: [], + authorizedUsers: [1], + }; + + await expect(schema.validate(users, { strict: true })).resolves.toStrictEqual( + users + ); + + const both = { + ownership: ResourceControlOwnership.RESTRICTED, + authorizedTeams: [1], + authorizedUsers: [2], + }; + + await expect(schema.validate(both, { strict: true })).resolves.toStrictEqual( + both + ); +}); + +test('when ownership is restricted, user is not admin with teams, should be valid', async () => { + const schema = validationSchema(false); + + const object = { + ownership: ResourceControlOwnership.RESTRICTED, + authorizedTeams: [1], + authorizedUsers: [], + }; + + await expect( + schema.validate(object, { strict: true }) + ).resolves.toStrictEqual(object); +}); diff --git a/app/portainer/components/accessControlForm/AccessControlForm.validation.ts b/app/portainer/access-control/AccessControlForm/AccessControlForm.validation.ts similarity index 56% rename from app/portainer/components/accessControlForm/AccessControlForm.validation.ts rename to app/portainer/access-control/AccessControlForm/AccessControlForm.validation.ts index 66630bc98..174367031 100644 --- a/app/portainer/components/accessControlForm/AccessControlForm.validation.ts +++ b/app/portainer/access-control/AccessControlForm/AccessControlForm.validation.ts @@ -1,17 +1,13 @@ -import { object, string, array, number, bool } from 'yup'; +import { object, string, array, number } from 'yup'; -import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership'; +import { ResourceControlOwnership } from '../types'; export function validationSchema(isAdmin: boolean) { return object() .shape({ - accessControlEnabled: bool(), ownership: string() .oneOf(Object.values(ResourceControlOwnership)) - .when('accessControlEnabled', { - is: true, - then: (schema) => schema.required(), - }), + .required(), authorizedUsers: array(number()), authorizedTeams: array(number()), }) @@ -20,16 +16,8 @@ export function validationSchema(isAdmin: boolean) { isAdmin ? 'You must specify at least one team or user.' : 'You must specify at least one team.', - ({ - accessControlEnabled, - ownership, - authorizedTeams, - authorizedUsers, - }) => { - if ( - !accessControlEnabled || - ownership !== ResourceControlOwnership.RESTRICTED - ) { + ({ ownership, authorizedTeams, authorizedUsers }) => { + if (ownership !== ResourceControlOwnership.RESTRICTED) { return true; } diff --git a/app/portainer/access-control/AccessControlForm/__snapshots__/AccessControlForm.validation.test.ts.snap b/app/portainer/access-control/AccessControlForm/__snapshots__/AccessControlForm.validation.test.ts.snap new file mode 100644 index 000000000..8a1abff11 --- /dev/null +++ b/app/portainer/access-control/AccessControlForm/__snapshots__/AccessControlForm.validation.test.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`when ownership is restricted and no teams or users, should be invalid 1`] = `"You must specify at least one team or user."`; + +exports[`when ownership is restricted and no teams or users, should be invalid 2`] = `"You must specify at least one team."`; diff --git a/app/portainer/components/accessControlForm/index.ts b/app/portainer/access-control/AccessControlForm/index.ts similarity index 100% rename from app/portainer/components/accessControlForm/index.ts rename to app/portainer/access-control/AccessControlForm/index.ts diff --git a/app/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx b/app/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx new file mode 100644 index 000000000..dd78df8af --- /dev/null +++ b/app/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx @@ -0,0 +1,173 @@ +import _ from 'lodash'; + +import { createMockTeams, createMockUsers } from '@/react-tools/test-mocks'; +import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { rest, server } from '@/setup-tests/server'; +import { Role } from '@/portainer/users/types'; + +import { + ResourceControlOwnership, + ResourceControlType, + TeamResourceAccess, + UserResourceAccess, +} from '../types'; +import { ResourceControlViewModel } from '../models/ResourceControlViewModel'; + +import { AccessControlPanelDetails } from './AccessControlPanelDetails'; + +test.each([ + [ResourceControlOwnership.ADMINISTRATORS], + [ResourceControlOwnership.PRIVATE], + [ResourceControlOwnership.PUBLIC], + [ResourceControlOwnership.RESTRICTED], +])( + 'when resource control with ownership %s is supplied, show its ownership', + async (ownership) => { + const resourceControl = buildViewModel(ownership); + const { queryByLabelText } = await renderComponent( + ResourceControlType.Container, + resourceControl + ); + + expect(queryByLabelText('ownership')).toHaveTextContent(ownership); + } +); + +test('when resource control is not supplied, show administrators', async () => { + const { queryByLabelText } = await renderComponent( + ResourceControlType.Container + ); + + expect(queryByLabelText('ownership')).toHaveTextContent( + ResourceControlOwnership.ADMINISTRATORS + ); +}); + +const inheritanceTests = [ + { + resourceType: ResourceControlType.Container, + parentType: ResourceControlType.Service, + }, + { + resourceType: ResourceControlType.Volume, + parentType: ResourceControlType.Container, + }, + ...[ + ResourceControlType.Config, + ResourceControlType.Container, + ResourceControlType.Network, + ResourceControlType.Secret, + ResourceControlType.Service, + ResourceControlType.Volume, + ].map((resourceType) => ({ + resourceType, + parentType: ResourceControlType.Stack, + })), +]; + +for (let i = 0; i < inheritanceTests.length; i += 1) { + const { resourceType, parentType } = inheritanceTests[i]; + test(`when resource is ${ResourceControlType[resourceType]} and resource control is ${ResourceControlType[parentType]}, show message`, async () => { + const resourceControl = buildViewModel( + ResourceControlOwnership.ADMINISTRATORS, + parentType + ); + + const { queryByLabelText } = await renderComponent( + resourceType, + resourceControl + ); + const inheritanceMessage = queryByLabelText('inheritance-message'); + expect(inheritanceMessage).toBeVisible(); + }); +} + +test('when resource is limited to specific users, show comma separated list of their names', async () => { + const users = createMockUsers(10, Role.Standard); + + server.use(rest.get('/api/users', (req, res, ctx) => res(ctx.json(users)))); + + const restrictedToUsers = _.sampleSize(users, 3); + + const resourceControl = buildViewModel( + ResourceControlOwnership.RESTRICTED, + ResourceControlType.Service, + restrictedToUsers.map((user) => ({ + UserId: user.Id, + AccessLevel: 1, + })) + ); + + const { queryByText, findByLabelText } = await renderComponent( + undefined, + resourceControl + ); + + expect(queryByText(/Authorized users/)).toBeVisible(); + + await expect(findByLabelText('authorized-users')).resolves.toHaveTextContent( + restrictedToUsers.map((user) => user.Username).join(', ') + ); +}); + +test('when resource is limited to specific teams, show comma separated list of their names', async () => { + const teams = createMockTeams(10); + + server.use(rest.get('/api/teams', (req, res, ctx) => res(ctx.json(teams)))); + + const restrictedToTeams = _.sampleSize(teams, 3); + + const resourceControl = buildViewModel( + ResourceControlOwnership.RESTRICTED, + ResourceControlType.Config, + [], + restrictedToTeams.map((team) => ({ + TeamId: team.Id, + AccessLevel: 1, + })) + ); + + const { queryByText, findByLabelText } = await renderComponent( + undefined, + resourceControl + ); + + expect(queryByText(/Authorized teams/)).toBeVisible(); + + await expect(findByLabelText('authorized-teams')).resolves.toHaveTextContent( + restrictedToTeams.map((team) => team.Name).join(', ') + ); +}); + +async function renderComponent( + resourceType: ResourceControlType = ResourceControlType.Container, + resourceControl?: ResourceControlViewModel +) { + const queries = renderWithQueryClient( + + ); + await expect(queries.findByText('Ownership')).resolves.toBeVisible(); + + return queries; +} + +function buildViewModel( + ownership: ResourceControlOwnership, + type: ResourceControlType = ResourceControlType.Config, + users: UserResourceAccess[] = [], + teams: TeamResourceAccess[] = [] +): ResourceControlViewModel { + return { + Id: 0, + Public: false, + ResourceId: 0, + System: false, + TeamAccesses: teams, + Ownership: ownership, + Type: type, + UserAccesses: users, + }; +} diff --git a/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx b/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx new file mode 100644 index 000000000..0a35479d9 --- /dev/null +++ b/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx @@ -0,0 +1,152 @@ +import { useReducer } from 'react'; + +import { Button } from '@/portainer/components/Button'; +import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget'; +import { useUser } from '@/portainer/hooks/useUser'; +import { r2a } from '@/react-tools/react2angular'; +import { TeamMembership, Role } from '@/portainer/teams/types'; +import { useUserMembership } from '@/portainer/users/queries'; + +import { ResourceControlType, ResourceId } from '../types'; +import { ResourceControlViewModel } from '../models/ResourceControlViewModel'; + +import { AccessControlPanelDetails } from './AccessControlPanelDetails'; +import { AccessControlPanelForm } from './AccessControlPanelForm'; + +interface Props { + resourceControl?: ResourceControlViewModel; + resourceType: ResourceControlType; + resourceId: ResourceId; + disableOwnershipChange?: boolean; + onUpdateSuccess(): void; +} + +export function AccessControlPanel({ + resourceControl, + resourceType, + disableOwnershipChange, + resourceId, + onUpdateSuccess, +}: Props) { + const [isEditMode, toggleEditMode] = useReducer((state) => !state, false); + const { isAdmin } = useUser(); + + const isInherited = checkIfInherited(); + + const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } = + useRestrictions(resourceControl); + + const isEditDisabled = + disableOwnershipChange || + isInherited || + (!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams); + + return ( +
+
+ + + + + + {!isEditDisabled && !isEditMode && ( +
+
+