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 { ServerContext } from '@/state/server';
import React from 'react';
import { usePermissions } from '@/plugins/usePermissions';
interface Props {
action: string | string[];
@ -8,35 +8,11 @@ interface Props {
}
const Can = ({ action, renderOnError, children }: Props) => {
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
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 ]);
const can = usePermissions(action);
return (
<>
{missingPermissionCount > 0 ?
renderOnError
:
children
}
{can.every(p => p) ? children : renderOnError}
</>
);
};

View file

@ -14,6 +14,8 @@ import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
import { ServerContext } from '@/state/server';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Can from '@/components/elements/Can';
import { usePermissions } from '@/plugins/usePermissions';
type Props = {
subuser?: Subuser;
@ -25,21 +27,32 @@ interface Values {
}
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;
&:not(.disabled) {
${tw`cursor-pointer`};
&:hover {
${tw`border-neutral-500 bg-neutral-800`};
}
}
`;
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
const [ canEditUser ] = usePermissions([ 'user.update' ]);
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
return (
<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'}/>
{!subuser &&
<div className={'mt-6'}>
@ -50,13 +63,14 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
/>
</div>
}
<div className={'mt-6'}>
<div className={'my-6'}>
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
<TitledGreyBox
key={key}
title={
<div className={'flex items-center'}>
<p className={'text-sm uppercase flex-1'}>{key}</p>
{canEditUser &&
<input
type={'checkbox'}
onClick={e => {
@ -78,6 +92,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
}
}}
/>
}
</div>
}
className={index !== 0 ? 'mt-4' : undefined}
@ -87,9 +102,11 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
</p>
{Object.keys(permissions[key].keys).map((pkey, index) => (
<PermissionLabel
key={`permission_${key}_${pkey}`}
htmlFor={`permission_${key}_${pkey}`}
className={classNames('transition-colors duration-75', {
'mt-2': index !== 0,
disabled: !canEditUser,
})}
>
<div className={'p-2'}>
@ -98,6 +115,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
name={'permissions'}
value={`${key}.${pkey}`}
className={'w-5 h-5 mr-2'}
disabled={!canEditUser}
/>
</div>
<div className={'flex-1'}>
@ -115,11 +133,13 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
</TitledGreyBox>
))}
</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'}>
{subuser ? 'Save' : 'Invite User'}
</button>
</div>
</Can>
</Modal>
);
});

View file

@ -7,6 +7,7 @@ import EditSubuserModal from '@/components/server/users/EditSubuserModal';
import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
import classNames from 'classnames';
import Can from '@/components/elements/Can';
interface Props {
subuser: Subuser;
@ -58,7 +59,9 @@ export default ({ subuser }: Props) => {
>
<FontAwesomeIcon icon={faPencilAlt}/>
</button>
<Can action={'user.delete'}>
<RemoveSubuserButton subuser={subuser}/>
</Can>
</div>
);
};

View file

@ -8,6 +8,7 @@ import UserRow from '@/components/server/users/UserRow';
import FlashMessageRender from '@/components/FlashMessageRender';
import getServerSubusers from '@/api/server/users/getServerSubusers';
import { httpErrorToHuman } from '@/api/http';
import Can from '@/components/elements/Can';
export default () => {
const [ loading, setLoading ] = useState(true);
@ -53,9 +54,11 @@ export default () => {
<UserRow key={subuser.uuid} subuser={subuser}/>
))
}
<Can action={'user.create'}>
<div className={'flex justify-end mt-6'}>
<AddSubuserButton/>
</div>
</Can>
</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 ]);
};