Update permissions checking code

This commit is contained in:
Dane Everitt 2020-03-29 14:19:17 -07:00
parent 2e9d327dfc
commit 8bc81c8c4b
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
7 changed files with 79 additions and 43 deletions

View file

@ -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
}
</> </>
); );
}; };

View file

@ -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>
); );
}); });

View file

@ -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>
); );
}; };

View file

@ -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>
); );
}; };

View 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;
};

View 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 ]);
};