feat: restrict oidc clients by user groups per default (#1164)

This commit is contained in:
Elias Schneider
2025-12-24 09:09:25 +01:00
committed by GitHub
parent e358c433f0
commit f75cef83d5
30 changed files with 469 additions and 102 deletions

View File

@@ -80,6 +80,44 @@
return success;
}
async function enableGroupRestriction() {
client.isGroupRestricted = true;
await oidcService
.updateClient(client.id, {
...client,
isGroupRestricted: true
})
.then(() => {
toast.success(m.user_groups_restriction_updated_successfully());
client.isGroupRestricted = true;
})
.catch(axiosErrorToast);
}
function disableGroupRestriction() {
openConfirmDialog({
title: m.unrestrict_oidc_client({ clientName: client.name }),
message: m.confirm_unrestrict_oidc_client_description({ clientName: client.name }),
confirm: {
label: m.unrestrict(),
destructive: true,
action: async () => {
await oidcService
.updateClient(client.id, {
...client,
isGroupRestricted: false
})
.then(() => {
toast.success(m.user_groups_restriction_updated_successfully());
client.allowedUserGroupIds = [];
client.isGroupRestricted = false;
})
.catch(axiosErrorToast);
}
}
});
}
async function createClientSecret() {
openConfirmDialog({
title: m.create_new_client_secret(),
@@ -120,6 +158,13 @@
<title>{m.oidc_client_name({ name: client.name })}</title>
</svelte:head>
{#snippet UnrestrictButton()}
<Button
onclick={enableGroupRestriction}
variant={client.isGroupRestricted ? 'secondary' : 'default'}>{m.restrict()}</Button
>
{/snippet}
<div>
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
><LucideChevronLeft class="size-5" /> {m.back()}</button
@@ -193,10 +238,19 @@
<CollapsibleCard
id="allowed-user-groups"
title={m.allowed_user_groups()}
description={m.add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups()}
button={!client.isGroupRestricted ? UnrestrictButton : undefined}
forcedExpanded={client.isGroupRestricted ? undefined : false}
description={client.isGroupRestricted
? m.allowed_user_groups_description()
: m.allowed_user_groups_status_unrestricted_description()}
>
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
<div class="mt-5 flex justify-end">
<UserGroupSelection
bind:selectedGroupIds={client.allowedUserGroupIds}
selectionDisabled={!client.isGroupRestricted}
/>
<div class="mt-5 flex justify-end gap-3">
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
<Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
</div>
</CollapsibleCard>

View File

@@ -102,7 +102,8 @@
logo: $inputs.logoUrl?.value ? undefined : logo,
logoUrl: $inputs.logoUrl?.value,
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo,
darkLogoUrl: $inputs.darkLogoUrl?.value
darkLogoUrl: $inputs.darkLogoUrl?.value,
isGroupRestricted: existingClient?.isGroupRestricted ?? true
});
const hasLogo = logo != null || !!$inputs.logoUrl?.value;

View File

@@ -15,11 +15,13 @@
import { backNavigate } from '../../users/navigate-back-util';
import UserGroupForm from '../user-group-form.svelte';
import UserSelection from '../user-selection.svelte';
import OidcClientSelection from './oidc-client-selection.svelte';
let { data } = $props();
let userGroup = $state({
...data.userGroup,
userIds: data.userGroup.users.map((u) => u.id)
userIds: data.userGroup.users.map((u) => u.id),
allowedOidcClientIds: data.userGroup.allowedOidcClients.map((c) => c.id)
});
const userGroupService = new UserGroupService();
@@ -56,6 +58,17 @@
axiosErrorToast(e);
});
}
async function updateAllowedOidcClients(allowedClients: string[]) {
await userGroupService
.updateAllowedOidcClients(userGroup.id, allowedClients)
.then(() => {
toast.success(m.allowed_oidc_clients_updated_successfully());
})
.catch((e) => {
axiosErrorToast(e);
});
}
</script>
<svelte:head>
@@ -110,3 +123,16 @@
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
</div>
</CollapsibleCard>
<CollapsibleCard
id="user-group-oidc-clients"
title={m.allowed_oidc_clients()}
description={m.allowed_oidc_clients_description()}
>
<OidcClientSelection bind:selectedGroupIds={userGroup.allowedOidcClientIds} />
<div class="mt-5 flex justify-end gap-3">
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
>{m.save()}</Button
>
</div>
</CollapsibleCard>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import ImageBox from '$lib/components/image-box.svelte';
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
import { m } from '$lib/paraglide/messages';
import OidcService from '$lib/services/oidc-service';
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
import type { ListRequestOptions } from '$lib/types/list-request.type';
import type { OidcClient } from '$lib/types/oidc.type';
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import { mode } from 'mode-watcher';
let {
selectedGroupIds = $bindable()
}: {
selectedGroupIds: string[];
} = $props();
const oidcClientService = new OidcService();
const isLightMode = $derived(mode.current === 'light');
const columns: AdvancedTableColumn<OidcClient>[] = [
{ label: 'ID', column: 'id', hidden: true },
{ label: m.logo(), key: 'logo', cell: LogoCell },
{ label: m.name(), column: 'name', sortable: true },
{
label: m.client_launch_url(),
column: 'launchURL',
hidden: true
},
{
label: m.public_client(),
column: 'isPublic',
sortable: true,
hidden: true
}
];
async function fetchCallback(requestOptions: ListRequestOptions) {
const clients = await oidcClientService.listClients(requestOptions);
const unrestrictedClientIds = clients.data.filter((c) => !c.isGroupRestricted).map((c) => c.id);
selectedGroupIds = [...new Set([...selectedGroupIds, ...unrestrictedClientIds])];
return clients;
}
</script>
{#snippet LogoCell({ item }: { item: OidcClient })}
{#if item.hasLogo}
<ImageBox
class="size-12 rounded-lg"
src={cachedOidcClientLogo.getUrl(item.id, isLightMode)}
alt={m.name_logo({ name: item.name })}
/>
{:else}
<div class="bg-muted flex size-12 items-center justify-center rounded-lg text-lg font-bold">
{item.name.charAt(0).toUpperCase()}
</div>
{/if}
{/snippet}
<AdvancedTable
id="oidc-client-selection"
{fetchCallback}
defaultSort={{ column: 'name', direction: 'asc' }}
bind:selectedIds={selectedGroupIds}
rowSelectionDisabled={(item) => !item.isGroupRestricted}
{columns}
/>

View File

@@ -10,19 +10,19 @@
AdvancedTableColumn,
CreateAdvancedTableActions
} from '$lib/types/advanced-table.type';
import type { UserGroup, UserGroupWithUserCount } from '$lib/types/user-group.type';
import type { UserGroupMinimal } from '$lib/types/user-group.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { LucidePencil, LucideTrash } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
const userGroupService = new UserGroupService();
let tableRef: AdvancedTable<UserGroupWithUserCount>;
let tableRef: AdvancedTable<UserGroupMinimal>;
export function refresh() {
return tableRef?.refresh();
}
const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
const columns: AdvancedTableColumn<UserGroupMinimal>[] = [
{ label: 'ID', column: 'id', hidden: true },
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
{ label: m.name(), column: 'name', sortable: true },
@@ -38,7 +38,7 @@
{ label: m.source(), key: 'source', hidden: !$appConfigStore.ldapEnabled, cell: SourceCell }
];
const actions: CreateAdvancedTableActions<UserGroupWithUserCount> = (group) => [
const actions: CreateAdvancedTableActions<UserGroupMinimal> = (group) => [
{
label: m.edit(),
primary: true,
@@ -55,7 +55,7 @@
}
];
async function deleteUserGroup(userGroup: UserGroup) {
async function deleteUserGroup(userGroup: UserGroupMinimal) {
openConfirmDialog({
title: m.delete_name({ name: userGroup.name }),
message: m.are_you_sure_you_want_to_delete_this_user_group(),
@@ -76,7 +76,7 @@
}
</script>
{#snippet SourceCell({ item }: { item: UserGroupWithUserCount })}
{#snippet SourceCell({ item }: { item: UserGroupMinimal })}
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}>
{item.ldapId ? m.ldap() : m.local()}
</Badge>