-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add docs for user permissions (#2128)
Co-authored-by: Johannes Obermair <[email protected]> Co-authored-by: Thomas Dax <[email protected]>
- Loading branch information
1 parent
649e7a5
commit 1b7eaf8
Showing
10 changed files
with
373 additions
and
69 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
--- | ||
title: Evaluate Content Scopes | ||
sidebar_position: 3 | ||
--- | ||
|
||
To evaluate the scope there a two technically very distinctive ways depending on the type of operation: | ||
|
||
### Operations that create entities or query lists | ||
|
||
If an operation does not handle existing entities, the scope has to be passed as an argument. COMET DXP expects the argument to be named `scope` in order to be able to validate it. So do not forget to provide the `scope` argument in your operation. | ||
|
||
### Operations that handle specific entities | ||
|
||
**@AffectedEntity** | ||
|
||
COMET DXP needs information on which entities are being handled in the operation (= which entities are affected). Therefore, every operation of this kind needs to be marked with this decorator. | ||
|
||
Use this decorator at the **operation level** to specify which entity (and thus scope) is affected by the operation. | ||
|
||
:::info | ||
By default COMET DXP tries to load the affected entity by id with the value of the submitted id-argument. However, the name of the argument can be altered by using the `idArg` setting. | ||
::: | ||
|
||
```ts | ||
@Query(Product) | ||
@AffectedEntity(Product) | ||
async product(@Args("id", { type: () => ID }) id: string): Promise<Product> { | ||
//... | ||
} | ||
``` | ||
|
||
```ts | ||
@Query([Product]) | ||
@AffectedEntity(Dealer, { idArg: "dealerId" }) | ||
async products(@Args("dealerId", { type: () => ID }) dealerId: string): Promise<Product[]> { | ||
// Note: you can trust "dealerId" being in a valid scope, but you need to make sure that your business code restricts this query to the given dealer | ||
} | ||
``` | ||
|
||
It's possible to add multiple `@AffectedEntity` decorators to one operation if multiple entities are affected: | ||
|
||
```ts | ||
@Query(Product) | ||
@AffectedEntity(Product) | ||
@AffectedEntity(Dealer, { idArg: "dealerId" }) | ||
async product(@Args("id", { type: () => ID }) id: string, @Args("dealerId", { type: () => ID }) dealerId: string): Promise<Product> { | ||
//... | ||
} | ||
``` | ||
|
||
**@ScopedEntity** | ||
|
||
Retrieving the affected entity alone is not sufficient, COMET DXP also needs to know the scope of the entity. The simplest case is when the entity has a field named `scope`. If this is true, this decorator is not necessary. | ||
|
||
If the scope is stored in a different field or the entity has a relation to another entity that stores the scope, additional information is required. This is where this decorator comes into play. | ||
|
||
Use this decorator at the **entity level** to return the scope of an entity. | ||
|
||
```ts | ||
@ScopedEntity(async (product: Product) => { | ||
return { | ||
dealer: product.dealer.id, | ||
}; | ||
}) | ||
@Entity() | ||
export class Product extends BaseEntity<Product, "id"> {} | ||
``` | ||
|
||
:::info | ||
You might have to load multiple relations for nested data. | ||
::: | ||
|
||
It's also possible to pass a function which returns the content scope to the `@ScopedEntity` decorator. This is necessary for documents that get their scope from a `PageTreeNode`. Here you can pass the helper service `PageTreeNodeDocumentEntityScopeService` provided by the library: | ||
|
||
```ts | ||
@ScopedEntity(PageTreeNodeDocumentEntityScopeService) | ||
export class PredefinedPage extends BaseEntity<PredefinedPage, "id"> implements DocumentInterface { | ||
``` |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
title: Logging | ||
sidebar_position: 8 | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
--- | ||
title: Access Control in the API | ||
sidebar_position: 2 | ||
--- | ||
|
||
:::note | ||
|
||
The term **operation** stands for the locations in which COMET DXP invokes permission checks: | ||
|
||
- Queries/Mutations in GraphQL-resolvers | ||
- Routes in REST-controllers | ||
|
||
Normally you want to decorate the methods of these classes, however, decorating the whole class is also possible. | ||
::: | ||
|
||
After activating the module, COMET DXP checks every operation for the required permissions and scopes. Therefore it is necessary to decorate the operations to let the system know what to check. COMET DXP then checks if the current user possesses the permission defined in the decorator. | ||
|
||
Additionally, the scope of the data in operation will be checked against the scope of the users. To achieve this, the system has to know the scope of the data that is being handled right now. | ||
|
||
:::note | ||
You might also want to check the permissions on field resolvers. To do that, you have to add `guards` to `fieldResolverEnhancers` in the configuration of the GraphQL-module. Please be aware that field resolvers are only checked for permissions but not for scopes. | ||
::: | ||
|
||
## Permission check | ||
|
||
**@RequiredPermission** | ||
|
||
This decorator is mandatory for all operations. The first parameter of type `string | string[] | "disablePermissionCheck"` configures which permission is necessary to access the decorated operation. | ||
|
||
The core of COMET DXP already defines a list of permissions (e.g. `pageTree`, `dam`, `cronJobs`, `userPermissions`). Permissions are defined as plain strings, in the most basic case they represent the main items of the menu bar in the admin panel. | ||
|
||
However, if you need a more fine-grained access control you might want to concatenate strings, e.g. `newsRead` or `newsCreate`. Only create as many permissions as really necessary. | ||
|
||
:::info | ||
Future version will support a dot-like notation (e.g. `news` will subsume `news.read` and `news.write`). | ||
::: | ||
|
||
## Scope check | ||
|
||
The scope check needs to know which scope is used for the current operation. This is described in [Evaluate Content Scopes documentation](/docs/content-scope/evaluate-content-scopes). | ||
|
||
:::caution | ||
COMET DXP validates the data relevant for the operation, but cannot check if the validated data is finally used. You are responsible for applying the validated data in your operations. | ||
::: | ||
|
||
## Disable permission/scope checks | ||
|
||
**skipScopeCheck** | ||
|
||
The scope check can be disabled by adding `{skipScopeCheck: true}` as the second argument of the `@RequiredPermission` decorator. | ||
|
||
:::caution | ||
Use this option only when you are sure that checking the scope is not necessary (e.g. the current entity does not have a scope). Do not add it just because it seems cumbersome at the moment to add the correct `AffectedEntity`/`ScopedEntity` decorators. | ||
::: | ||
|
||
:::note | ||
Also, try to avoid using the `@GetCurrentUser` decorator (which often leads to use `skipScopeCheck`). Instead, you should explicitly send all the data needed in an operation. In the following example, this requires adding `userId` as a scope part as well as passing the data throughout the client. In general, this leads to a cleaner API design. | ||
|
||
```diff | ||
- @RequiredPermission("products", {skipScopeCheck: true}) | ||
+ @RequiredPermission("products") | ||
+ @AffectedEntity(User, { idArg: "userId" }) | ||
- async myProducts(@GetCurrentUser() currentUser: CurrentUser): Promise<Product[]> { | ||
+ async productsForUser(@Args("userId", { type: () => ID }) userId: string): Promise<Product[]> { | ||
//... | ||
} | ||
``` | ||
|
||
::: | ||
|
||
### @DisableCometGuards | ||
|
||
`@DisableCometGuards()` disables the global auth guards (`CometAuthGuard`, `UserPermissionsGuard`). This may be used if a different authentication method is desired (e.g., basic authentication) for a specific handler or class in combination with a custom guard. | ||
|
||
e.g.: | ||
|
||
```typescript | ||
@DisableCometGuards() | ||
@UseGuards(MyCustomGuard) | ||
async handlerThatUsesACustomGuard(): { | ||
... | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
--- | ||
title: Permissions in Admin | ||
sidebar_position: 3 | ||
--- | ||
|
||
:::caution | ||
Please be aware that there is no access control possible in the admin panel. You have to check every permission and scope in the API. The following functions only assist for showing the correct user interface. | ||
::: | ||
|
||
### CurrentUserProvider | ||
|
||
The `CurrentUserProvider` loads the current user from the API and provides the following hooks: | ||
|
||
**useCurrentUser** | ||
|
||
You can use this hook to access the current user. | ||
|
||
:::note | ||
The current user object provides `allowedContentScopes` which may be used for the content scope selector. | ||
::: | ||
|
||
:::info | ||
Try not to use the permissions field of the current user object directly as this is subject to change in future versions. | ||
::: | ||
|
||
**useUserPermissionCheck** | ||
|
||
This hook provides a function that behaves like the `isAllowed` function in the API. | ||
|
||
```ts | ||
const isAllowed = useUserPermissionCheck(); | ||
if (isAllowed("pageTree")) { | ||
// ... | ||
} | ||
``` | ||
|
||
:::info | ||
Since this function also checks the content scope, it requires the `ContentScopeProvider` in the rendering tree. | ||
::: | ||
|
||
### MasterMenuData | ||
|
||
The `MasterMenuData` data type provides a unified format for | ||
|
||
- the `menu` prop in `MasterMenu` | ||
- the `menu` prop in `MasterMenuRoutes` | ||
|
||
Regarding user permissions, `MasterMenuData` also provides a `requiredPermission` field. Both of the mentioned components use this field to filter the data. | ||
|
||
```ts | ||
// src/common/MasterMenu.tsx | ||
export const masterMenuData: MasterMenuData = [ | ||
{ | ||
type: "route", | ||
primary: "Demo", | ||
icon: <Snips />, | ||
route: ..., | ||
requiredPermission: "demo", | ||
}, | ||
{ | ||
type: "collapsible", | ||
primary: "Project Snips", | ||
icon: <Snips />, | ||
items: [ | ||
{ | ||
primary: "Main Menu", | ||
route: { | ||
path: "/project-snips/main-menu", | ||
component: MainMenu, | ||
}, | ||
}, | ||
], | ||
requiredPermission: "pageTree", | ||
}, | ||
// ... | ||
]; | ||
|
||
export const AppMasterMenu = () => <MasterMenu menu={masterMenuData} />; | ||
|
||
// src/App.tsx | ||
<Route | ||
render={() => ( | ||
<MasterLayout | ||
headerComponent={MasterHeader} | ||
menuComponent={AppMasterMenu} | ||
> | ||
<MasterMenuRoutes menu={masterMenuData} /> | ||
</MasterLayout> | ||
)} | ||
/> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
--- | ||
title: User Permissions | ||
sidebar_position: 6 | ||
--- | ||
|
||
While COMET DXP does not provide authentication, it handles authorization by providing a User Permissions system. | ||
|
||
## Key concepts | ||
|
||
The user permissions system | ||
|
||
- streamlines authorization throughout the application | ||
- not only checks operations but also the handled data | ||
- relies on external user handling | ||
- allows assigning permissions by code as well as in the admin panel | ||
- offers an admin panel that works out of the box | ||
|
||
COMET DXP checks authentication in two dimensions: | ||
|
||
**Permissions** are used for access control to specific resolvers or controllers. | ||
|
||
**Content Scopes** (also referred to as scopes) are used to control which data is allowed to be handled (see [documentation about content scopes](/docs/content-scope)). | ||
|
||
Users in COMET DXP possess permissions and scopes. Every operation is assigned to one or more permissions and handles data that is bound to a scope. The system then tries to match if the requested permissions and scopes are covered by the user's permissions and scopes. | ||
|
||
There are no roles as they can easily be represented as a combination of permissions. Furthermore, the ability to check scopes is more powerful than just being assigned a single role. | ||
|
||
## Important types | ||
|
||
- `User` is provided by COMET DXP as an interface so that it's possible to enhance the type by TypeScript module augmentation. By default, a ` User` object contains the fields `id`, `name` and `email`. | ||
- `CurrentUser` is used as a GraphQL-type and is returned by `@GetCurrentUser`. It's not customizable and enhances the default `User` type with the current permissions and scopes. | ||
- `ContentScope` is provided as an interface and should be augmented in the application. | ||
- There is no custom type for permissions, they are reflected as plain strings. |
Oops, something went wrong.