Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/app-frontend/src/hooks/oidc-login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';

import { roleFromRoles } from './oidc-login';

describe('roleFromRoles', () => {
it('returns admin when groups include admins', () => {
expect(roleFromRoles(['tenant-admin'], ['admins'])).toBe('admin');
});

it('returns admin when groups include admins even with tenant-idp-manager role', () => {
expect(roleFromRoles(['tenant-idp-manager'], ['admins'])).toBe('admin');
});

it('returns tenant-admin when roles include tenant-admin and not in admins group', () => {
expect(roleFromRoles(['tenant-admin'], [])).toBe('tenant-admin');
});

it('returns tenant-admin over tenant-idp-manager when both roles present', () => {
expect(roleFromRoles(['tenant-admin', 'tenant-idp-manager'], [])).toBe('tenant-admin');
});

it('returns tenant-idp-manager when roles include tenant-idp-manager and not tenant-admin', () => {
expect(roleFromRoles(['tenant-idp-manager'], [])).toBe('tenant-idp-manager');
});

it('returns tenant-user when no recognized group or role is present', () => {
expect(roleFromRoles([], [])).toBe('tenant-user');
});

it('returns tenant-user for unrecognized roles and groups', () => {
expect(roleFromRoles(['some-other-role'], ['some-group'])).toBe('tenant-user');
});
});
19 changes: 11 additions & 8 deletions apps/app-frontend/src/hooks/oidc-login.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import * as React from 'react';

import { DemoShellRole } from '@osac/ui-components/shellTypes';
import type { UserRole } from '@osac/ui-components/shellTypes';
import { getErrorMessage } from '@osac/ui-components/utils/error';

const roleFromRoles = (roles: string[], groups: string[]): DemoShellRole => {
export const roleFromRoles = (roles: string[] = [], groups: string[] = []): UserRole => {
if (groups.includes('admins')) {
return 'providerAdmin';
return 'admin';
}
if (roles.includes('tenant-admin')) {
return 'tenantAdmin';
return 'tenant-admin';
}
return 'tenantUser';
if (roles.includes('tenant-idp-manager')) {
return 'tenant-idp-manager';
}
return 'tenant-user';
};

const fetchLoginInfo = async (): Promise<{
Expand Down Expand Up @@ -41,15 +44,15 @@ const maxTimeout = 2 ** 31 - 1;

export const useOIDCLogin = (): [
string,
DemoShellRole,
UserRole,
boolean,
string | undefined,
() => Promise<void>,
] => {
const [triggerRelogin, setTriggerRelogin] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true);
const [username, setUsername] = React.useState<string>('');
const [role, setRole] = React.useState<DemoShellRole>('tenantUser');
const [role, setRole] = React.useState<UserRole>('tenant-user');
const [error, setError] = React.useState<string>();

const refreshTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -133,7 +136,7 @@ export const useOIDCLogin = (): [
if (result) {
setError(undefined);
setUsername(result.username);
setRole(roleFromRoles(result.roles ?? [], result.groups ?? []));
setRole(roleFromRoles(result.roles, result.groups));
setIsLoading(false);
scheduleRefresh();
} else {
Expand Down
1 change: 0 additions & 1 deletion apps/app-frontend/src/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export const AppShell = ({ logout }: { logout: () => Promise<void> }) => {
</ShellRoute>
}
/>

<Route path="*" element={<Navigate to={defaultRoute} replace />} />
</Routes>
</Page>
Expand Down
7 changes: 4 additions & 3 deletions apps/app-frontend/src/shell/ShellMasthead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ import { UserIcon } from '@patternfly/react-icons/dist/esm/icons/user-icon';

import UserPreferencesModal from '@osac/ui-components/components/UserPreferences/UserPreferencesModal';
import { useSession } from '@osac/ui-components/hooks/use-session';
import { useTranslation } from '@osac/ui-components/hooks/useTranslation';
import { userRoleLabels } from '@osac/ui-components/shellTypes';
import { getErrorMessage } from '@osac/ui-components/utils/error';

import { operatingModeLabel } from './shellLabels';

interface ShellMastheadProps {
onLogout: () => Promise<void>;
}

export const ShellMasthead = ({ onLogout }: ShellMastheadProps) => {
const { t } = useTranslation();
const [isUserMenuOpen, setIsUserMenuOpen] = React.useState(false);
const [isPreferencesOpen, setPreferencesOpen] = React.useState(false);
const [logoutError, setLogoutError] = React.useState<string>();
Expand Down Expand Up @@ -102,7 +103,7 @@ export const ShellMasthead = ({ onLogout }: ShellMastheadProps) => {
>
{displayName}{' '}
<Label color="grey" variant="outline" isCompact>
{operatingModeLabel(role)}
{userRoleLabels(t)[role]}
</Label>
</MenuToggle>
)}
Expand Down
11 changes: 0 additions & 11 deletions apps/app-frontend/src/shell/shellLabels.ts

This file was deleted.

8 changes: 4 additions & 4 deletions apps/app-frontend/src/shell/shellNav.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { describe, expect, it } from 'vitest';

import type { DemoShellRole } from '@osac/ui-components/shellTypes';
import type { UserRole } from '@osac/ui-components/shellTypes';
import { tIdentity } from '@osac/ui-components/test-utils/i18n';

import { navRowsForRole } from './shellNav';

const roles: DemoShellRole[] = ['tenantUser', 'tenantAdmin', 'providerAdmin'];
const roles: UserRole[] = ['tenant-user', 'tenant-admin', 'tenant-idp-manager', 'admin'];

const findSection = (role: DemoShellRole, sectionId: string) =>
const findSection = (role: UserRole, sectionId: string) =>
navRowsForRole(role, tIdentity).find((row) => row.sectionId === sectionId);

const servicesChildren = (role: DemoShellRole) =>
const servicesChildren = (role: UserRole) =>
findSection(role, 'nav-tenant-services')?.children ?? [];

describe('navRowsForRole', () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/app-frontend/src/shell/shellNav.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** Role-based sidebar navigation (sectioned NavGroup layout). Nav icons: shellNavIcon in @osac/ui-components/icons */
import type { TFunction } from 'i18next';

import type { DemoShellRole } from '@osac/ui-components/shellTypes';
import type { UserRole } from '@osac/ui-components/shellTypes';

export type NavLink = { id: string; label: string; path: string };

Expand Down Expand Up @@ -45,4 +45,4 @@ const getBaseNav = (t: TFunction): NavRow[] => [
},
];

export const navRowsForRole = (_role: DemoShellRole, t: TFunction): NavRow[] => getBaseNav(t);
export const navRowsForRole = (_role: UserRole, t: TFunction): NavRow[] => getBaseNav(t);
7 changes: 4 additions & 3 deletions apps/app-frontend/src/shell/shellRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { defaultRouteForRole } from './shellRoutes';

describe('defaultRouteForRole', () => {
it('lands every role on /catalog', () => {
expect(defaultRouteForRole('tenantUser')).toBe('/catalog');
expect(defaultRouteForRole('tenantAdmin')).toBe('/catalog');
expect(defaultRouteForRole('providerAdmin')).toBe('/catalog');
expect(defaultRouteForRole('tenant-user')).toBe('/catalog');
expect(defaultRouteForRole('tenant-admin')).toBe('/catalog');
expect(defaultRouteForRole('tenant-idp-manager')).toBe('/catalog');
expect(defaultRouteForRole('admin')).toBe('/catalog');
});
});
4 changes: 2 additions & 2 deletions apps/app-frontend/src/shell/shellRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import type { DemoShellRole } from '@osac/ui-components/shellTypes';
import type { UserRole } from '@osac/ui-components/shellTypes';

export const defaultRouteForRole = (_role: DemoShellRole): string => '/catalog';
export const defaultRouteForRole = (_role: UserRole): string => '/catalog';
4 changes: 4 additions & 0 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"CIDR overlaps with existing subnet \"{{name}}\" ({{cidr}})": "CIDR overlaps with existing subnet \"{{name}}\" ({{cidr}})",
"Close": "Close",
"Cloud Init User Data": "Cloud Init User Data",
"Cloud provider admin": "Cloud provider admin",
"Cluster conditions": "Cluster conditions",
"Cluster node sets": "Cluster node sets",
"Cluster password": "Cluster password",
Expand Down Expand Up @@ -167,6 +168,7 @@
"Host type": "Host type",
"Host type is required": "Host type is required",
"ICMP": "ICMP",
"IdP manager": "IdP manager",
"Inbound Rules": "Inbound Rules",
"Instance type": "Instance type",
"Internal IP": "Internal IP",
Expand Down Expand Up @@ -278,6 +280,8 @@
"Subnets": "Subnets",
"Take over": "Take over",
"TCP": "TCP",
"Tenant admin": "Tenant admin",
"Tenant user": "Tenant user",
"The console is available when the virtual machine is running.": "The console is available when the virtual machine is running.",
"This console is already open in another tab in this browser. Take over to continue here, or switch to that tab.": "This console is already open in another tab in this browser. Take over to continue here, or switch to that tab.",
"This field is required": "This field is required",
Expand Down
6 changes: 3 additions & 3 deletions libs/ui-components/src/hooks/use-session.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createContext, useContext } from 'react';

import type { DemoShellRole } from '../shellTypes';
import type { UserRole } from '../shellTypes';
import { type ResolvedTheme, type Theme, useTheme } from './use-theme';

interface SessionContextValue {
role: DemoShellRole;
role: UserRole;
username: string;
userTheme: Theme;
resolvedTheme: ResolvedTheme;
Expand All @@ -15,7 +15,7 @@ const SessionContext = createContext<SessionContextValue | null>(null);

interface SessionProviderProps {
children: React.ReactNode;
role: DemoShellRole;
role: UserRole;
username: string;
}

Expand Down
13 changes: 11 additions & 2 deletions libs/ui-components/src/shellTypes.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
/** Demo / OIDC shell roles mapped from Keycloak realm roles. */
export type DemoShellRole = 'providerAdmin' | 'tenantAdmin' | 'tenantUser';
import { TFunction } from 'i18next';

/** OIDC shell roles mapped from Keycloak realm roles and groups. */
export type UserRole = 'admin' | 'tenant-idp-manager' | 'tenant-admin' | 'tenant-user';

export const userRoleLabels = (t: TFunction): Record<UserRole, string> => ({
admin: t('Cloud provider admin'),
'tenant-idp-manager': t('IdP manager'),
'tenant-admin': t('Tenant admin'),
'tenant-user': t('Tenant user'),
});
Loading