Skip to content

Commit

Permalink
feat: rbac middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuaellis committed Apr 8, 2024
1 parent df42d2a commit 9c3b21b
Show file tree
Hide file tree
Showing 41 changed files with 634 additions and 778 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -124,61 +124,3 @@ type UseRBAC = (
};
};
```

---

## useSyncRBAC

:::note
This hook is only available in the content-manager.
:::

This hook because it's only used in the content-manager, has specific redux dependencies on the injected reducers for the content-manager.
It sets / resets and provides the permissions based on the specific view of the content-manager you're in e.g. the listView (known as the explorer)
in conjunction with the content-type you're viewing e.g. `api::post.post`.

### Usage

In the below example, we use the hook to only get the permissions for the `api::post.post` content-type.

:::caution
While it is required to pass three arguments, only the middle argument is currently used.
:::

```tsx
import { useQueryParams } from '@strapi/strapi/admin';
import { useSyncRBAC } from 'path/to/conent-manager/hooks';

const MyComponent = () => {
const [{ query }] = useQueryParams();

const permissions = useSyncRBAC(query, 'api::post.post', 'explorer');

return !permissions ? (
<p>Loading...</p>
) : (
<Page.Protect permissions={permissions}>
<h1>aha you found me</h1>
</Page.Protect>
);
};
```

### Typescript

```ts
interface Permission {
id: number;
action: string;
subject: string | null;
// This can be custom defined to the needs of the plugin/application
properties: Record<string, any>;
conditions: Array<string>;
}

type UseSyncRBAC = (
query: Record<string, string> | undefined,
contentTypeUID: string,
viewId?: string
) => Permission[];
```
57 changes: 0 additions & 57 deletions docs/docs/docs/01-core/admin/04-features/authentication.md

This file was deleted.

1 change: 0 additions & 1 deletion packages/admin-test-utils/src/fixtures/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit';

const reducers = {
admin_app: jest.fn(() => ({ permissions: {}, status: 'init' })),
'content-manager_rbacManager': jest.fn(() => ({ permissions: null })),
};

const store = configureStore({
Expand Down
10 changes: 10 additions & 0 deletions packages/core/admin/admin/src/StrapiApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Theme } from './components/Theme';
import { ADMIN_PERMISSIONS_CE, HOOKS } from './constants';
import { CustomFields } from './core/apis/CustomFields';
import { Plugin, PluginConfig } from './core/apis/Plugin';
import { RBAC, RBACMiddleware } from './core/apis/rbac';
import { RootState, Store, configureStore } from './core/store/configure';
import { getBasename } from './core/utils/basename';
import { Handler, createHook } from './core/utils/createHook';
Expand Down Expand Up @@ -156,6 +157,7 @@ class StrapiApp {
/**
* APIs
*/
rbac = new RBAC();
library: Library = {
components: {},
fields: {},
Expand Down Expand Up @@ -272,6 +274,14 @@ class StrapiApp {
});
};

addRBACMiddleware = (m: RBACMiddleware | RBACMiddleware[]) => {
if (Array.isArray(m)) {
this.rbac.use(m);
} else {
this.rbac.use(m);
}
};

addReducers = (reducers: ReducersMapObject) => {
/**
* TODO: when we upgrade to redux-toolkit@2 and we can also dynamically add middleware,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/admin/admin/src/components/PageHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ const Loading = ({ children = 'Loading content.' }: LoadingProps) => {
* -----------------------------------------------------------------------------------------------*/
interface ErrorProps extends Partial<EmptyStateLayoutProps> {}

/**
* TODO: should we start passing our errors here so they're persisted on the screen?
* This could follow something similar to how the global app error works...?
*/

/**
* @public
* @description An error component that should be rendered as the page
Expand Down
59 changes: 29 additions & 30 deletions packages/core/admin/admin/src/components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,27 @@ interface ProvidersProps {

const Providers = ({ children, strapi, store }: ProvidersProps) => {
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<HistoryProvider>
<LanguageProvider messages={strapi.configurations.translations}>
<Theme themes={strapi.configurations.themes}>
<NotificationsProvider>
<StrapiAppProvider
components={strapi.library.components}
customFields={strapi.customFields}
fields={strapi.library.fields}
menu={strapi.menu}
getAdminInjectedComponents={strapi.getAdminInjectedComponents}
getPlugin={strapi.getPlugin}
plugins={strapi.plugins}
runHookParallel={strapi.runHookParallel}
runHookWaterfall={(name, initialValue) => {
return strapi.runHookWaterfall(name, initialValue, store);
}}
runHookSeries={strapi.runHookSeries}
settings={strapi.settings}
>
<StrapiAppProvider
components={strapi.library.components}
customFields={strapi.customFields}
fields={strapi.library.fields}
menu={strapi.menu}
getAdminInjectedComponents={strapi.getAdminInjectedComponents}
getPlugin={strapi.getPlugin}
plugins={strapi.plugins}
rbac={strapi.rbac}
runHookParallel={strapi.runHookParallel}
runHookWaterfall={(name, initialValue) => strapi.runHookWaterfall(name, initialValue, store)}
runHookSeries={strapi.runHookSeries}
settings={strapi.settings}
>
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<HistoryProvider>
<LanguageProvider messages={strapi.configurations.translations}>
<Theme themes={strapi.configurations.themes}>
<NotificationsProvider>
<TrackingProvider>
<GuidedTourProvider>
<ConfigurationProvider
Expand All @@ -67,14 +66,14 @@ const Providers = ({ children, strapi, store }: ProvidersProps) => {
</ConfigurationProvider>
</GuidedTourProvider>
</TrackingProvider>
</StrapiAppProvider>
</NotificationsProvider>
</Theme>
</LanguageProvider>
</HistoryProvider>
</AuthProvider>
</QueryClientProvider>
</Provider>
</NotificationsProvider>
</Theme>
</LanguageProvider>
</HistoryProvider>
</AuthProvider>
</QueryClientProvider>
</Provider>
</StrapiAppProvider>
);
};

Expand Down
58 changes: 58 additions & 0 deletions packages/core/admin/admin/src/core/apis/rbac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Location } from 'react-router-dom';

import type { Permission, User } from '../../features/Auth';

interface RBACContext extends Pick<Location, 'pathname' | 'search'> {
/**
* The current user.
*/
user?: User;
/**
* The permissions of the current user.
*/
permissions: Permission[];
}

interface RBACMiddleware {
(
ctx: RBACContext
): (
next: (permissions: Permission[]) => Promise<Permission[]> | Permission[],
permissions: Permission[]
) => Promise<Permission[]> | Permission[];
}

class RBAC {
private middlewares: RBACMiddleware[] = [];

constructor() {}

use(middleware: RBACMiddleware[]): void;
use(middleware: RBACMiddleware): void;
use(middleware: RBACMiddleware | RBACMiddleware[]): void {
if (Array.isArray(middleware)) {
this.middlewares.push(...middleware);
} else {
this.middlewares.push(middleware);
}
}

run = async (ctx: RBACContext, permissions: Permission[]): Promise<Permission[]> => {
let index = 0;

const middlewaresToRun = this.middlewares.map((middleware) => middleware(ctx));

const next = async (permissions: Permission[]) => {
if (index < this.middlewares.length) {
return middlewaresToRun[index++](next, permissions);
}

return permissions;
};

return next(permissions);
};
}

export { RBAC };
export type { RBACMiddleware, RBACContext };

0 comments on commit 9c3b21b

Please sign in to comment.