Update permissions checking code
This commit is contained in:
parent
2e9d327dfc
commit
8bc81c8c4b
7 changed files with 79 additions and 43 deletions
|
@ -1,5 +1,5 @@
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { ServerContext } from '@/state/server';
|
import { usePermissions } from '@/plugins/usePermissions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
action: string | string[];
|
action: string | string[];
|
||||||
|
@ -8,35 +8,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Can = ({ action, renderOnError, children }: Props) => {
|
const Can = ({ action, renderOnError, children }: Props) => {
|
||||||
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
const can = usePermissions(action);
|
||||||
const actions = Array.isArray(action) ? action : [ action ];
|
|
||||||
|
|
||||||
const missingPermissionCount = useMemo(() => {
|
|
||||||
if (userPermissions[0] === '*') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions.filter(permission => {
|
|
||||||
return !(
|
|
||||||
// Allows checking for any permission matching a name, for example files.*
|
|
||||||
// will return if the user has any permission under the file.XYZ namespace.
|
|
||||||
(
|
|
||||||
permission.endsWith('.*') &&
|
|
||||||
permission !== 'websocket.*' &&
|
|
||||||
userPermissions.filter(p => p.startsWith(permission.split('.')[0])).length > 0
|
|
||||||
)
|
|
||||||
// Otherwise just check if the entire permission exists in the array or not.
|
|
||||||
|| userPermissions.indexOf(permission) >= 0);
|
|
||||||
}).length;
|
|
||||||
}, [ action, userPermissions ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{missingPermissionCount > 0 ?
|
{can.every(p => p) ? children : renderOnError}
|
||||||
renderOnError
|
|
||||||
:
|
|
||||||
children
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,8 @@ import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
import { usePermissions } from '@/plugins/usePermissions';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subuser?: Subuser;
|
subuser?: Subuser;
|
||||||
|
@ -25,21 +27,32 @@ interface Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionLabel = styled.label`
|
const PermissionLabel = styled.label`
|
||||||
${tw`flex items-center border border-transparent rounded p-2 cursor-pointer`};
|
${tw`flex items-center border border-transparent rounded p-2`};
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|
||||||
|
&:not(.disabled) {
|
||||||
|
${tw`cursor-pointer`};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${tw`border-neutral-500 bg-neutral-800`};
|
${tw`border-neutral-500 bg-neutral-800`};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
|
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
|
||||||
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
|
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||||
|
const [ canEditUser ] = usePermissions([ 'user.update' ]);
|
||||||
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
|
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props} showSpinnerOverlay={isSubmitting}>
|
<Modal {...props} showSpinnerOverlay={isSubmitting}>
|
||||||
<h3 ref={ref}>{subuser ? `Modify permissions for ${subuser.email}` : 'Create new subuser'}</h3>
|
<h3 ref={ref}>
|
||||||
|
{subuser ?
|
||||||
|
`${canEditUser ? 'Modify' : 'View'} permissions for ${subuser.email}`
|
||||||
|
:
|
||||||
|
'Create new subuser'
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/>
|
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/>
|
||||||
{!subuser &&
|
{!subuser &&
|
||||||
<div className={'mt-6'}>
|
<div className={'mt-6'}>
|
||||||
|
@ -50,13 +63,14 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className={'mt-6'}>
|
<div className={'my-6'}>
|
||||||
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
|
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
|
||||||
<TitledGreyBox
|
<TitledGreyBox
|
||||||
key={key}
|
key={key}
|
||||||
title={
|
title={
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
<p className={'text-sm uppercase flex-1'}>{key}</p>
|
<p className={'text-sm uppercase flex-1'}>{key}</p>
|
||||||
|
{canEditUser &&
|
||||||
<input
|
<input
|
||||||
type={'checkbox'}
|
type={'checkbox'}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
@ -78,6 +92,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={index !== 0 ? 'mt-4' : undefined}
|
className={index !== 0 ? 'mt-4' : undefined}
|
||||||
|
@ -87,9 +102,11 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
</p>
|
</p>
|
||||||
{Object.keys(permissions[key].keys).map((pkey, index) => (
|
{Object.keys(permissions[key].keys).map((pkey, index) => (
|
||||||
<PermissionLabel
|
<PermissionLabel
|
||||||
|
key={`permission_${key}_${pkey}`}
|
||||||
htmlFor={`permission_${key}_${pkey}`}
|
htmlFor={`permission_${key}_${pkey}`}
|
||||||
className={classNames('transition-colors duration-75', {
|
className={classNames('transition-colors duration-75', {
|
||||||
'mt-2': index !== 0,
|
'mt-2': index !== 0,
|
||||||
|
disabled: !canEditUser,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={'p-2'}>
|
<div className={'p-2'}>
|
||||||
|
@ -98,6 +115,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
name={'permissions'}
|
name={'permissions'}
|
||||||
value={`${key}.${pkey}`}
|
value={`${key}.${pkey}`}
|
||||||
className={'w-5 h-5 mr-2'}
|
className={'w-5 h-5 mr-2'}
|
||||||
|
disabled={!canEditUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
|
@ -115,11 +133,13 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
</TitledGreyBox>
|
</TitledGreyBox>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 pb-6 flex justify-end'}>
|
<Can action={subuser ? 'user.update' : 'user.delete'}>
|
||||||
|
<div className={'pb-6 flex justify-end'}>
|
||||||
<button className={'btn btn-primary btn-sm'} type={'submit'}>
|
<button className={'btn btn-primary btn-sm'} type={'submit'}>
|
||||||
{subuser ? 'Save' : 'Invite User'}
|
{subuser ? 'Save' : 'Invite User'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</Can>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ import EditSubuserModal from '@/components/server/users/EditSubuserModal';
|
||||||
import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
|
import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
|
||||||
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
|
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
subuser: Subuser;
|
subuser: Subuser;
|
||||||
|
@ -58,7 +59,9 @@ export default ({ subuser }: Props) => {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||||
</button>
|
</button>
|
||||||
|
<Can action={'user.delete'}>
|
||||||
<RemoveSubuserButton subuser={subuser}/>
|
<RemoveSubuserButton subuser={subuser}/>
|
||||||
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import UserRow from '@/components/server/users/UserRow';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import getServerSubusers from '@/api/server/users/getServerSubusers';
|
import getServerSubusers from '@/api/server/users/getServerSubusers';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
@ -53,9 +54,11 @@ export default () => {
|
||||||
<UserRow key={subuser.uuid} subuser={subuser}/>
|
<UserRow key={subuser.uuid} subuser={subuser}/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
<Can action={'user.create'}>
|
||||||
<div className={'flex justify-end mt-6'}>
|
<div className={'flex justify-end mt-6'}>
|
||||||
<AddSubuserButton/>
|
<AddSubuserButton/>
|
||||||
</div>
|
</div>
|
||||||
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
12
resources/scripts/plugins/useDeepMemo.ts
Normal file
12
resources/scripts/plugins/useDeepMemo.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
|
export const useDeepMemo = <T, K> (fn: () => T, key: K): T => {
|
||||||
|
const ref = useRef<{ key: K, value: T }>();
|
||||||
|
|
||||||
|
if (!ref.current || !isEqual(key, ref.current.key)) {
|
||||||
|
ref.current = { key, value: fn() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current.value;
|
||||||
|
};
|
22
resources/scripts/plugins/usePermissions.ts
Normal file
22
resources/scripts/plugins/usePermissions.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import { useDeepMemo } from '@/plugins/useDeepMemo';
|
||||||
|
|
||||||
|
export const usePermissions = (action: string | string[]): boolean[] => {
|
||||||
|
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
||||||
|
|
||||||
|
return useDeepMemo(() => {
|
||||||
|
return (Array.isArray(action) ? action : [ action ])
|
||||||
|
.map(permission => (
|
||||||
|
// Allows checking for any permission matching a name, for example files.*
|
||||||
|
// will return if the user has any permission under the file.XYZ namespace.
|
||||||
|
(
|
||||||
|
permission.endsWith('.*') &&
|
||||||
|
permission !== 'websocket.*' &&
|
||||||
|
userPermissions.filter(p => p.startsWith(permission.split('.')[0])).length > 0
|
||||||
|
) ||
|
||||||
|
// Otherwise just check if the entire permission exists in the array or not.
|
||||||
|
userPermissions.indexOf(permission) >= 0
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [ action, userPermissions ]);
|
||||||
|
};
|
Loading…
Reference in a new issue