Skip to content

Commit 1943894

Browse files
mnonnenmacheroheger-bosch
authored andcommitted
feat(ui): Switch to new authorization model
Use the previously introduced endpoint to get information about the user's superuser status and permissions. The superuser status is made available globally via the `useUser` hook while hierarchy related permissions are made available via the router context. As before, if the user has the superuser status, checking the specific permissions is skipped. Also, the results of all queries are cached for 60s to reduce the amount of backend calls. This means that it can take up to 60s for some permission changes to be reflected by the UI. Signed-off-by: Martin Nonnenmacher <[email protected]>
1 parent f4f0ad3 commit 1943894

File tree

26 files changed

+271
-172
lines changed

26 files changed

+271
-172
lines changed

ui/src/app.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import { Button } from '@/components/ui/button.tsx';
2828
import { Textarea } from '@/components/ui/textarea.tsx';
2929
import { config } from '@/config';
3030
import { authRef, useUser } from '@/hooks/use-user.ts';
31+
import {
32+
OrganizationPermissions,
33+
ProductPermissions,
34+
RepositoryPermissions,
35+
} from '@/lib/permissions.ts';
3136
import { queryClient } from '@/lib/query-client.ts';
3237
import { routeTree } from '@/routeTree.gen';
3338

@@ -45,6 +50,11 @@ export interface RouterContext {
4550
repo: string | undefined;
4651
run: string | undefined;
4752
};
53+
permissions: {
54+
organization: OrganizationPermissions | undefined;
55+
product: ProductPermissions | undefined;
56+
repository: RepositoryPermissions | undefined;
57+
};
4858
auth: ReturnType<typeof useUser>;
4959
}
5060

@@ -60,6 +70,11 @@ const router = createRouter({
6070
repo: undefined,
6171
run: undefined,
6272
},
73+
permissions: {
74+
organization: undefined,
75+
product: undefined,
76+
repository: undefined,
77+
},
6378
auth: undefined!,
6479
},
6580
defaultPreload: 'intent',

ui/src/components/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ export const Header = () => {
388388
<span>{user.username}</span>
389389
</div>
390390
</DropdownMenuItem>
391-
{user.hasRole(['superuser']) && (
391+
{user.isSuperuser && (
392392
<>
393393
<DropdownMenuSeparator />
394394
<Link to='/admin'>

ui/src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const oidcConfig = {
4848
redirect_uri: UI_URL,
4949
client_id: CLIENT_ID,
5050
automaticSilentRenew: true,
51-
loadUserInfo: true,
51+
loadUserInfo: false,
5252
onSigninCallback: () => {
5353
// This removes the Keycloak related query parameters from the URL after a successful login.
5454
const url = new URL(window.location.href);

ui/src/hooks/use-infrastructure-services.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {
2626
getRepositoryInfrastructureServicesOptions,
2727
} from '@/api/@tanstack/react-query.gen';
2828
import { ALL_ITEMS } from '@/lib/constants';
29+
import {
30+
OrganizationPermissions,
31+
ProductPermissions,
32+
RepositoryPermissions,
33+
} from '@/lib/permissions.ts';
2934

3035
export type InfrastructureServiceWithHierarchy = InfrastructureService & {
3136
hierarchy: 'organization' | 'product' | 'repository';
@@ -35,16 +40,18 @@ type UseInfrastructureServicesParams = {
3540
orgId?: string;
3641
productId?: string;
3742
repoId?: string;
38-
user: {
39-
hasRole: (roles: string[]) => boolean;
43+
permissions: {
44+
organization: OrganizationPermissions | undefined;
45+
product: ProductPermissions | undefined;
46+
repository: RepositoryPermissions | undefined;
4047
};
4148
};
4249

4350
export const useInfrastructureServices = ({
4451
orgId,
4552
productId,
4653
repoId,
47-
user,
54+
permissions,
4855
}: UseInfrastructureServicesParams) => {
4956
// Only fetch infrastructure services the user has access to.
5057
const infrastructureServices = useQueries({
@@ -58,10 +65,7 @@ export const useInfrastructureServices = ({
5865
limit: ALL_ITEMS,
5966
},
6067
}),
61-
enabled: user.hasRole([
62-
'superuser',
63-
`permission_organization_${orgId}_read`,
64-
]),
68+
enabled: permissions.organization?.includes('READ'),
6569
},
6670
{
6771
...getProductInfrastructureServicesOptions({
@@ -72,10 +76,7 @@ export const useInfrastructureServices = ({
7276
limit: ALL_ITEMS,
7377
},
7478
}),
75-
enabled: user.hasRole([
76-
'superuser',
77-
`permission_product_${productId}_read`,
78-
]),
79+
enabled: permissions.product?.includes('READ'),
7980
},
8081
{
8182
...getRepositoryInfrastructureServicesOptions({
@@ -86,10 +87,7 @@ export const useInfrastructureServices = ({
8687
limit: ALL_ITEMS,
8788
},
8889
}),
89-
enabled: user.hasRole([
90-
'superuser',
91-
`permission_repository_${repoId}_read`,
92-
]),
90+
enabled: permissions.repository?.includes('READ'),
9391
},
9492
],
9593
combine: (results) => {

ui/src/hooks/use-secrets.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {
2626
getRepositorySecretsOptions,
2727
} from '@/api/@tanstack/react-query.gen';
2828
import { ALL_ITEMS } from '@/lib/constants';
29+
import {
30+
OrganizationPermissions,
31+
ProductPermissions,
32+
RepositoryPermissions,
33+
} from '@/lib/permissions.ts';
2934

3035
export type SecretWithHierarchy = Secret & {
3136
hierarchy: 'organization' | 'product' | 'repository';
@@ -35,16 +40,18 @@ type UseSecretsParams = {
3540
orgId?: string;
3641
productId?: string;
3742
repositoryId?: string;
38-
user: {
39-
hasRole: (roles: string[]) => boolean;
43+
permissions: {
44+
organization: OrganizationPermissions | undefined;
45+
product: ProductPermissions | undefined;
46+
repository: RepositoryPermissions | undefined;
4047
};
4148
};
4249

4350
export function useSecrets({
4451
orgId,
4552
productId,
4653
repositoryId,
47-
user,
54+
permissions,
4855
}: UseSecretsParams) {
4956
const secrets = useQueries({
5057
queries: [
@@ -57,10 +64,7 @@ export function useSecrets({
5764
limit: ALL_ITEMS,
5865
},
5966
}),
60-
enabled: user.hasRole([
61-
'superuser',
62-
`permission_organization_${orgId}_read`,
63-
]),
67+
enabled: permissions.organization?.includes('READ'),
6468
},
6569
{
6670
...getProductSecretsOptions({
@@ -71,10 +75,7 @@ export function useSecrets({
7175
limit: ALL_ITEMS,
7276
},
7377
}),
74-
enabled: user.hasRole([
75-
'superuser',
76-
`permission_product_${productId}_read`,
77-
]),
78+
enabled: permissions.product?.includes('READ'),
7879
},
7980
{
8081
...getRepositorySecretsOptions({
@@ -85,10 +86,7 @@ export function useSecrets({
8586
limit: ALL_ITEMS,
8687
},
8788
}),
88-
enabled: user.hasRole([
89-
'superuser',
90-
`permission_repository_${repositoryId}_read`,
91-
]),
89+
enabled: permissions.repository?.includes('READ'),
9290
},
9391
],
9492
combine: (results) => {

ui/src/hooks/use-user.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20+
import { useQuery } from '@tanstack/react-query';
2021
import { useEffect } from 'react';
2122
import { AuthContextProps, useAuth } from 'react-oidc-context';
2223

24+
import { getSuperuserOptions } from '@/api/@tanstack/react-query.gen.ts';
2325
import { config } from '@/config';
2426

2527
export const authRef: { current: AuthContextProps | null } = {
@@ -62,6 +64,12 @@ export const useUser = () => {
6264
// Return end-user's full name, including all name parts.
6365
const fullName = auth?.user?.profile?.name;
6466

67+
// TODO: Handle errors for the superuser query.
68+
const { data: isSuperuser } = useQuery({
69+
...getSuperuserOptions(),
70+
staleTime: 60000,
71+
});
72+
6573
useEffect(() => {
6674
// Store the auth context in a ref for use outside of components.
6775
authRef.current = auth;
@@ -72,6 +80,7 @@ export const useUser = () => {
7280
username,
7381
fullName,
7482
refreshUser,
83+
isSuperuser,
7584
...auth,
7685
};
7786
};

ui/src/lib/permissions.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
import { QueryClient } from '@tanstack/react-query';
21+
22+
import {
23+
getSuperuserOptions,
24+
getUserInfoOptions,
25+
} from '@/api/@tanstack/react-query.gen.ts';
26+
27+
class BasePermissions {
28+
constructor(
29+
public readonly isSuperuser: boolean,
30+
public readonly permissions: string[]
31+
) {}
32+
33+
includes(requiredPermission: string): boolean {
34+
return this.isSuperuser || this.permissions.includes(requiredPermission);
35+
}
36+
}
37+
38+
export class OrganizationPermissions extends BasePermissions {
39+
constructor(
40+
public readonly organizationId: number,
41+
isSuperuser: boolean,
42+
permissions: string[]
43+
) {
44+
super(isSuperuser, permissions);
45+
}
46+
}
47+
48+
export class ProductPermissions extends BasePermissions {
49+
constructor(
50+
public readonly productId: number,
51+
isSuperuser: boolean,
52+
permissions: string[]
53+
) {
54+
super(isSuperuser, permissions);
55+
}
56+
}
57+
58+
export class RepositoryPermissions extends BasePermissions {
59+
constructor(
60+
public readonly repositoryId: number,
61+
isSuperuser: boolean,
62+
permissions: string[]
63+
) {
64+
super(isSuperuser, permissions);
65+
}
66+
}
67+
68+
type PermissionEntity = 'organizationId' | 'productId' | 'repositoryId';
69+
70+
async function fetchPermissions<T extends BasePermissions>(
71+
queryClient: QueryClient,
72+
entityId: number,
73+
entityType: PermissionEntity,
74+
PermissionsClass: new (
75+
entityId: number,
76+
isSuperuser: boolean,
77+
permissions: string[]
78+
) => T
79+
): Promise<T> {
80+
const isSuperuser = await queryClient.fetchQuery({
81+
...getSuperuserOptions(),
82+
staleTime: 60000,
83+
});
84+
85+
if (isSuperuser) {
86+
return new PermissionsClass(entityId, true, []);
87+
}
88+
89+
const userInfo = await queryClient.fetchQuery({
90+
...getUserInfoOptions({
91+
query: {
92+
[entityType]: entityId,
93+
},
94+
}),
95+
staleTime: 60000,
96+
});
97+
98+
return new PermissionsClass(entityId, false, userInfo.permissions);
99+
}
100+
101+
export const fetchOrganizationPermissions = (
102+
queryClient: QueryClient,
103+
organizationId: number
104+
) =>
105+
fetchPermissions(
106+
queryClient,
107+
organizationId,
108+
'organizationId',
109+
OrganizationPermissions
110+
);
111+
112+
export const fetchProductPermissions = (
113+
queryClient: QueryClient,
114+
productId: number
115+
) => fetchPermissions(queryClient, productId, 'productId', ProductPermissions);
116+
117+
export const fetchRepositoryPermissions = (
118+
queryClient: QueryClient,
119+
repositoryId: number
120+
) =>
121+
fetchPermissions(
122+
queryClient,
123+
repositoryId,
124+
'repositoryId',
125+
RepositoryPermissions
126+
);

ui/src/routes/admin/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const Layout = () => {
106106
export const Route = createFileRoute('/admin')({
107107
component: Layout,
108108
beforeLoad: ({ context }) => {
109-
if (!context.auth.hasRole(['superuser'])) {
109+
if (!context.auth.isSuperuser) {
110110
throw redirect({
111111
to: '/403',
112112
});

ui/src/routes/organizations/$orgId/infrastructure-services/create/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ import {
5555
} from '@/components/ui/select';
5656
import { capitalize } from '@/helpers/capitalize';
5757
import { useSecrets } from '@/hooks/use-secrets';
58-
import { useUser } from '@/hooks/use-user';
5958
import { ApiError } from '@/lib/api-error';
6059
import { toast } from '@/lib/toast';
6160

@@ -73,11 +72,11 @@ type FormSchema = z.infer<typeof formSchema>;
7372
const CreateInfrastructureServicePage = () => {
7473
const navigate = useNavigate();
7574
const params = Route.useParams();
76-
const user = useUser();
75+
const permissions = Route.useRouteContext().permissions;
7776

7877
const secrets = useSecrets({
7978
orgId: params.orgId,
80-
user,
79+
permissions,
8180
});
8281

8382
const { mutateAsync, isPending } = useMutation({

0 commit comments

Comments
 (0)