Skip to content

Commit a1c3730

Browse files
committed
Update frontend to only allow selection of valid permissions for subusers
1 parent 00b0d30 commit a1c3730

File tree

4 files changed

+52
-10
lines changed

4 files changed

+52
-10
lines changed

app/Http/Controllers/Api/Client/Servers/SubuserController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Request;
66
use Pterodactyl\Models\Server;
77
use Illuminate\Http\JsonResponse;
8+
use Pterodactyl\Models\Permission;
89
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
910
use Pterodactyl\Services\Subusers\SubuserCreationService;
1011
use Pterodactyl\Transformers\Api\Client\SubuserTransformer;
@@ -123,6 +124,6 @@ public function delete(DeleteSubuserRequest $request, Server $server)
123124
*/
124125
protected function getDefaultPermissions(Request $request): array
125126
{
126-
return array_unique(array_merge($request->input('permissions') ?? [], ['websocket.connect']));
127+
return array_unique(array_merge($request->input('permissions') ?? [], [Permission::ACTION_WEBSOCKET_CONNECT]));
127128
}
128129
}

resources/scripts/components/server/users/EditSubuserModal.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, useRef } from 'react';
1+
import React, { forwardRef, useEffect, useRef } from 'react';
22
import { Subuser } from '@/state/server/subusers';
33
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
44
import { array, object, string } from 'yup';
@@ -16,6 +16,7 @@ import { httpErrorToHuman } from '@/api/http';
1616
import FlashMessageRender from '@/components/FlashMessageRender';
1717
import Can from '@/components/elements/Can';
1818
import { usePermissions } from '@/plugins/usePermissions';
19+
import { useDeepMemo } from '@/plugins/useDeepMemo';
1920

2021
type Props = {
2122
subuser?: Subuser;
@@ -37,12 +38,38 @@ const PermissionLabel = styled.label`
3738
${tw`border-neutral-500 bg-neutral-800`};
3839
}
3940
}
41+
42+
&.disabled {
43+
${tw`opacity-50`};
44+
45+
& input[type="checkbox"]:not(:checked) {
46+
${tw`border-0`};
47+
}
48+
}
4049
`;
4150

4251
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
4352
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
44-
const [ canEditUser ] = usePermissions([ 'user.update' ]);
45-
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
53+
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
54+
const permissions = useStoreState(state => state.permissions.data);
55+
56+
// The currently logged in user's permissions. We're going to filter out any permissions
57+
// that they should not need.
58+
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
59+
60+
// The permissions that can be modified by this user.
61+
const editablePermissions = useDeepMemo(() => {
62+
const cleaned = Object.keys(permissions)
63+
.map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`));
64+
65+
const list: string[] = ([] as string[]).concat.apply([], Object.values(cleaned));
66+
67+
if (loggedInPermissions.length === 1 && loggedInPermissions[0] === '*') {
68+
return list;
69+
}
70+
71+
return list.filter(key => loggedInPermissions.indexOf(key) >= 0);
72+
}, [permissions, loggedInPermissions]);
4673

4774
return (
4875
<Modal {...props} top={false} showSpinnerOverlay={isSubmitting}>
@@ -54,6 +81,12 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
5481
}
5582
</h3>
5683
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/>
84+
<div className={'mt-4 pl-4 py-2 border-l-4 border-cyan-400'}>
85+
<p className={'text-sm text-neutral-300'}>
86+
Only permissions which your account is currently assigned may be selected when creating or
87+
modifying other users.
88+
</p>
89+
</div>
5790
{!subuser &&
5891
<div className={'mt-6'}>
5992
<Field
@@ -70,7 +103,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
70103
title={
71104
<div className={'flex items-center'}>
72105
<p className={'text-sm uppercase flex-1'}>{key}</p>
73-
{canEditUser &&
106+
{canEditUser && editablePermissions.indexOf(key) >= 0 &&
74107
<input
75108
type={'checkbox'}
76109
onClick={e => {
@@ -106,7 +139,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
106139
htmlFor={`permission_${key}_${pkey}`}
107140
className={classNames('transition-colors duration-75', {
108141
'mt-2': index !== 0,
109-
disabled: !canEditUser,
142+
disabled: !canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0,
110143
})}
111144
>
112145
<div className={'p-2'}>
@@ -115,7 +148,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
115148
name={'permissions'}
116149
value={`${key}.${pkey}`}
117150
className={'w-5 h-5 mr-2'}
118-
disabled={!canEditUser}
151+
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
119152
/>
120153
</div>
121154
<div className={'flex-1'}>
@@ -133,7 +166,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
133166
</TitledGreyBox>
134167
))}
135168
</div>
136-
<Can action={subuser ? 'user.update' : 'user.delete'}>
169+
<Can action={subuser ? 'user.update' : 'user.create'}>
137170
<div className={'pb-6 flex justify-end'}>
138171
<button className={'btn btn-primary btn-sm'} type={'submit'}>
139172
{subuser ? 'Save' : 'Invite User'}
@@ -169,6 +202,10 @@ export default ({ subuser, ...props }: Props) => {
169202
});
170203
};
171204

205+
useEffect(() => {
206+
clearFlashes('user:edit');
207+
}, []);
208+
172209
return (
173210
<Formik
174211
onSubmit={submit}

resources/scripts/components/server/users/UserRow.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
88
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
99
import classNames from 'classnames';
1010
import Can from '@/components/elements/Can';
11+
import { useStoreState } from 'easy-peasy';
1112

1213
interface Props {
1314
subuser: Subuser;
1415
}
1516

1617
export default ({ subuser }: Props) => {
18+
const uuid = useStoreState(state => state.user!.data!.uuid);
1719
const [ visible, setVisible ] = useState(false);
1820

1921
return (
@@ -54,7 +56,9 @@ export default ({ subuser }: Props) => {
5456
<button
5557
type={'button'}
5658
aria-label={'Edit subuser'}
57-
className={'block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4'}
59+
className={classNames('block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4', {
60+
hidden: subuser.uuid === uuid,
61+
})}
5862
onClick={() => setVisible(true)}
5963
>
6064
<FontAwesomeIcon icon={faPencilAlt}/>

resources/styles/components/forms.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ a.btn {
229229
}
230230

231231
input[type="checkbox"], input[type="radio"] {
232-
@apply .appearance-none .inline-block .align-middle .select-none .flex-no-shrink .w-4 .h-4 .text-primary-400 .border .border-neutral-300 .rounded-sm;
232+
@apply .cursor-pointer .appearance-none .inline-block .align-middle .select-none .flex-no-shrink .w-4 .h-4 .text-primary-400 .border .border-neutral-300 .rounded-sm;
233233
color-adjust: exact;
234234
background-origin: border-box;
235235
transition: all 75ms linear, box-shadow 25ms linear;

0 commit comments

Comments
 (0)