fix: prevent user rename if they are an oidc user
This commit is contained in:
parent
8d1132606a
commit
0ad578e651
@ -17,6 +17,7 @@ import cn from '~/utils/cn';
|
|||||||
interface MenuProps extends MenuTriggerProps {
|
interface MenuProps extends MenuTriggerProps {
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
disabledKeys?: Key[];
|
||||||
children: [
|
children: [
|
||||||
React.ReactElement<ButtonProps> | React.ReactElement<IconButtonProps>,
|
React.ReactElement<ButtonProps> | React.ReactElement<IconButtonProps>,
|
||||||
React.ReactElement<MenuPanelProps>,
|
React.ReactElement<MenuPanelProps>,
|
||||||
@ -26,7 +27,7 @@ interface MenuProps extends MenuTriggerProps {
|
|||||||
// TODO: onAction is called twice for some reason?
|
// TODO: onAction is called twice for some reason?
|
||||||
// TODO: isDisabled per-prop
|
// TODO: isDisabled per-prop
|
||||||
function Menu(props: MenuProps) {
|
function Menu(props: MenuProps) {
|
||||||
const { placement = 'bottom', isDisabled } = props;
|
const { placement = 'bottom', isDisabled, disabledKeys = [] } = props;
|
||||||
const state = useMenuTriggerState(props);
|
const state = useMenuTriggerState(props);
|
||||||
const ref = useRef<HTMLButtonElement | null>(null);
|
const ref = useRef<HTMLButtonElement | null>(null);
|
||||||
const { menuTriggerProps, menuProps } = useMenuTrigger<object>(
|
const { menuTriggerProps, menuProps } = useMenuTrigger<object>(
|
||||||
@ -51,6 +52,7 @@ function Menu(props: MenuProps) {
|
|||||||
...menuProps,
|
...menuProps,
|
||||||
autoFocus: state.focusStrategy ?? true,
|
autoFocus: state.focusStrategy ?? true,
|
||||||
onClose: () => state.close(),
|
onClose: () => state.close(),
|
||||||
|
disabledKeys,
|
||||||
})}
|
})}
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
@ -60,6 +62,7 @@ function Menu(props: MenuProps) {
|
|||||||
|
|
||||||
interface MenuPanelProps extends AriaMenuProps<object> {
|
interface MenuPanelProps extends AriaMenuProps<object> {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
disabledKeys?: Key[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function Panel(props: MenuPanelProps) {
|
function Panel(props: MenuPanelProps) {
|
||||||
@ -74,7 +77,12 @@ function Panel(props: MenuPanelProps) {
|
|||||||
className="pt-1 pb-1 shadow-xs rounded-md min-w-[200px] focus:outline-none"
|
className="pt-1 pb-1 shadow-xs rounded-md min-w-[200px] focus:outline-none"
|
||||||
>
|
>
|
||||||
{[...state.collection].map((item) => (
|
{[...state.collection].map((item) => (
|
||||||
<MenuSection key={item.key} section={item} state={state} />
|
<MenuSection
|
||||||
|
key={item.key}
|
||||||
|
section={item}
|
||||||
|
state={state}
|
||||||
|
disabledKeys={props.disabledKeys}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
@ -83,9 +91,10 @@ function Panel(props: MenuPanelProps) {
|
|||||||
interface MenuSectionProps<T> {
|
interface MenuSectionProps<T> {
|
||||||
section: Node<T>;
|
section: Node<T>;
|
||||||
state: TreeState<T>;
|
state: TreeState<T>;
|
||||||
|
disabledKeys?: Key[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuSection<T>({ section, state }: MenuSectionProps<T>) {
|
function MenuSection<T>({ section, state, disabledKeys }: MenuSectionProps<T>) {
|
||||||
const { itemProps, groupProps } = useMenuSection({
|
const { itemProps, groupProps } = useMenuSection({
|
||||||
heading: section.rendered,
|
heading: section.rendered,
|
||||||
'aria-label': section['aria-label'],
|
'aria-label': section['aria-label'],
|
||||||
@ -109,7 +118,12 @@ function MenuSection<T>({ section, state }: MenuSectionProps<T>) {
|
|||||||
<li {...itemProps}>
|
<li {...itemProps}>
|
||||||
<ul {...groupProps}>
|
<ul {...groupProps}>
|
||||||
{[...section.childNodes].map((item) => (
|
{[...section.childNodes].map((item) => (
|
||||||
<MenuItem key={item.key} item={item} state={state} />
|
<MenuItem
|
||||||
|
key={item.key}
|
||||||
|
item={item}
|
||||||
|
state={state}
|
||||||
|
isDisabled={disabledKeys?.includes(item.key)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@ -120,14 +134,14 @@ function MenuSection<T>({ section, state }: MenuSectionProps<T>) {
|
|||||||
interface MenuItemProps<T> {
|
interface MenuItemProps<T> {
|
||||||
item: Node<T>;
|
item: Node<T>;
|
||||||
state: TreeState<T>;
|
state: TreeState<T>;
|
||||||
|
isDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem<T>({ item, state }: MenuItemProps<T>) {
|
function MenuItem<T>({ item, state, isDisabled }: MenuItemProps<T>) {
|
||||||
const ref = useRef<HTMLLIElement | null>(null);
|
const ref = useRef<HTMLLIElement | null>(null);
|
||||||
const { menuItemProps } = useMenuItem({ key: item.key }, state, ref);
|
const { menuItemProps } = useMenuItem({ key: item.key }, state, ref);
|
||||||
|
|
||||||
const isFocused = state.selectionManager.focusedKey === item.key;
|
const isFocused = state.selectionManager.focusedKey === item.key;
|
||||||
const isDisabled = state.selectionManager.isDisabled(item.key);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function UserMenu({ user }: MenuProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Menu>
|
<Menu disabledKeys={user.provider === 'oidc' ? ['rename'] : []}>
|
||||||
<Menu.IconButton
|
<Menu.IconButton
|
||||||
label="Machine Options"
|
label="Machine Options"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { LoadContext } from '~/server';
|
|||||||
import { Capabilities, Roles } from '~/server/web/roles';
|
import { Capabilities, Roles } from '~/server/web/roles';
|
||||||
import { AuthSession } from '~/server/web/sessions';
|
import { AuthSession } from '~/server/web/sessions';
|
||||||
import { User } from '~/types';
|
import { User } from '~/types';
|
||||||
|
import { data400, data403 } from '~/utils/res';
|
||||||
|
|
||||||
export async function userAction({
|
export async function userAction({
|
||||||
request,
|
request,
|
||||||
@ -11,14 +12,14 @@ export async function userAction({
|
|||||||
const session = await context.sessions.auth(request);
|
const session = await context.sessions.auth(request);
|
||||||
const check = await context.sessions.check(request, Capabilities.write_users);
|
const check = await context.sessions.check(request, Capabilities.write_users);
|
||||||
if (!check) {
|
if (!check) {
|
||||||
return data({ success: false }, 403);
|
throw data403('You do not have permission to update users');
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = session.get('api_key')!;
|
const apiKey = session.get('api_key')!;
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const action = formData.get('action_id')?.toString();
|
const action = formData.get('action_id')?.toString();
|
||||||
if (!action) {
|
if (!action) {
|
||||||
return data({ success: false }, 400);
|
throw data400('Missing `action_id` in the form data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@ -31,7 +32,7 @@ export async function userAction({
|
|||||||
case 'reassign_user':
|
case 'reassign_user':
|
||||||
return reassignUser(formData, apiKey, context, session);
|
return reassignUser(formData, apiKey, context, session);
|
||||||
default:
|
default:
|
||||||
return data({ success: false }, 400);
|
throw data400('Invalid `action_id` provided.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ async function createUser(
|
|||||||
const email = formData.get('email')?.toString();
|
const email = formData.get('email')?.toString();
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return data({ success: false }, 400);
|
throw data400('Missing `username` in the form data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.client.post('v1/user', apiKey, {
|
await context.client.post('v1/user', apiKey, {
|
||||||
@ -62,7 +63,7 @@ async function deleteUser(
|
|||||||
) {
|
) {
|
||||||
const userId = formData.get('user_id')?.toString();
|
const userId = formData.get('user_id')?.toString();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return data({ success: false }, 400);
|
throw data400('Missing `user_id` in the form data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.client.delete(`v1/user/${userId}`, apiKey);
|
await context.client.delete(`v1/user/${userId}`, apiKey);
|
||||||
@ -79,6 +80,21 @@ async function renameUser(
|
|||||||
return data({ success: false }, 400);
|
return data({ success: false }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { users } = await context.client.get<{ users: User[] }>(
|
||||||
|
'v1/user',
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = users.find((user) => user.id === userId);
|
||||||
|
if (!user) {
|
||||||
|
throw data400(`No user found with id: ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.provider === 'oidc') {
|
||||||
|
// OIDC users cannot be renamed via this endpoint, return an error
|
||||||
|
throw data403('Users managed by OIDC cannot be renamed');
|
||||||
|
}
|
||||||
|
|
||||||
await context.client.post(`v1/user/${userId}/rename/${newName}`, apiKey);
|
await context.client.post(`v1/user/${userId}/rename/${newName}`, apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +102,11 @@ async function reassignUser(
|
|||||||
formData: FormData,
|
formData: FormData,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
session: Session<AuthSession, unknown>,
|
|
||||||
) {
|
) {
|
||||||
const userId = formData.get('user_id')?.toString();
|
const userId = formData.get('user_id')?.toString();
|
||||||
const newRole = formData.get('new_role')?.toString();
|
const newRole = formData.get('new_role')?.toString();
|
||||||
if (!userId || !newRole) {
|
if (!userId || !newRole) {
|
||||||
return data({ success: false }, 400);
|
throw data400('Missing `user_id` or `new_role` in the form data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { users } = await context.client.get<{ users: User[] }>(
|
const { users } = await context.client.get<{ users: User[] }>(
|
||||||
@ -101,14 +116,16 @@ async function reassignUser(
|
|||||||
|
|
||||||
const user = users.find((user) => user.id === userId);
|
const user = users.find((user) => user.id === userId);
|
||||||
if (!user?.providerId) {
|
if (!user?.providerId) {
|
||||||
return data({ success: false }, 400);
|
throw data400('Specified user is not an OIDC user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// For some reason, headscale makes providerID a url where the
|
// For some reason, headscale makes providerID a url where the
|
||||||
// last component is the subject, so we need to strip that out
|
// last component is the subject, so we need to strip that out
|
||||||
const subject = user.providerId?.split('/').pop();
|
const subject = user.providerId?.split('/').pop();
|
||||||
if (!subject) {
|
if (!subject) {
|
||||||
return data({ success: false }, 400);
|
throw data400(
|
||||||
|
'Malformed `providerId` for the specified user. Cannot find subject.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await context.sessions.reassignSubject(
|
const result = await context.sessions.reassignSubject(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user