Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: implement admin permissions #429

Draft
wants to merge 3 commits into
base: 4.0
Choose a base branch
from
Draft
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
53 changes: 51 additions & 2 deletions helm/api-platform/keycloak/config/realm-demo.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,59 @@
],
"clientScopes": [
{
"id": "45ece602-0403-472f-bec5-0b1ed8d3c4ec",
"name": "roles",
"description": "OpenID Connect scope for add user roles to the access token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true"
}
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": "${rolesScopeConsentText}"
},
"protocolMappers": [
{
"id": "66670b4f-adc1-430b-bf4d-ac81cd03fb16",
"name": "client roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"id": "ac84d603-553b-487f-bc11-eb809a10df1a",
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"id": "a498765c-7294-481b-8143-318b06377834",
"name": "audience resolve",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-resolve-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"access.token.claim": "true"
}
}
]
}
],
"clients": [
Expand Down
15 changes: 14 additions & 1 deletion pwa/app/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { NEXT_PUBLIC_OIDC_CLIENT_ID, NEXT_PUBLIC_OIDC_SERVER_URL, NEXT_PUBLIC_OI
export interface Session extends DefaultSession {
error?: "RefreshAccessTokenError"
accessToken: string
permissions: string
idToken: string
user?: User
}

interface JWT {
accessToken: string
permissions: string
idToken: string
expiresAt: number
refreshToken: string
Expand All @@ -31,9 +33,19 @@ export const { handlers: { GET, POST }, auth } = NextAuth({
// @ts-ignore
async jwt({ token, account }: { token: JWT, account: Account }): Promise<JWT> {
if (account) {
// Retrieve user roles
const response = await fetch(`${NEXT_PUBLIC_OIDC_SERVER_URL_INTERNAL}/protocol/openid-connect/userinfo`, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${account.access_token}`,
},
});
const token = await response.json();

// Save the access token and refresh token in the JWT on the initial login
return {
...token,
permissions: token?.realm_access?.roles,
accessToken: account.access_token,
idToken: account.id_token,
expiresAt: Math.floor(Date.now() / 1000 + account.expires_in),
Expand Down Expand Up @@ -85,6 +97,7 @@ export const { handlers: { GET, POST }, auth } = NextAuth({
async session({ session, token }: { session: Session, token: JWT }): Promise<Session> {
// Save the access token in the Session for API calls
if (token) {
session.permissions = token.permissions;
session.accessToken = token.accessToken;
session.idToken = token.idToken;
session.error = token.error;
Expand Down Expand Up @@ -120,7 +133,7 @@ export const { handlers: { GET, POST }, auth } = NextAuth({
// https://authjs.dev/guides/basics/refresh-token-rotation#jwt-strategy
params: {
access_type: "offline",
scope: "openid profile email",
scope: "openid profile email roles",
prompt: "consent",
},
},
Expand Down
1 change: 1 addition & 0 deletions pwa/components/admin/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ENTRYPOINT } from "../../config/entrypoint";
import bookResourceProps from "./book";
import reviewResourceProps from "./review";
import i18nProvider from "./i18nProvider";
import Logout from "./layout/Logout";

const apiDocumentationParser = (session: Session) => async () => {
try {
Expand Down
17 changes: 13 additions & 4 deletions pwa/components/admin/authProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AuthProvider } from "react-admin";
import { getSession, signIn, signOut } from "next-auth/react";
import {AuthProvider} from "react-admin";
import {getSession, signIn, signOut} from "next-auth/react";

import { NEXT_PUBLIC_OIDC_SERVER_URL } from "../../config/keycloak";
import {NEXT_PUBLIC_OIDC_SERVER_URL} from "../../config/keycloak";

const authProvider: AuthProvider = {
// Nothing to do here, this function will never be called
Expand Down Expand Up @@ -42,7 +42,16 @@ const authProvider: AuthProvider = {

return Promise.resolve();
},
getPermissions: () => Promise.resolve(),
getPermissions: async () => {
const session = getSession();
vincentchalamon marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore
if (!session || !!session?.error) {
return Promise.reject();
}

// @ts-ignore
return Promise.resolve(session?.permissions);
},
getIdentity: async () => {
const session = getSession();

Expand Down
50 changes: 34 additions & 16 deletions pwa/components/admin/book/BooksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
List,
EditButton,
WrapperField,
usePermissions,
} from "react-admin";
import { Card, CardContent } from "@mui/material";

import { ShowButton } from "./ShowButton";
import { RatingField } from "../review/RatingField";
Expand All @@ -28,19 +30,35 @@ const filters = [
<ConditionInput source="condition" key="condition" />,
];

export const BooksList = () => (
<List filters={filters} exporter={false} title="Books">
<Datagrid>
<FieldGuesser source="title" />
<FieldGuesser source="author" sortable={false} />
<WrapperField label="Condition" sortable={false}>
<ConditionField />
</WrapperField>
<WrapperField label="Rating" sortable={false}>
<RatingField />
</WrapperField>
<ShowButton />
<EditButton />
</Datagrid>
</List>
);
export const BooksList = () => {
const { isPending, permissions } = usePermissions();

if (isPending) {
return <div>Waiting for permissions...</div>;
}

if (permissions !== 'admin') {
return (
<Card>
<CardContent>You are not allowed to access this page.</CardContent>
</Card>
);
}

return (
<List filters={filters} exporter={false} title="Books">
<Datagrid>
<FieldGuesser source="title"/>
<FieldGuesser source="author" sortable={false}/>
<WrapperField label="Condition" sortable={false}>
<ConditionField/>
</WrapperField>
<WrapperField label="Rating" sortable={false}>
<RatingField/>
</WrapperField>
<ShowButton/>
<EditButton/>
</Datagrid>
</List>
);
};
21 changes: 18 additions & 3 deletions pwa/tests/admin/User.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { expect, test } from "./test";

test.describe("User authentication", () => {
test.beforeEach(async ({ bookPage }) => {
test("I can sign out of Admin @login", async ({ bookPage, page }) => {
await bookPage.gotoList();
});

test("I can sign out of Admin @login", async ({ userPage, page }) => {
await page.getByLabel("Profile").click();
await page.getByRole("menu").getByText("Logout").waitFor({ state: "visible" });
await page.getByRole("menu").getByText("Logout").click();
Expand All @@ -21,4 +19,21 @@ test.describe("User authentication", () => {
await expect(page.locator("#kc-form-login")).toContainText("Login as user: [email protected]");
await expect(page.locator("#kc-form-login")).toContainText("Login as admin: [email protected]");
});

test("I see a permission denied error page if I don't have the right permissions @login", async ({ userPage, page }) => {
await userPage.goToAdminWithInvalidUser();

await expect(page.locator("#forbidden")).toContainText("You are not allowed to access this page.");
await page.getByLabel("Profile").click();
await page.getByRole("menu").getByText("Logout").waitFor({ state: "visible" });
await page.getByRole("menu").getByText("Logout").click();

await expect(page).toHaveURL(/\/$/);

// I should be logged out from Keycloak also
await page.goto("/admin");
await page.waitForURL(/\/oidc\/realms\/demo\/protocol\/openid-connect\/auth/);
// @ts-ignore assert declared on test.ts
await expect(page).toBeOnLoginPage();
});
});
20 changes: 20 additions & 0 deletions pwa/tests/admin/pages/UserPage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
import { AbstractPage } from "./AbstractPage";

export class UserPage extends AbstractPage {
public async goToAdminWithInvalidUser() {
await this.registerMock();

await this.page.goto("/admin");
await this.loginWithPublicUser();
await this.page.waitForURL(/\/admin#\/admin/);

return this.page;
}

public async loginWithPublicUser() {
await this.page.getByLabel("Email").fill("[email protected]");
await this.page.getByLabel("Password").fill("Pa55w0rd");
await this.page.getByRole("button", { name: "Sign In" }).click();
if (await this.page.getByRole("button", { name: "Sign in with Keycloak" }).count()) {
await this.page.getByRole("button", { name: "Sign in with Keycloak" }).click();
}

return this.page;
}
}
Loading