mirror of
https://github.com/strapi/strapi.git
synced 2025-08-15 12:18:38 +00:00
Add the permission package
This commit is contained in:
parent
6571302986
commit
ad154e74e3
16
packages/core/permissions/.editorconfig
Normal file
16
packages/core/permissions/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[{package.json,*.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
100
packages/core/permissions/.npmignore
Normal file
100
packages/core/permissions/.npmignore
Normal file
@ -0,0 +1,100 @@
|
||||
############################
|
||||
# OS X
|
||||
############################
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
._*
|
||||
|
||||
############################
|
||||
# Linux
|
||||
############################
|
||||
|
||||
*~
|
||||
|
||||
############################
|
||||
# Windows
|
||||
############################
|
||||
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
############################
|
||||
# Packages
|
||||
############################
|
||||
|
||||
*.7z
|
||||
*.csv
|
||||
*.dat
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.seed
|
||||
*.so
|
||||
*.swo
|
||||
*.swp
|
||||
*.swn
|
||||
*.swm
|
||||
*.out
|
||||
*.pid
|
||||
|
||||
############################
|
||||
# Logs and databases
|
||||
############################
|
||||
|
||||
.tmp
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
|
||||
############################
|
||||
# Misc.
|
||||
############################
|
||||
|
||||
*#
|
||||
.editorconfig
|
||||
.idea
|
||||
nbproject
|
||||
|
||||
############################
|
||||
# Node.js
|
||||
############################
|
||||
|
||||
lib-cov
|
||||
lcov.info
|
||||
pids
|
||||
logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
.node_history
|
||||
|
||||
############################
|
||||
# Tests
|
||||
############################
|
||||
|
||||
test
|
||||
tests
|
||||
__tests__
|
||||
jest.config.js
|
||||
testApp
|
||||
coverage
|
22
packages/core/permissions/LICENSE
Normal file
22
packages/core/permissions/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2015-present Strapi Solutions SAS
|
||||
|
||||
Portions of the Strapi software are licensed as follows:
|
||||
|
||||
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
|
||||
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
|
||||
|
||||
MIT Expat License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
143
packages/core/permissions/README.md
Normal file
143
packages/core/permissions/README.md
Normal file
@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://strapi.io">
|
||||
<img src="https://strapi.io/assets/strapi-logo-dark.svg" width="318px" alt="Strapi logo" />
|
||||
</a>
|
||||
</p>
|
||||
<h3 align="center">API creation made simple, secure and fast.</h3>
|
||||
<p align="center">The most advanced open-source headless CMS to build powerful APIs with no effort.</p>
|
||||
<p align="center"><a href="https://strapi.io/demo">Try live demo</a></p>
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.org/package/@strapi/strapi">
|
||||
<img src="https://img.shields.io/npm/v/@strapi/strapi/latest.svg" alt="NPM Version" />
|
||||
</a>
|
||||
<a href="https://github.com/strapi/strapi/actions/workflows/tests.yml">
|
||||
<img src="https://github.com/strapi/strapi/actions/workflows/tests.yml/badge.svg?branch=master" alt="Tests" />
|
||||
</a>
|
||||
<a href="https://discord.strapi.io">
|
||||
<img src="https://img.shields.io/discord/811989166782021633?label=Discord" alt="Strapi on Discord" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://strapi.io">
|
||||
<img src="https://raw.githubusercontent.com/strapi/strapi/0bcebf77b37182fe021cb59cc19be8f5db4a18ac/public/assets/administration_panel.png" alt="Administration panel" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
Strapi is a free and open-source headless CMS delivering your content anywhere you need.
|
||||
|
||||
- **Keep control over your data**. With Strapi, you know where your data is stored, and you keep full control at all times.
|
||||
- **Self-hosted**. You can host and scale Strapi projects the way you want. You can choose any hosting platform you want: AWS, Render, Netlify, Heroku, a VPS, or a dedicated server. You can scale as you grow, 100% independent.
|
||||
- **Database agnostic**. Strapi works with SQL databases. You can choose the database you prefer: PostgreSQL, MySQL, MariaDB, and SQLite.
|
||||
- **Customizable**. You can quickly build your logic by fully customizing APIs, routes, or plugins to fit your needs perfectly.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<a href="https://docs.strapi.io/developer-docs/latest/getting-started/quick-start.html" target="_blank">Read the Getting Started tutorial</a> or follow the steps below:
|
||||
|
||||
### ⏳ Installation
|
||||
|
||||
Install Strapi with this **Quickstart** command to create a Strapi project instantly:
|
||||
|
||||
- (Use **yarn** to install the Strapi project (recommended). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).)
|
||||
|
||||
```bash
|
||||
yarn create strapi-app my-project --quickstart
|
||||
```
|
||||
|
||||
**or**
|
||||
|
||||
- (Use npm/npx to install the Strapi project.)
|
||||
|
||||
```bash
|
||||
npx create-strapi-app my-project --quickstart
|
||||
```
|
||||
|
||||
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload). The **Quickstart** command installs Strapi using a **SQLite** database which is used for prototyping in development.
|
||||
|
||||
Enjoy 🎉
|
||||
|
||||
### 🖐 Requirements
|
||||
|
||||
Complete installation requirements can be found in the documentation under <a href="https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html">Installation Requirements</a>.
|
||||
|
||||
**Supported operating systems**:
|
||||
|
||||
- Ubuntu LTS/Debian 9.x
|
||||
- CentOS/RHEL 8
|
||||
- macOS Mojave
|
||||
- Windows 10
|
||||
- Docker - [Docker-Repo](https://github.com/strapi/strapi-docker)
|
||||
|
||||
(Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)
|
||||
|
||||
**Node:**
|
||||
|
||||
- NodeJS >= 12 <= 16
|
||||
- NPM >= 6.x
|
||||
|
||||
**Database:**
|
||||
|
||||
- MySQL >= 5.7.8
|
||||
- MariaDB >= 10.2.7
|
||||
- PostgreSQL >= 10
|
||||
- SQLite >= 3
|
||||
|
||||
**We recommend always using the latest version of Strapi to start your new projects**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Modern Admin Panel:** Elegant, entirely customizable and a fully extensible admin panel.
|
||||
- **Secure by default:** Reusable policies, CORS, CSP, P3P, Xframe, XSS, and more.
|
||||
- **Plugins Oriented:** Install the auth system, content management, custom plugins, and more, in seconds.
|
||||
- **Blazing Fast:** Built on top of Node.js, Strapi delivers amazing performance.
|
||||
- **Front-end Agnostic:** Use any front-end framework (React, Vue, Angular, etc.), mobile apps or even IoT.
|
||||
- **Powerful CLI:** Scaffold projects and APIs on the fly.
|
||||
- **SQL databases:** Works with PostgreSQL, MySQL, MariaDB, and SQLite.
|
||||
|
||||
**[See more on our website](https://strapi.io/overview)**.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a Pull Request to the project.
|
||||
|
||||
## Community support
|
||||
|
||||
For general help using Strapi, please refer to [the official Strapi documentation](https://docs.strapi.io). For additional help, you can use one of these channels to ask a question:
|
||||
|
||||
- [Discord](https://discord.strapi.io) (For live discussion with the Community and Strapi team)
|
||||
- [GitHub](https://github.com/strapi/strapi) (Bug reports, Contributions)
|
||||
- [Community Forum](https://forum.strapi.io) (Questions and Discussions)
|
||||
- [Roadmap & Feature Requests](https://feedback.strapi.io/)
|
||||
- [Twitter](https://twitter.com/strapijs) (Get the news fast)
|
||||
- [Facebook](https://www.facebook.com/Strapi-616063331867161)
|
||||
- [YouTube Channel](https://www.youtube.com/strapi) (Learn from Video Tutorials)
|
||||
|
||||
## Migration
|
||||
|
||||
Follow our [migration guides](https://docs.strapi.io/developer-docs/latest/update-migration-guides/migration-guides.html) on the documentation to keep your projects up-to-date.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Check out our [roadmap](https://feedback.strapi.io/) to get informed of the latest features released and the upcoming ones. You may also give us insights and vote for a specific feature.
|
||||
|
||||
## Documentation
|
||||
|
||||
See our dedicated [repository](https://github.com/strapi/documentation) for the Strapi documentation, or view our documentation live:
|
||||
|
||||
- [Developer docs](https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html)
|
||||
- [User guide](https://docs.strapi.io/user-docs/latest/getting-started/introduction.html)
|
||||
|
||||
## Try live demo
|
||||
|
||||
See for yourself what's under the hood by getting access to a [hosted Strapi project](https://strapi.io/demo) with sample data.
|
||||
|
||||
## License
|
||||
|
||||
See the [LICENSE](./LICENSE) file for licensing information.
|
48
packages/core/permissions/index.d.ts
vendored
Normal file
48
packages/core/permissions/index.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
import { Ability } from '@casl/ability';
|
||||
import { hooks, providerFactory } from '@strapi/utils';
|
||||
|
||||
type Provider = ReturnType<typeof providerFactory>;
|
||||
|
||||
interface Permission {
|
||||
action: string;
|
||||
subject?: string | object;
|
||||
properties?: object;
|
||||
conditions?: string[];
|
||||
}
|
||||
|
||||
interface Action {
|
||||
name: string;
|
||||
section: string;
|
||||
}
|
||||
|
||||
interface Condition {
|
||||
name: string;
|
||||
|
||||
handler(): boolean | object;
|
||||
}
|
||||
|
||||
type StrapiHook<
|
||||
T extends keyof Pick<
|
||||
typeof hooks,
|
||||
'createAsyncParallelHook' | 'createAsyncSeriesHook' | 'createAsyncSeriesWaterfallHook'
|
||||
>
|
||||
> = ReturnType<typeof hooks[T]>;
|
||||
|
||||
interface EngineHooks {
|
||||
willEvaluatePermission: StrapiHook<'createAsyncSeriesHook'>;
|
||||
willRegisterPermission: StrapiHook<'createAsyncSeriesHook'>;
|
||||
}
|
||||
|
||||
interface ActionProvider<T extends Action = Action> extends Provider {}
|
||||
interface ConditionProvider<T extends Condition = Condition> extends Provider {}
|
||||
|
||||
interface PermissionEngine {
|
||||
hooks: EngineHooks;
|
||||
|
||||
generateAbility(permissions: Permission[], options?: object): Ability;
|
||||
}
|
||||
|
||||
interface PermissionEngineParams {
|
||||
providers: { action: ActionProvider; condition: ConditionProvider };
|
||||
abilityBuilderFactory(): { can: Function; build: Function };
|
||||
}
|
10
packages/core/permissions/jest.config.js
Normal file
10
packages/core/permissions/jest.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
const baseConfig = require('../../../jest.base-config');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
displayName: (pkg.strapi && pkg.strapi.name) || pkg.name,
|
||||
roots: [__dirname],
|
||||
};
|
7
packages/core/permissions/lib/domain/index.js
Normal file
7
packages/core/permissions/lib/domain/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const permission = require('./permission');
|
||||
|
||||
module.exports = {
|
||||
permission,
|
||||
};
|
68
packages/core/permissions/lib/domain/permission/index.js
Normal file
68
packages/core/permissions/lib/domain/permission/index.js
Normal file
@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash/fp');
|
||||
|
||||
const PERMISSION_FIELDS = ['action', 'subject', 'properties', 'conditions'];
|
||||
|
||||
const sanitizePermissionFields = _.pick(PERMISSION_FIELDS);
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../index').Permission} Permission
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a permission with default values for optional properties
|
||||
*
|
||||
* @return {Pick<Permission, 'conditions' | 'properties' | 'subject'>}
|
||||
*/
|
||||
const getDefaultPermission = () => ({
|
||||
conditions: [],
|
||||
properties: {},
|
||||
subject: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new permission based on given attributes
|
||||
*
|
||||
* @param {object} attributes
|
||||
*
|
||||
* @return {Permission}
|
||||
*/
|
||||
const create = _.pipe(_.pick(PERMISSION_FIELDS), _.merge(getDefaultPermission()));
|
||||
|
||||
/**
|
||||
* Add a condition to a permission
|
||||
*
|
||||
* @param {string} condition The condition to add
|
||||
* @param {Permission} permission The permission on which we want to add the condition
|
||||
*
|
||||
* @return {Permission}
|
||||
*/
|
||||
const addCondition = _.curry((condition, permission) => {
|
||||
const { conditions } = permission;
|
||||
|
||||
const newConditions = Array.isArray(conditions)
|
||||
? _.uniq(conditions.concat(condition))
|
||||
: [condition];
|
||||
|
||||
return _.set('conditions', newConditions, permission);
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets a property or a part of a property from a permission.
|
||||
*
|
||||
* @function
|
||||
*
|
||||
* @param {string} property - The property to get
|
||||
* @param {Permission} permission - The permission on which we want to access the property
|
||||
*
|
||||
* @return {Permission}
|
||||
*/
|
||||
const getProperty = _.curry((property, permission) => _.get(`properties.${property}`, permission));
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
sanitizePermissionFields,
|
||||
addCondition,
|
||||
getProperty,
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const sift = require('sift');
|
||||
const { AbilityBuilder, Ability } = require('@casl/ability');
|
||||
const { pick, isNil, isObject } = require('lodash/fp');
|
||||
|
||||
const allowedOperations = [
|
||||
'$or',
|
||||
'$and',
|
||||
'$eq',
|
||||
'$ne',
|
||||
'$in',
|
||||
'$nin',
|
||||
'$lt',
|
||||
'$lte',
|
||||
'$gt',
|
||||
'$gte',
|
||||
'$exists',
|
||||
'$elemMatch',
|
||||
];
|
||||
|
||||
const operations = pick(allowedOperations, sift);
|
||||
|
||||
const conditionsMatcher = conditions => {
|
||||
return sift.createQueryTester(conditions, { operations });
|
||||
};
|
||||
|
||||
/**
|
||||
* Casl Ability Builder.
|
||||
*/
|
||||
const caslAbilityBuilder = () => {
|
||||
const { can, build, ...rest } = new AbilityBuilder(Ability);
|
||||
|
||||
return {
|
||||
can(permission) {
|
||||
const { action, subject, properties = {}, condition } = permission;
|
||||
const { fields } = properties;
|
||||
|
||||
return can(
|
||||
action,
|
||||
isNil(subject) ? 'all' : subject,
|
||||
fields,
|
||||
isObject(condition) ? condition : undefined
|
||||
);
|
||||
},
|
||||
|
||||
build() {
|
||||
return build({ conditionsMatcher });
|
||||
},
|
||||
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
caslAbilityBuilder,
|
||||
};
|
7
packages/core/permissions/lib/engine/abilities/index.js
Normal file
7
packages/core/permissions/lib/engine/abilities/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { caslAbilityBuilder } = require('./casl-ability');
|
||||
|
||||
module.exports = {
|
||||
caslAbilityBuilder,
|
||||
};
|
106
packages/core/permissions/lib/engine/hooks.js
Normal file
106
packages/core/permissions/lib/engine/hooks.js
Normal file
@ -0,0 +1,106 @@
|
||||
'use strict';
|
||||
|
||||
const { cloneDeep, has } = require('lodash/fp');
|
||||
const { hooks } = require('@strapi/utils');
|
||||
|
||||
const domain = require('../domain');
|
||||
|
||||
/**
|
||||
* Create a hook map used by the permission Engine
|
||||
*/
|
||||
const createEngineHooks = () => ({
|
||||
'before-format::validate.permission': hooks.createAsyncBailHook(),
|
||||
'format.permission': hooks.createAsyncSeriesWaterfallHook(),
|
||||
'post-format::validate.permission': hooks.createAsyncBailHook(),
|
||||
'before-evaluate.permission': hooks.createAsyncSeriesHook(),
|
||||
'before-register.permission': hooks.createAsyncSeriesHook(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a context from a domain {@link Permission} used by the validate hooks
|
||||
* @param {Permission} permission
|
||||
* @return {{ readonly permission: Permission }}
|
||||
*/
|
||||
const createValidateContext = permission => ({
|
||||
get permission() {
|
||||
return cloneDeep(permission);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a context from a domain {@link Permission} used by the format hook
|
||||
* @param {Permission} permission
|
||||
* @return {{ readonly permission: Permission }}
|
||||
*/
|
||||
const createFormatContext = permission => ({
|
||||
get permission() {
|
||||
return cloneDeep(permission);
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Create a context from a domain {@link Permission} used by the before valuate hook
|
||||
* @param {Permission} permission
|
||||
* @return {{readonly permission: Permission, addCondition(string): this}}
|
||||
*/
|
||||
const createBeforeEvaluateContext = permission => ({
|
||||
get permission() {
|
||||
return cloneDeep(permission);
|
||||
},
|
||||
|
||||
addCondition(condition) {
|
||||
Object.assign(permission, domain.permission.addCondition(condition, permission));
|
||||
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a context from a casl Permission & some options
|
||||
* @param caslPermission
|
||||
* @param {object} options
|
||||
* @param {Permission} options.permission
|
||||
* @param {object} options.user
|
||||
*/
|
||||
const createWillRegisterContext = ({ permission, options }) => ({
|
||||
...options,
|
||||
|
||||
get permission() {
|
||||
return cloneDeep(permission);
|
||||
},
|
||||
|
||||
condition: {
|
||||
and(rawConditionObject) {
|
||||
if (!permission.condition) {
|
||||
Object.assign(permission, { condition: { $and: [] } });
|
||||
}
|
||||
|
||||
permission.condition.$and.push(rawConditionObject);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
or(rawConditionObject) {
|
||||
if (!permission.condition) {
|
||||
Object.assign(permission, { condition: { $and: [] } });
|
||||
}
|
||||
|
||||
const orClause = permission.condition.$and.find(has('$or'));
|
||||
|
||||
if (orClause) {
|
||||
orClause.$or.push(rawConditionObject);
|
||||
} else {
|
||||
permission.condition.$and.push({ $or: [rawConditionObject] });
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createEngineHooks,
|
||||
createValidateContext,
|
||||
createFormatContext,
|
||||
createBeforeEvaluateContext,
|
||||
createWillRegisterContext,
|
||||
};
|
210
packages/core/permissions/lib/engine/index.js
Normal file
210
packages/core/permissions/lib/engine/index.js
Normal file
@ -0,0 +1,210 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash/fp');
|
||||
|
||||
const abilities = require('./abilities');
|
||||
|
||||
const {
|
||||
createEngineHooks,
|
||||
createWillRegisterContext,
|
||||
createBeforeEvaluateContext,
|
||||
createFormatContext,
|
||||
createValidateContext,
|
||||
} = require('./hooks');
|
||||
|
||||
/**
|
||||
* @typedef {import("../..").PermissionEngine} PermissionEngine
|
||||
* @typedef {import("../..").ActionProvider} ActionProvider
|
||||
* @typedef {import("../..").ConditionProvider} ConditionProvider
|
||||
* @typedef {import("../..").PermissionEngineParams} PermissionEngineParams
|
||||
* @typedef {import("../..").Permission} Permission
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a default state object for the engine
|
||||
*/
|
||||
const createEngineState = () => {
|
||||
const hooks = createEngineHooks();
|
||||
|
||||
return { hooks };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
abilities,
|
||||
|
||||
/**
|
||||
* Create a new instance of a permission engine
|
||||
*
|
||||
* @param {PermissionEngineParams} params
|
||||
*
|
||||
* @return {PermissionEngine}
|
||||
*/
|
||||
new(params) {
|
||||
const { providers, abilityBuilderFactory = abilities.caslAbilityBuilder } = params;
|
||||
|
||||
const state = createEngineState();
|
||||
|
||||
const runValidationHook = async (hook, context) => state.hooks[hook].call(context);
|
||||
|
||||
/**
|
||||
* Evaluate a permission using local and registered behaviors (using hooks).
|
||||
* Validate, format (add condition, etc...), evaluate (evaluate conditions) and register a permission
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} params.options
|
||||
* @param {Function} params.register
|
||||
* @param {Permission} params.permission
|
||||
*/
|
||||
const evaluate = async params => {
|
||||
const { options, register, permission } = params;
|
||||
|
||||
const preFormatValidation = await runValidationHook(
|
||||
'before-format::validate.permission',
|
||||
createBeforeEvaluateContext(permission)
|
||||
);
|
||||
|
||||
if (preFormatValidation === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await state.hooks['format.permission'].call(createFormatContext(permission));
|
||||
|
||||
const postFormatValidation = await runValidationHook(
|
||||
'post-format::validate.permission',
|
||||
createValidateContext(permission)
|
||||
);
|
||||
|
||||
if (postFormatValidation === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await state.hooks['before-evaluate.permission'].call(createBeforeEvaluateContext(permission));
|
||||
|
||||
const { action, subject, properties, conditions = [] } = permission;
|
||||
|
||||
if (conditions.length === 0) {
|
||||
return register({ action, subject, properties });
|
||||
}
|
||||
|
||||
const resolveConditions = _.map(providers.condition.get);
|
||||
|
||||
const removeInvalidConditions = _.filter(condition => _.isFunction(condition.handler));
|
||||
|
||||
const evaluateConditions = conditions => {
|
||||
return Promise.all(
|
||||
conditions.map(async condition => ({
|
||||
condition,
|
||||
result: await condition.handler(
|
||||
_.merge(options, { permission: _.cloneDeep(permission) })
|
||||
),
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const removeInvalidResults = _.filter(
|
||||
({ result }) => _.isBoolean(result) || _.isObject(result)
|
||||
);
|
||||
|
||||
const evaluatedConditions = await Promise.resolve(conditions)
|
||||
.then(resolveConditions)
|
||||
.then(removeInvalidConditions)
|
||||
.then(evaluateConditions)
|
||||
.then(removeInvalidResults);
|
||||
|
||||
const resultPropEq = _.propEq('result');
|
||||
const pickResults = _.map(_.prop('result'));
|
||||
|
||||
if (evaluatedConditions.every(resultPropEq(true))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isEmpty(evaluatedConditions) || evaluatedConditions.some(resultPropEq(false))) {
|
||||
return register({ action, subject, properties });
|
||||
}
|
||||
|
||||
const results = pickResults(evaluatedConditions).filter(_.isObject);
|
||||
|
||||
if (_.isEmpty(results)) {
|
||||
return register({ action, subject, properties });
|
||||
}
|
||||
|
||||
return register({
|
||||
action,
|
||||
subject,
|
||||
properties,
|
||||
condition: { $and: [{ $or: results }] },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a register function that wraps a `can` function
|
||||
* used to register a permission in the ability builder
|
||||
*
|
||||
* @param {Function} can
|
||||
* @param {object} options
|
||||
*
|
||||
* @return {Function}
|
||||
*/
|
||||
const createRegisterFunction = (can, options) => {
|
||||
return async permission => {
|
||||
const hookContext = createWillRegisterContext({ options, permission });
|
||||
|
||||
await state.hooks['before-register.permission'].call(hookContext);
|
||||
|
||||
return can(permission);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
get hooks() {
|
||||
return state.hooks;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a new handler for a given hook
|
||||
*
|
||||
* @param {string} hook
|
||||
* @param {Function} handler
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
on(hook, handler) {
|
||||
const validHooks = Object.keys(state.hooks);
|
||||
const isValidHook = validHooks.includes(hook);
|
||||
|
||||
if (!isValidHook) {
|
||||
throw new Error(
|
||||
`Invalid hook supplied when trying to register an handler to the permission engine. Got "${hook}" but expected one of ${validHooks.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
state.hooks[hook].register(handler);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an ability based on the instance's
|
||||
* ability builder and the given permissions
|
||||
*
|
||||
* @param {Permission[]} permissions
|
||||
* @param {object} [options]
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
async generateAbility(permissions, options = {}) {
|
||||
const { can, build } = abilityBuilderFactory();
|
||||
|
||||
for (const permission of permissions) {
|
||||
const register = createRegisterFunction(can, options);
|
||||
|
||||
await evaluate({ permission, options, register });
|
||||
}
|
||||
|
||||
return build();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
9
packages/core/permissions/lib/index.js
Normal file
9
packages/core/permissions/lib/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const domain = require('./domain');
|
||||
const engine = require('./engine');
|
||||
|
||||
module.exports = {
|
||||
domain,
|
||||
engine,
|
||||
};
|
36
packages/core/permissions/package.json
Normal file
36
packages/core/permissions/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@strapi/permissions",
|
||||
"version": "4.2.2",
|
||||
"description": "Strapi's permission layer.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/strapi/strapi.git"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"author": {
|
||||
"name": "Strapi Solutions SAS",
|
||||
"email": "hi@strapi.io",
|
||||
"url": "https://strapi.io"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Strapi Solutions SAS",
|
||||
"email": "hi@strapi.io",
|
||||
"url": "https://strapi.io"
|
||||
}
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"scripts": {
|
||||
"test:unit": "jest --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/utils": "4.2.2",
|
||||
"lodash": "4.17.21",
|
||||
"@casl/ability": "5.4.4",
|
||||
"sift": "16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.19.1 <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user