Compare commits

..

1 Commits

Author SHA1 Message Date
Elias Schneider
f61c784988 feat: restrict oidc clients by user groups per default 2025-12-23 13:51:35 +01:00
10 changed files with 90 additions and 15 deletions

View File

@@ -18,6 +18,7 @@ type OidcClientDto struct {
IsPublic bool `json:"isPublic"` IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"` PkceEnabled bool `json:"pkceEnabled"`
Credentials OidcClientCredentialsDto `json:"credentials"` Credentials OidcClientCredentialsDto `json:"credentials"`
IsGroupRestricted bool `json:"isGroupRestricted"`
} }
type OidcClientWithAllowedUserGroupsDto struct { type OidcClientWithAllowedUserGroupsDto struct {
@@ -43,6 +44,7 @@ type OidcClientUpdateDto struct {
HasDarkLogo bool `json:"hasDarkLogo"` HasDarkLogo bool `json:"hasDarkLogo"`
LogoURL *string `json:"logoUrl"` LogoURL *string `json:"logoUrl"`
DarkLogoURL *string `json:"darkLogoUrl"` DarkLogoURL *string `json:"darkLogoUrl"`
IsGroupRestricted bool `json:"isGroupRestricted"`
} }
type OidcClientCreateDto struct { type OidcClientCreateDto struct {

View File

@@ -58,6 +58,7 @@ type OidcClient struct {
RequiresReauthentication bool `sortable:"true" filterable:"true"` RequiresReauthentication bool `sortable:"true" filterable:"true"`
Credentials OidcClientCredentials Credentials OidcClientCredentials
LaunchURL *string LaunchURL *string
IsGroupRestricted bool
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"` AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
CreatedByID *string CreatedByID *string

View File

@@ -226,7 +226,7 @@ func (s *OidcService) hasAuthorizedClientInternal(ctx context.Context, clientID,
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client // IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool { func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
if len(client.AllowedUserGroups) == 0 { if !client.IsGroupRestricted {
return true return true
} }
@@ -816,6 +816,7 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.PkceEnabled = input.IsPublic || input.PkceEnabled client.PkceEnabled = input.IsPublic || input.PkceEnabled
client.RequiresReauthentication = input.RequiresReauthentication client.RequiresReauthentication = input.RequiresReauthentication
client.LaunchURL = input.LaunchURL client.LaunchURL = input.LaunchURL
client.IsGroupRestricted = input.IsGroupRestricted
// Credentials // Credentials
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities)) client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))

View File

@@ -0,0 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,13 @@
PRAGMA foreign_keys= OFF;
BEGIN;
ALTER TABLE oidc_clients
ADD COLUMN is_group_restricted BOOLEAN NOT NULL DEFAULT 0;
UPDATE oidc_clients
SET is_group_restricted = (SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END
FROM oidc_client_user_groups
WHERE oidc_client_user_groups.oidc_client_id = oidc_clients.id);
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -301,13 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.", "are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
"generate": "Generate", "generate": "Generate",
"new_client_secret_created_successfully": "New client secret created successfully", "new_client_secret_created_successfully": "New client secret created successfully",
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
"oidc_client_name": "OIDC Client {name}", "oidc_client_name": "OIDC Client {name}",
"client_id": "Client ID", "client_id": "Client ID",
"client_secret": "Client secret", "client_secret": "Client secret",
"show_more_details": "Show more details", "show_more_details": "Show more details",
"allowed_user_groups": "Allowed User Groups", "allowed_user_groups": "Allowed User Groups",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.", "allowed_user_groups_description": "Select user groups to restrict signing in to this client to only users in these groups.",
"allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.",
"unrestrict": "Unrestrict",
"restrict": "Restrict",
"user_groups_restriction_updated_successfully": "User groups restriction updated successfully",
"favicon": "Favicon", "favicon": "Favicon",
"light_mode_logo": "Light Mode Logo", "light_mode_logo": "Light Mode Logo",
"dark_mode_logo": "Dark Mode Logo", "dark_mode_logo": "Dark Mode Logo",

View File

@@ -12,6 +12,8 @@
title, title,
description, description,
defaultExpanded = false, defaultExpanded = false,
forcedExpanded,
button,
icon, icon,
children children
}: { }: {
@@ -19,7 +21,9 @@
title: string; title: string;
description?: string; description?: string;
defaultExpanded?: boolean; defaultExpanded?: boolean;
forcedExpanded?: boolean;
icon?: typeof IconType; icon?: typeof IconType;
button?: Snippet;
children: Snippet; children: Snippet;
} = $props(); } = $props();
@@ -47,6 +51,12 @@
} }
loadExpandedState(); loadExpandedState();
}); });
$effect(() => {
if (forcedExpanded !== undefined) {
expanded = forcedExpanded;
}
});
</script> </script>
<Card.Root> <Card.Root>
@@ -63,11 +73,18 @@
<Card.Description>{description}</Card.Description> <Card.Description>{description}</Card.Description>
{/if} {/if}
</div> </div>
{#if button}
{@render button()}
{:else}
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}> <Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
<LucideChevronDown <LucideChevronDown
class={cn('size-5 transition-transform duration-200', expanded && 'rotate-180 transform')} class={cn(
'size-5 transition-transform duration-200',
expanded && 'rotate-180 transform'
)}
/> />
</Button> </Button>
{/if}
</div> </div>
</Card.Header> </Card.Header>
{#if expanded} {#if expanded}

View File

@@ -28,6 +28,7 @@ export type OidcClient = OidcClientMetaData & {
requiresReauthentication: boolean; requiresReauthentication: boolean;
credentials?: OidcClientCredentials; credentials?: OidcClientCredentials;
launchURL?: string; launchURL?: string;
isGroupRestricted: boolean;
}; };
export type OidcClientWithAllowedUserGroups = OidcClient & { export type OidcClientWithAllowedUserGroups = OidcClient & {

View File

@@ -80,6 +80,16 @@
return success; return success;
} }
async function toggleGroupRestriction() {
client.isGroupRestricted = !client.isGroupRestricted;
await oidcService
.updateClient(client.id, client)
.then(() => {
toast.success(m.user_groups_restriction_updated_successfully());
})
.catch(axiosErrorToast);
}
async function createClientSecret() { async function createClientSecret() {
openConfirmDialog({ openConfirmDialog({
title: m.create_new_client_secret(), title: m.create_new_client_secret(),
@@ -104,7 +114,7 @@
await oidcService await oidcService
.updateAllowedUserGroups(client.id, allowedGroups) .updateAllowedUserGroups(client.id, allowedGroups)
.then(() => { .then(() => {
toast.success(m.allowed_user_groups_updated_successfully()); toast.success(m.user_groups_restriction_updated_successfully());
}) })
.catch((e) => { .catch((e) => {
axiosErrorToast(e); axiosErrorToast(e);
@@ -120,6 +130,14 @@
<title>{m.oidc_client_name({ name: client.name })}</title> <title>{m.oidc_client_name({ name: client.name })}</title>
</svelte:head> </svelte:head>
{#snippet UnrestrictButton()}
<Button
onclick={toggleGroupRestriction}
variant={client.isGroupRestricted ? 'secondary' : 'default'}
>{client.isGroupRestricted ? m.unrestrict() : m.restrict()}</Button
>
{/snippet}
<div> <div>
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go} <button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
><LucideChevronLeft class="size-5" /> {m.back()}</button ><LucideChevronLeft class="size-5" /> {m.back()}</button
@@ -193,12 +211,23 @@
<CollapsibleCard <CollapsibleCard
id="allowed-user-groups" id="allowed-user-groups"
title={m.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} /> {#if client.isGroupRestricted}
<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={toggleGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
<Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button> <Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
</div> </div>
{/if}
</CollapsibleCard> </CollapsibleCard>
<Card.Root> <Card.Root>
<Card.Header> <Card.Header>

View File

@@ -102,7 +102,8 @@
logo: $inputs.logoUrl?.value ? undefined : logo, logo: $inputs.logoUrl?.value ? undefined : logo,
logoUrl: $inputs.logoUrl?.value, logoUrl: $inputs.logoUrl?.value,
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo, 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; const hasLogo = logo != null || !!$inputs.logoUrl?.value;