diff --git a/.github/workflows/deploy_docs_3x.yml b/.github/workflows/deploy_docs_3x.yml index 6ebfa977..8ac8f250 100644 --- a/.github/workflows/deploy_docs_3x.yml +++ b/.github/workflows/deploy_docs_3x.yml @@ -21,3 +21,4 @@ jobs: with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/authorization-docs-3' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + branch: '3.x' diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml new file mode 100644 index 00000000..21b7e237 --- /dev/null +++ b/.github/workflows/docs-validation.yml @@ -0,0 +1,27 @@ +name: Documentation Validation + +on: + push: + branches: + - 3.x + paths: + - 'docs/**' + - '.github/**' + pull_request: + paths: + - 'docs/**' + - '.github/**' + +jobs: + validate: + uses: cakephp/.github/.github/workflows/docs-validation.yml@5.x + with: + docs-path: 'docs' + vitepress-path: 'docs/.vitepress' + enable-config-js-check: true + enable-json-lint: true + enable-toc-check: true + enable-spell-check: true + enable-markdown-lint: true + enable-link-check: true + tools-ref: '5.x' diff --git a/Dockerfile b/Dockerfile index 7acfb27e..832f6bc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,36 @@ -# Basic docker based environment -# Necessary to trick dokku into building the documentation -# using dockerfile instead of herokuish -FROM ubuntu:22.04 - -# Add basic tools -RUN apt-get update && \ - apt-get install -y build-essential \ - software-properties-common \ - curl \ - git \ - libxml2 \ - libffi-dev \ - libssl-dev - -# Prevent interactive timezone input -ENV DEBIAN_FRONTEND=noninteractive -RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ - apt-get update && \ - apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite - -WORKDIR /code - -VOLUME ["/code"] - -CMD [ '/bin/bash' ] +# ---------------------- +# 1. Build stage +# ---------------------- +FROM node:22-alpine AS builder + +# Git is required because docs/package.json pulls a dependency from GitHub. +RUN apk add --no-cache git openssh-client + +WORKDIR /app/docs + +# Copy dependency manifests first to preserve Docker layer caching. +COPY docs/ ./ +RUN npm ci + +# Increase max-old-space-size to avoid memory issues during build +ENV NODE_OPTIONS="--max-old-space-size=8192" + +# Build the site. +RUN npm run docs:build + +# ---------------------- +# 2. Runtime stage (nginx) +# ---------------------- +FROM nginx:1.27-alpine AS runner + +# Copy built files +COPY --from=builder /app/docs/.vitepress/dist /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Health check (optional) +HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docs.Dockerfile b/docs.Dockerfile deleted file mode 100644 index 9bb0a51e..00000000 --- a/docs.Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -# Generate the HTML output. -FROM ghcr.io/cakephp/docs-builder as builder - -ENV LANGS="en es fr ja" - -WORKDIR /data/docs-builder - -COPY docs /data/docs - -# Build docs with sphinx -RUN make website LANGS="$LANGS" SOURCE=/data/docs DEST=/data/website - -# Build a small nginx container with just the static site in it. -FROM ghcr.io/cakephp/docs-builder:runtime as runtime - -# Configure search index script -ENV LANGS="en es fr ja" -ENV SEARCH_SOURCE="/usr/share/nginx/html" -ENV SEARCH_URL_PREFIX="/authorization/3" - -COPY --from=builder /data/docs /data/docs -COPY --from=builder /data/website/html/ /usr/share/nginx/html/ -COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf - -RUN ln -s /usr/share/nginx/html /usr/share/nginx/html/2.x diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..974d64e4 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +node_modules +*/public/ +.vitepress/cache +.vitepress/dist diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 00000000..c257861f --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,75 @@ +import baseConfig from '@cakephp/docs-skeleton/config' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) +const tocEn = require('./toc_en.json') +const tocEs = require('./toc_es.json') +const tocFr = require('./toc_fr.json') +const tocJa = require('./toc_ja.json') + +const versions = { + text: '3.x', + items: [ + { text: '3.x (current)', link: 'https://book.cakephp.org/authorization/3/', target: '_self' }, + { text: '2.x', link: 'https://book.cakephp.org/authorization/2/en/', target: '_self' }, + ], +} + +export default { + extends: baseConfig, + srcDir: '.', + title: 'Authorization', + description: 'CakePHP Authorization Documentation', + base: '/authorization/3/', + rewrites: { + 'en/:slug*': ':slug*', + }, + sitemap: { + hostname: 'https://book.cakephp.org/authorization/3/', + }, + themeConfig: { + socialLinks: [ + { icon: 'github', link: 'https://github.com/cakephp/authorization' }, + ], + editLink: { + pattern: 'https://github.com/cakephp/authorization/edit/3.x/docs/:path', + text: 'Edit this page on GitHub', + }, + sidebar: tocEn, + nav: [ + { text: 'CakePHP', link: 'https://cakephp.org' }, + { text: 'API', link: 'https://api.cakephp.org/authorization/' }, + { ...versions }, + ], + }, + locales: { + root: { + label: 'English', + lang: 'en', + themeConfig: { + sidebar: tocEn, + }, + }, + es: { + label: 'Español', + lang: 'es', + themeConfig: { + sidebar: tocEs, + }, + }, + fr: { + label: 'Français', + lang: 'fr', + themeConfig: { + sidebar: tocFr, + }, + }, + ja: { + label: '日本語', + lang: 'ja', + themeConfig: { + sidebar: tocJa, + }, + }, + }, +} diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 00000000..e33e19ec --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1 @@ +export { default } from '@cakephp/docs-skeleton' diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json new file mode 100644 index 00000000..b973bf1d --- /dev/null +++ b/docs/.vitepress/toc_en.json @@ -0,0 +1,25 @@ +{ + "/": [ + { + "text": "Getting Started", + "collapsed": false, + "items": [ + { "text": "Quick Start", "link": "/" }, + { "text": "Policies", "link": "/policies" }, + { "text": "Policy Resolvers", "link": "/policy-resolvers" }, + { "text": "Authorization Middleware", "link": "/middleware" }, + { "text": "Checking Authorization", "link": "/checking-authorization" }, + { "text": "AuthorizationComponent", "link": "/component" }, + { "text": "Request Authorization Middleware", "link": "/request-authorization-middleware" } + ] + }, + { + "text": "Migration Guides", + "collapsed": false, + "items": [ + { "text": "2.0 Migration Guide", "link": "/2-0-migration-guide" }, + { "text": "3.0 Migration Guide", "link": "/3-0-migration-guide" } + ] + } + ] +} diff --git a/docs/.vitepress/toc_es.json b/docs/.vitepress/toc_es.json new file mode 100644 index 00000000..bf5cc10e --- /dev/null +++ b/docs/.vitepress/toc_es.json @@ -0,0 +1,12 @@ +{ + "/es/": [ + { + "text": "Introducción", + "collapsed": false, + "items": [ + { "text": "Inicio Rápido", "link": "/es/" }, + { "text": "Policy", "link": "/es/policies" } + ] + } + ] +} diff --git a/docs/.vitepress/toc_fr.json b/docs/.vitepress/toc_fr.json new file mode 100644 index 00000000..c6d4cc13 --- /dev/null +++ b/docs/.vitepress/toc_fr.json @@ -0,0 +1,24 @@ +{ + "/fr/": [ + { + "text": "Prise en main", + "collapsed": false, + "items": [ + { "text": "Introduction", "link": "/fr/" }, + { "text": "Policies", "link": "/fr/policies" }, + { "text": "Résolveurs de Policy", "link": "/fr/policy-resolvers" }, + { "text": "Middleware Authorization", "link": "/fr/middleware" }, + { "text": "Vérifier une Autorisation", "link": "/fr/checking-authorization" }, + { "text": "AuthorizationComponent", "link": "/fr/component" }, + { "text": "Middleware d'Autorisation de Requête", "link": "/fr/request-authorization-middleware" } + ] + }, + { + "text": "Migration", + "collapsed": false, + "items": [ + { "text": "Guide de Migration vers 2.0", "link": "/fr/2-0-migration-guide" } + ] + } + ] +} diff --git a/docs/.vitepress/toc_ja.json b/docs/.vitepress/toc_ja.json new file mode 100644 index 00000000..12b49556 --- /dev/null +++ b/docs/.vitepress/toc_ja.json @@ -0,0 +1,24 @@ +{ + "/ja/": [ + { + "text": "はじめに", + "collapsed": false, + "items": [ + { "text": "クイックスタート", "link": "/ja/" }, + { "text": "Policies", "link": "/ja/policies" }, + { "text": "ポリシーリゾルバー", "link": "/ja/policy-resolvers" }, + { "text": "認可ミドルウェア", "link": "/ja/middleware" }, + { "text": "認可の確認", "link": "/ja/checking-authorization" }, + { "text": "AuthorizationComponent", "link": "/ja/component" }, + { "text": "リクエスト認証ミドルウェア", "link": "/ja/request-authorization-middleware" } + ] + }, + { + "text": "移行ガイド", + "collapsed": false, + "items": [ + { "text": "2.0 Migration Guide", "link": "/ja/2-0-migration-guide" } + ] + } + ] +} diff --git a/docs/config/__init__.py b/docs/config/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/config/all.py b/docs/config/all.py deleted file mode 100644 index 5fc514ac..00000000 --- a/docs/config/all.py +++ /dev/null @@ -1,47 +0,0 @@ -# Global configuration information used across all the -# translations of documentation. -# -# Import the base theme configuration -from cakephpsphinx.config.all import * - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# - -# The full version, including alpha/beta/rc tags. -release = '3.x' - -# The search index version -search_version = 'authorization-3' - -# The marketing display name for the book. -version_name = '' - -# Project name shown in the black header bar -project = 'CakePHP Authorization' - -# Other versions that display in the version picker menu. -version_list = [ - {'name': '1.x', 'number': '/authorization/1.1', 'title': '1.x'}, - {'name': '2.x', 'number': '/authorization/2.x', 'title': '2.x'}, - {'name': '3.x', 'number': '/authorization/3.x', 'title': '3.x', 'current': True}, -] - -# Languages available. -languages = ['en', 'es', 'fr'] - -# The GitHub branch name for this version of the docs -# for edit links to point at. -branch = '3.x' - -# Current version being built -version = '3.x' - -show_root_link = True - -repository = 'cakephp/authorization' - -source_path = 'docs/' - -hide_page_contents = ('search', '404', 'contents') diff --git a/docs/en/2-0-migration-guide.md b/docs/en/2-0-migration-guide.md new file mode 100644 index 00000000..1a89d804 --- /dev/null +++ b/docs/en/2-0-migration-guide.md @@ -0,0 +1,13 @@ +# 2.0 Migration Guide + +Authorization 2.0 introduced new features and a few breaking changes. + +## Breaking Changes + +`IdentityInterface` gained type declarations. If you implement that interface +in your application, update your implementation to match the new signatures. + +`IdentityInterface` also gained `canResult()`. It always returns a +`ResultInterface`, while `can()` now always returns a boolean. In 1.x, `can()` +could return either a boolean or a `ResultInterface`, which made it difficult +to rely on consistently. diff --git a/docs/en/2-0-migration-guide.rst b/docs/en/2-0-migration-guide.rst deleted file mode 100644 index 473294b0..00000000 --- a/docs/en/2-0-migration-guide.rst +++ /dev/null @@ -1,18 +0,0 @@ -2.0 Migration Guide -################### - -Authorization 2.0 contains new features and a few breaking changes. - -Breaking Changes -================ - -The ``IdentityInterface`` has had typehinting added. If you have implemented the -``IdentityInterface`` you will need to update your application's implementation -to reflect the new typehints. - -In addition to typehints ``IdentityInterface`` has a ``canResult()`` method -added. This method always returns a ``ResultInterface`` object while ``can()`` -always returns a boolean. In 1.x the ``can()`` method would return a boolean or -``ResultInterface`` depending on what the policy returned. This made knowing the -return value of ``can()`` very hard. The new methods and additional typings -make ``IdentityInterface`` simpler and more reliable to use. diff --git a/docs/en/3-0-migration-guide.md b/docs/en/3-0-migration-guide.md new file mode 100644 index 00000000..221d2959 --- /dev/null +++ b/docs/en/3-0-migration-guide.md @@ -0,0 +1,27 @@ +# 3.0 Migration Guide + +Authorization 3.0 includes new features and several breaking changes. + +## Breaking Changes + +The following interfaces now have explicit parameter and return types: + +- `AuthorizationServiceInterface` +- `IdentityInterface` +- `BeforePolicyInterface` +- `RequestPolicyInterface` +- `ResolverInterface` + +## Multiple Optional Arguments + +The following methods were updated to accept and pass through multiple optional +arguments: + +- `IdentityInterface::applyScope` +- `AuthorizationServiceInterface::applyScope` +- `AuthorizationServiceInterface::can` +- `AuthorizationServiceInterface::canResult` + +## Removed Methods + +- `AuthorizationService::resultTypeCheck`, replaced by an `assert()` call. diff --git a/docs/en/3-0-migration-guide.rst b/docs/en/3-0-migration-guide.rst deleted file mode 100644 index 3da92c37..00000000 --- a/docs/en/3-0-migration-guide.rst +++ /dev/null @@ -1,30 +0,0 @@ -3.0 Migration Guide -################### - -Authorization 3.0 contains new features and a few breaking changes. - -Breaking Changes -================ - -The following interfaces now have appropriate parameter and return types added: - -- ``AuthorizationServiceInterface`` -- ``IdentityInterface`` -- ``BeforePolicyInterface`` -- ``RequestPolicyInterface.php`` -- ``ResolverInterface`` - -Multiple optional arguments for ``applyScope``, ``can`` and ``canResult`` -------------------------------------------------------------------------- - -The following interface methods have been adjusted to pass on multiple optional arguments. -- ``IdentityInterface::applyScope`` -- ``AuthorizationServiceInterface::applyScope`` -- ``AuthorizationServiceInterface::can`` -- ``AuthorizationServiceInterface::canResult`` - -Removed methods ---------------- - -- ``AuthorizationService::resultTypeCheck`` - has been replaced with an ``assert()`` call - diff --git a/docs/en/checking-authorization.md b/docs/en/checking-authorization.md new file mode 100644 index 00000000..b64800f4 --- /dev/null +++ b/docs/en/checking-authorization.md @@ -0,0 +1,46 @@ +# Checking Authorization + +Once you have applied the [middleware](/middleware) and attached an `identity` +to the request, you can start checking authorization. The middleware decorates +the request identity with authorization-related helper methods. + +You can pass the `identity` into models, services, or templates and check +permissions anywhere in your application. See the +[identity decorator section](/middleware#identity-decorator) for customization +options. + +## Checking Authorization for a Single Resource + +Use `can()` to check authorization for a single resource, typically an ORM +entity or domain object: + +```php +$user = $this->request->getAttribute('identity'); + +if ($user->can('delete', $article)) { + // Do delete operation +} +``` + +If your policies return result objects, use `canResult()` and inspect the +status: + +```php +$result = $user->canResult('delete', $article); +if ($result->getStatus()) { + // Do deletion +} +``` + +## Applying Scope Conditions + +When working with collections such as paginated queries, apply authorization +conditions through scopes so only accessible records are returned: + +```php +$user = $this->request->getAttribute('identity'); +$query = $user->applyScope('index', $query); +``` + +In controller actions, [AuthorizationComponent](/component) can streamline +checks that should raise exceptions on failure. diff --git a/docs/en/checking-authorization.rst b/docs/en/checking-authorization.rst deleted file mode 100644 index d68a26ad..00000000 --- a/docs/en/checking-authorization.rst +++ /dev/null @@ -1,55 +0,0 @@ -Checking Authorization -###################### - -Once you have applied the :doc:`/middleware` to your application and added an -``identity`` to the request, you can start checking authorization. The -middleware will wrap the ``identity`` in each request with an -``IdentityDecorator`` that adds authorization related methods. - -You can pass the ``identity`` into your models, services or templates allowing -you to check authorization anywhere in your application easily. See the -:ref:`identity-decorator` section for how to customize or replace the default -decorator. - -Checking Authorization for a Single Resource -============================================ - -The ``can`` method enables you to check authorization on a single resource. -Typically this is an ORM entity, or application domain object. Your -:doc:`/policies` provide logic to make the authorization decision:: - - // Get the identity from the request - $user = $this->request->getAttribute('identity'); - - // Check authorization on $article - if ($user->can('delete', $article)) { - // Do delete operation - } - -If your policies return :ref:`policy-result-objects` -be sure to check their status as ``canResult()`` returns the result instance:: - - // Assuming our policy returns a result. - $result = $user->canResult('delete', $article); - if ($result->getStatus()) { - // Do deletion - } - -Applying Scope Conditions -========================= - -When you need to apply authorization checks to a collection of objects like -a paginated query you will often want to only fetch records that the current -user has access to. This plugin implements this concept as 'scopes'. Scope -policies allow you to 'scope' a query or result set and return the updated list -or query object:: - - // Get the identity from the request - $user = $this->request->getAttribute('identity'); - - // Apply permission conditions to a query so only - // the records the current user has access to are returned. - $query = $user->applyScope('index', $query); - -The :doc:`/component` can be used in controller actions -to streamline authorization checks that raise exceptions on failure. diff --git a/docs/en/component.md b/docs/en/component.md new file mode 100644 index 00000000..30d456b0 --- /dev/null +++ b/docs/en/component.md @@ -0,0 +1,131 @@ +# AuthorizationComponent + +`AuthorizationComponent` exposes convention-based helpers for checking +permissions in controllers. It handles fetching the user and calling `can()` or +`applyScope()` for you. The component depends on the middleware, so make sure +the middleware is already in place. + +Load it in your `AppController`: + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +## Automatic Authorization Checks + +You can configure the component to authorize actions automatically based on the +controller's default model class and the current action: + +```php +$this->Authorization->authorizeModel('index', 'add'); +``` + +You can also mark actions as public by skipping authorization: + +```php +$this->loadComponent('Authorization.Authorization', [ + 'skipAuthorization' => [ + 'login', + ], +]); +``` + +By default, every action requires authorization when authorization checking is +enabled. + +## Checking Authorization + +In controller actions or callbacks: + +```php +public function edit($id) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article); + + // Rest of edit method +} +``` + +If you omit the action name, the request action is used. You can override it: + +```php +$this->Authorization->authorize($article, 'update'); +``` + +`authorize()` throws `Authorization\Exception\ForbiddenException` when access is +denied. Use `can()` if you want a boolean result: + +```php +if ($this->Authorization->can($article, 'update')) { + // Do something to the article +} +``` + +## Anonymous Users + +Policies decide whether unauthenticated users may access a resource. Both +`can()` and `authorize()` support anonymous users, and your policy methods can +expect `null` for the user parameter when no one is logged in. + +## Applying Policy Scopes + +You can apply policy scopes through the component: + +```php +$query = $this->Authorization->applyScope($this->Articles->find()); +``` + +If there is no logged-in user, `MissingIdentityException` is raised. + +If your controller actions map to different policy method names, use +`actionMap`: + +```php +$this->Authorization->mapActions([ + 'index' => 'list', + 'delete' => 'remove', + 'add' => 'insert', +]); + +$this->Authorization + ->mapAction('index', 'list') + ->mapAction('delete', 'remove') + ->mapAction('add', 'insert'); +``` + +Example usage: + +```php +public function index() +{ + $query = $this->Articles->find(); + $this->Authorization->applyScope($query); +} + +public function delete($id) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article); +} + +public function add() +{ + $this->Authorization->authorizeModel(); +} +``` + +## Skipping Authorization + +You can also skip authorization inside an action: + +```php +public function view($id) +{ + $this->Authorization->skipAuthorization(); +} +``` diff --git a/docs/en/component.rst b/docs/en/component.rst deleted file mode 100644 index 350ebe3b..00000000 --- a/docs/en/component.rst +++ /dev/null @@ -1,145 +0,0 @@ -AuthorizationComponent -###################### - -The ``AuthorizationComponent`` exposes a few conventions based helper methods for -checking permissions from your controllers. It abstracts getting the user and -calling the ``can`` or ``applyScope`` methods. Using the AuthorizationComponent -requires use of the Middleware, so make sure it is applied as well. To use the -component, first load it:: - - // In your AppController - public function initialize(): void - { - parent::initialize(); - $this->loadComponent('Authorization.Authorization'); - } - -Automatic authorization checks -============================== - -``AuthorizationComponent`` can be configured to automatically apply -authorization based on the controller's default model class and current action -name. In the following example ``index`` and ``add`` actions will be authorized:: - - $this->Authorization->authorizeModel('index', 'add'); - -You can also configure actions to skip authorization. This will make actions **public**, -accessible to all users. By default all actions require authorization and -``AuthorizationRequiredException`` will be thrown if authorization checking is enabled. - -Authorization can be skipped for individual actions:: - - $this->loadComponent('Authorization.Authorization', [ - 'skipAuthorization' => [ - 'login', - ] - ]); - -Checking Authorization -====================== - -In your controller actions or callback methods you can check authorization using -the component:: - - // In the Articles Controller. - public function edit($id) - { - $article = $this->Articles->get($id); - $this->Authorization->authorize($article); - // Rest of the edit method. - } - -Above we see an article being authorized for the current user. Since we haven't -specified the action to check the request's ``action`` is used. You can specify -a policy action with the second parameter:: - - // Use a policy method that doesn't match the current controller action. - $this->Authorization->authorize($article, 'update'); - -The ``authorize()`` method will raise an ``Authorization\Exception\ForbiddenException`` -when permission is denied. If you want to check authorization and get a boolean -result you can use the ``can()`` method:: - - if ($this->Authorization->can($article, 'update')) { - // Do something to the article. - } - -Anonymous Users -=============== - -Some resources in your application may be accessible to users who are not logged -in. Whether or not a resource can be accessed by an un-authenticated -user is in the domain of policies. Through the component you can check -authorization for anonymous users. Both the ``can()`` and ``authorize()`` support -anonymous users. Your policies can expect to get ``null`` for the 'user' parameter -when the user is not logged in. - -.. _applying-policy-scopes: - -Applying Policy Scopes -====================== - -You can also apply policy scopes using the component:: - -$query = $this->Authorization->applyScope($this->Articles->find()); - -If the current action has no logged in user a ``MissingIdentityException`` will -be raised. - -If you want to map actions to different authorization methods use the -``actionMap`` option:: - - // In your controller initialize() method: - $this->Authorization->mapActions([ - 'index' => 'list', - 'delete' => 'remove', - 'add' => 'insert', - ]); - - // or map actions individually. - $this->Authorization - ->mapAction('index','list') - ->mapAction('delete', 'remove') - ->mapAction('add', 'insert'); - -Example:: - - //ArticlesController.php - - public function index() - { - $query = $this->Articles->find(); - - //this will apply `list` scope while being called in `index` controller action. - $this->Authorization->applyScope($query); - ... - } - - public function delete($id) - { - $article = $this->Articles->get($id); - - //this will authorize against `remove` entity action while being called in `delete` controller action. - $this->Authorization->authorize($article); - ... - } - - public function add() - { - //this will authorize against `insert` model action while being called in `add` controller action. - $this->Authorization->authorizeModel(); - ... - } - -Skipping Authorization -====================== - -Authorization can also be skipped inside an action:: - - //ArticlesController.php - - public function view($id) - { - $this->Authorization->skipAuthorization(); - ... - } diff --git a/docs/en/conf.py b/docs/en/conf.py deleted file mode 100644 index f638bda2..00000000 --- a/docs/en/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'en' diff --git a/docs/en/contents.rst b/docs/en/contents.rst deleted file mode 100644 index 9588a970..00000000 --- a/docs/en/contents.rst +++ /dev/null @@ -1,16 +0,0 @@ -Contents -######## - -.. toctree:: - :maxdepth: 2 - :caption: CakePHP Authorization - - /index - /policies - /policy-resolvers - /middleware - /checking-authorization - /component - /request-authorization-middleware - /2-0-migration-guide - /3-0-migration-guide diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 00000000..e4fd143e --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,115 @@ +# Quick Start + +## Installation + +Install the plugin with [Composer](https://getcomposer.org/) from your CakePHP +project root, where `composer.json` is located: + +```bash +php composer.phar require "cakephp/authorization:^3.0" +``` + +Authorization 3.x is compatible with CakePHP 5. + +Load the plugin in `src/Application.php`: + +```php +$this->addPlugin('Authorization'); +``` + +## Getting Started + +The Authorization plugin integrates into your application as middleware and, +optionally, as a component that makes authorization checks easier in +controllers. + +In `src/Application.php`, add the required imports: + +```php +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Psr\Http\Message\ServerRequestInterface; +``` + +Implement `AuthorizationServiceProviderInterface` on your application class: + +```php +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +``` + +Then register the middleware: + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) + ->add(new AssetMiddleware()) + ->add(new RoutingMiddleware($this)) + ->add(new BodyParserMiddleware()) + + // If you use Authentication it must come before Authorization. + ->add(new AuthenticationMiddleware($this)) + + // Add Authorization after routing, body parsing, and authentication. + ->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; +} +``` + +`AuthorizationMiddleware` must be added after authentication so the request +contains an `identity` for authorization checks. + +The middleware calls a hook on your application to obtain the authorization +service. Add this method to `src/Application.php`: + +```php +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); +} +``` + +This configures the basic [policy resolvers](/policy-resolvers) that map ORM +entities to policy classes. + +Next, load the component in `src/Controller/AppController.php`: + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +With the [AuthorizationComponent](/component) loaded, you can authorize +resources per action: + +```php +public function edit($id = null) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article, 'update'); + + // Rest of action +} +``` + +Calling `authorize()` lets your [policies](/policies) enforce access-control +rules. You can also check permissions anywhere you have access to the +[request identity](/checking-authorization). + +## Further Reading + +- [Policies](/policies) +- [Policy Resolvers](/policy-resolvers) +- [Authorization Middleware](/middleware) +- [AuthorizationComponent](/component) +- [Checking Authorization](/checking-authorization) +- [Request Authorization Middleware](/request-authorization-middleware) diff --git a/docs/en/index.rst b/docs/en/index.rst deleted file mode 100644 index 65956556..00000000 --- a/docs/en/index.rst +++ /dev/null @@ -1,110 +0,0 @@ -Quick Start -########### - -Installation -============ - -Install the plugin with `composer `__ from your CakePHP -Project's ROOT directory (where the **composer.json** file is located) - -.. code-block:: shell - - php composer.phar require "cakephp/authorization:^3.0" - -Version 3 of the Authorization Plugin is compatible with CakePHP 5. - -Load the plugin by adding the following statement in your project's -``src/Application.php``:: - - $this->addPlugin('Authorization'); - -Getting Started -=============== - -The Authorization plugin integrates into your application as a middleware layer -and optionally a component to make checking authorization easier. First, let's -apply the middleware. In **src/Application.php** add the following to the class -imports:: - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ServerRequestInterface; - -Add the ``AuthorizationServiceProviderInterface`` to the implemented interfaces -on your application:: - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - -Then make your application's ``middleware()`` method look like:: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue - { - // Middleware provided by CakePHP - $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) - ->add(new AssetMiddleware()) - ->add(new RoutingMiddleware($this)) - ->add(new BodyParserMiddleware()) - - // If you are using Authentication it should be *before* Authorization. - ->add(new AuthenticationMiddleware($this)) - - // Add the AuthorizationMiddleware *after* routing, body parser - // and authentication middleware. - ->add(new AuthorizationMiddleware($this)); - - return $middlewareQueue; - } - -The placement of the ``AuthorizationMiddleware`` is important and must be added -*after* your authentication middleware. This ensures that the request has an -``identity`` which can be used for authorization checks. - -The ``AuthorizationMiddleware`` will call a hook method on your application when -it starts handling the request. This hook method allows your application to -define the ``AuthorizationService`` it wants to use. Add the following method your -**src/Application.php**:: - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - -This configures basic :doc:`/policy-resolvers` that will match -ORM entities with their policy classes. - -Next, let's add the ``AuthorizationComponent`` to ``AppController``. In -**src/Controller/AppController.php** add the following to the ``initialize()`` -method:: - - $this->loadComponent('Authorization.Authorization'); - -By loading the :doc:`/component` we'll be able to check -authorization on a per-action basis more easily. For example, we can do:: - - public function edit($id = null) - { - $article = $this->Articles->get($id); - $this->Authorization->authorize($article, 'update'); - - // Rest of action - } - -By calling ``authorize`` we can use our :doc:`/policies` to enforce our -application's access control rules. You can check permissions anywhere by using -the :doc:`identity stored in the request `. - - -Further Reading -=============== - -* :doc:`/policies` -* :doc:`/policy-resolvers` -* :doc:`/middleware` -* :doc:`/component` -* :doc:`/checking-authorization` -* :doc:`/request-authorization-middleware` diff --git a/docs/en/middleware.md b/docs/en/middleware.md new file mode 100644 index 00000000..b4306fd2 --- /dev/null +++ b/docs/en/middleware.md @@ -0,0 +1,252 @@ +# Authorization Middleware + +Authorization is applied as middleware. `AuthorizationMiddleware` handles two +main responsibilities: + +- Decorating the request `identity` with `can`, `canResult`, and `applyScope` + methods when needed. +- Ensuring each request has authorization checked or explicitly bypassed. + +To use the middleware, implement +`AuthorizationServiceProviderInterface` in your application class, then add the +middleware to the queue. + +```php +namespace App; + +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Cake\Http\BaseApplication; +use Psr\Http\Message\ServerRequestInterface; + +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +{ + public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface + { + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); + } + + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + $middlewareQueue->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; + } +} +``` + +The authorization service requires a policy resolver. See [Policies](/policies) +and [Policy Resolvers](/policy-resolvers) for the available options. + +## Identity Decorator + +By default, the request `identity` is wrapped in +`Authorization\IdentityDecorator`. The decorator proxies method calls, array +access, and property access to the underlying identity. Use +`getOriginalData()` to retrieve the original value: + +```php +$originalUser = $user->getOriginalData(); +``` + +If you use the +[cakephp/authentication](https://github.com/cakephp/authentication) plugin, +`Authorization\Identity` is used instead. It implements both +`Authentication\IdentityInterface` and `Authorization\IdentityInterface`, so +the decorated identity works with Authentication helpers and components. + +### Using Your User Class as the Identity + +If your existing user class already represents identity data, implement +`Authorization\IdentityInterface` and use the `identityDecorator` option to +skip the wrapper: + +```php +namespace App\Model\Entity; + +use Authorization\AuthorizationServiceInterface; +use Authorization\IdentityInterface; +use Authorization\Policy\ResultInterface; +use Cake\ORM\Entity; + +class User extends Entity implements IdentityInterface +{ + public function can(string $action, mixed $resource): bool + { + return $this->authorization->can($this, $action, $resource); + } + + public function canResult(string $action, mixed $resource): ResultInterface + { + return $this->authorization->canResult($this, $action, $resource); + } + + public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed + { + return $this->authorization->applyScope($this, $action, $resource, ...$optionalArgs); + } + + public function getOriginalData(): \ArrayAccess|array + { + return $this; + } + + public function setAuthorization(AuthorizationServiceInterface $service): static + { + $this->authorization = $service; + + return $this; + } +} +``` + +Then configure the middleware: + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'identityDecorator' => function ($auth, $user) { + return $user->setAuthorization($auth); + }, +])); +``` + +If you also use Authentication, implement both interfaces: + +```php +use Authentication\IdentityInterface as AuthenticationIdentity; +use Authorization\IdentityInterface as AuthorizationIdentity; + +class User extends Entity implements AuthorizationIdentity, AuthenticationIdentity +{ + public function getIdentifier(): int|string|null + { + return $this->id; + } +} +``` + +## Ensuring Authorization Is Applied + +By default, `AuthorizationMiddleware` verifies that each request with an +`identity` also performed or bypassed authorization checks. If not, +`AuthorizationRequiredException` is raised after controller and middleware +execution completes. + +This does not prevent unauthorized access by itself, but it is valuable during +development and testing. You can disable the requirement: + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'requireAuthorizationCheck' => false, +])); +``` + +## Handling Unauthorized Requests + +By default, authorization exceptions are rethrown. You can configure handlers +for unauthorized requests, such as redirecting a user to a login page. + +Built-in handlers: + +- `Exception`, which rethrows the exception. +- `Redirect`, which redirects to a plain URL. +- `CakeRedirect`, which redirects using CakePHP router syntax. + +Shared redirect options: + +- `url`, the redirect target. +- `exceptions`, the exception classes that trigger redirect handling. +- `queryParam`, the query string key used for the original URL. Defaults to + `redirect`. +- `statusCode`, the redirect status code. Defaults to `302`. +- `allowedRedirectExtensions`, a whitelist of file extensions that may still + redirect. Set this carefully to avoid redirecting API-style requests. + +Example: + +```php +use Authorization\Exception\MissingIdentityException; + +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'unauthorizedHandler' => [ + 'className' => 'Authorization.Redirect', + 'url' => '/pages/unauthorized', + 'queryParam' => 'redirectUrl', + 'exceptions' => [ + MissingIdentityException::class, + OtherException::class, + ], + 'allowedRedirectExtensions' => ['csv', 'pdf'], + ], +])); +``` + +All handlers receive the thrown exception, which is always an instance of +`Authorization\Exception\Exception`. + +To handle more exception types gracefully, add them to `exceptions`: + +```php +'exceptions' => [ + MissingIdentityException::class, + ForbiddenException::class, +], +``` + +## Adding a Flash Message After Redirect + +If you need to add a flash message or custom side effects on redirect, create +your own unauthorized handler based on the built-in redirect handler. + +Create `src/Middleware/UnauthorizedHandler/CustomRedirectHandler.php`: + +```php +getFlash()->error('You are not authorized to access that location'); + + return $response; + } +} +``` + +Then reference it from your middleware configuration: + +```php +use Authorization\Exception\ForbiddenException; +use Authorization\Exception\MissingIdentityException; + +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'unauthorizedHandler' => [ + 'className' => 'CustomRedirect', + 'url' => '/users/login', + 'queryParam' => 'redirectUrl', + 'exceptions' => [ + MissingIdentityException::class, + ForbiddenException::class, + ], + 'custom_param' => true, + ], +])); +``` + +Any additional handler configuration is available in the `$options` array +received by `handle()`. diff --git a/docs/en/middleware.rst b/docs/en/middleware.rst deleted file mode 100644 index 35255654..00000000 --- a/docs/en/middleware.rst +++ /dev/null @@ -1,299 +0,0 @@ -Authorization Middleware -######################## - -Authorization is applied to your application as a middleware. The -``AuthorizationMiddleware`` handles the following responsibilities: - -* Decorating the request 'identity' with a decorator that adds the ``can``, - ``canResult``, and ``applyScope`` methods if necessary. -* Ensuring that authorization has been checked/bypassed in the request. - -To use the middleware implement ``AuthorizationServiceProviderInterface`` in your -application class. Then pass your app instance into the middleware and add the -middleware to the queue. - -A basic example would be:: - - namespace App; - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Cake\Http\BaseApplication; - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - { - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue - { - // other middleware - $middlewareQueue->add(new AuthorizationMiddleware($this)); - - return $middlewareQueue; - } - } - -The authorization service requires a policy resolver. See the -:doc:`/policies` documentation on what resolvers are available and how -to use them. - -.. _identity-decorator: - -Identity Decorator -================== - -By default the ``identity`` in the request will be decorated (wrapped) with -``Authorization\IdentityDecorator``. The decorator class proxies method calls, -array access and property access to the decorated identity object. To access the -underlying identity directly use ``getOriginalData()``:: - - $originalUser = $user->getOriginalData(); - -If your application uses the `cakephp/authentication -`_ plugin then the -``Authorization\Identity`` class will be used. This class implements the -``Authentication\IdentityInterface`` in addition to the -``Authorization\IdentityInterface``. This allows you to use the -``Authentication`` lib's component and helper to get the decorated identity. - -Using your User class as the Identity -------------------------------------- - -If you have an existing ``User`` or identity class you can skip the decorator by -implementing the ``Authorization\IdentityInterface`` and using the -``identityDecorator`` middleware option. First lets update our ``User`` class:: - - namespace App\Model\Entity; - - use Authorization\AuthorizationServiceInterface; - use Authorization\IdentityInterface; - use Authorization\Policy\ResultInterface; - use Cake\ORM\Entity; - - class User extends Entity implements IdentityInterface - { - /** - * @inheritDoc - */ - public function can(string $action, mixed $resource): bool - { - return $this->authorization->can($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function canResult(string $action, mixed $resource): ResultInterface - { - return $this->authorization->canResult($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed - { - return $this->authorization->applyScope($this, $action, $resource, ...$optionalArgs); - } - - /** - * @inheritDoc - */ - public function getOriginalData(): \ArrayAccess|array - { - return $this; - } - - /** - * Setter to be used by the middleware. - */ - public function setAuthorization(AuthorizationServiceInterface $service): static - { - $this->authorization = $service; - - return $this; - } - - // Other methods - } - -Now that our user implements the necessary interface, lets update our middleware -setup:: - - // In your Application::middleware() method; - - // Authorization - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'identityDecorator' => function ($auth, $user) { - return $user->setAuthorization($auth); - } - ])); - -You no longer have to change any existing typehints, and can start using -authorization policies anywhere you have access to your user. - -If you also use the Authentication plugin make sure to implement both interfaces.:: - - use Authorization\IdentityInterface as AuthorizationIdentity; - use Authentication\IdentityInterface as AuthenticationIdentity; - - class User extends Entity implements AuthorizationIdentity, AuthenticationIdentity - { - ... - - /** - * Authentication\IdentityInterface method - */ - public function getIdentifier(): int|string|null - { - return $this->id; - } - - ... - } - -Ensuring Authorization is Applied ---------------------------------- - -By default the ``AuthorizationMiddleware`` will ensure that each request -containing an ``identity`` also has authorization checked/bypassed. If -authorization is not checked an ``AuthorizationRequiredException`` will be raised. -This exception is raised **after** your other middleware/controller actions are -complete, so you cannot rely on it to prevent unauthorized access, however it is -a helpful aid during development/testing. You can disable this behavior via an -option:: - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'requireAuthorizationCheck' => false - ])); - -Handling Unauthorized Requests ------------------------------- - -By default authorization exceptions thrown by the application are rethrown by the middleware. -You can configure handlers for unauthorized requests and perform custom action, e.g. -redirect the user to the login page. - -The built-in handlers are: - -* ``Exception`` - this handler will rethrow the exception, this is a default - behavior of the middleware. -* ``Redirect`` - this handler will redirect the request to the provided URL. -* ``CakeRedirect`` - redirect handler with support for CakePHP Router. - -Both redirect handlers share the same configuration options: - -* ``url`` - URL to redirect to (``CakeRedirect`` supports CakePHP Router syntax). -* ``exceptions`` - a list of exception classes that should be redirected. By - default only ``MissingIdentityException`` is redirected. -* ``queryParam`` - the accessed request URL will be attached to the redirect URL - query parameter (``redirect`` by default). -* ``statusCode`` - HTTP status code of a redirect, ``302`` by default. -* ``allowedRedirectExtensions`` - an array of allowed file extensions for redirecting. - If the request URL has a file extension that is not in this list, the redirect will not - happen and the exception will be rethrown. Can also be a boolean to toggle on/off - redirects entirely. This is useful to prevent unauthorized access to API based - responses, that should not be redirecting in any case. `true` by default and not enabled then. - -For example:: - - use Authorization\Exception\MissingIdentityException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'Authorization.Redirect', - 'url' => '/pages/unauthorized', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - OtherException::class, - ], - 'allowedRedirectExtensions' => ['csv', 'pdf'], - ], - ])); - -All handlers get the thrown exception object given as a parameter. -This exception will always be an instance of ``Authorization\Exception\Exception``. -In this example the ``Authorization.Redirect`` handler just gives you the option to -specify which exceptions you want to listen to. - -So in this example where we use the ``Authorization.Redirect`` handler we can -add other ``Authorization\Exception\Exception`` based exceptions to the -``execeptions`` array if we want to handle them gracefully:: - - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class, - ], - -See the `RedirectHandler source `__ - -Configuration options are passed to the handler's ``handle()`` method as the -last parameter. - -Add a flash message after being redirected by an unauthorized request ----------------------------------------------------------------------- - -Currently there is no straightforward way to add a flash message to the unauthorized redirect. - -Therefore you need to create your own Handler which adds the flash message (or any -other logic you want to happen on redirect) - -How to create a custom UnauthorizedHandler -------------------------------------------- - -#. Create this file ``src/Middleware/UnauthorizedHandler/CustomRedirectHandler.php``:: - - getFlash()->error( 'You are not authorized to access that location' ); - return $response; - } - } - -#. Tell the AuthorizationMiddleware that it should use your new custom Handler:: - - // in your src/Application.php - - use Authorization\Exception\MissingIdentityException; - use Authorization\Exception\ForbiddenException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'CustomRedirect', // <--- see here - 'url' => '/users/login', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class, - ], - 'custom_param' => true, - ], - ])); - -As you can see you still have the same config parameters as if we are using ``Authorization.Redirect`` as a className. - -This is, because we extend our handler based on the RedirectHandler present in the plugin. Therefore all that functionality is present + our own funtionality in the ``handle()`` function. - -The ``custom_param`` appears in the ``$options`` array given to you in the ``handle()`` function inside your ``CustomRedirectHandler`` if you wish to add some more config parameters to your functionality. - -You can look at `CakeRedirectHandler `__ or `RedirectHandler `__ -how such a Handler can/should look like. diff --git a/docs/en/policies.md b/docs/en/policies.md new file mode 100644 index 00000000..302355d3 --- /dev/null +++ b/docs/en/policies.md @@ -0,0 +1,159 @@ +# Policies + +Policies are classes that resolve permissions for a given object. You can +create policies for any class in your application that needs authorization +checks. + +## Creating Policies + +Create policies in `src/Policy`. They do not need to extend a base class or +implement a shared interface. Application classes are resolved to matching +policy classes by a policy resolver. See [Policy Resolvers](/policy-resolvers) +for the available approaches. + +Most applications use the `Policy` suffix in `src/Policy`. For example, an +article entity policy in `src/Policy/ArticlePolicy.php`: + +```php +id === $article->user_id; +} +``` + +Policy methods must return `true` or a `Result` object to indicate success. Any +other return value is treated as failure. + +Policy methods receive `null` for `$user` when evaluating anonymous users. If +you want anonymous users to fail automatically, typehint +`IdentityInterface`. + +## Policy Result Objects + +Besides booleans, policy methods may return `Authorization\Policy\Result` for +additional context: + +```php +use Authorization\Policy\Result; + +public function canUpdate(IdentityInterface $user, Article $article): Result +{ + if ($user->id === $article->user_id) { + return new Result(true); + } + + return new Result(false, 'not-owner'); +} +``` + +Any return value other than `true` or a `ResultInterface` instance is +considered a failure. + +## Policy Scopes + +Policies can also define scopes that modify another object with authorization +conditions. A common use case is restricting list views to the current user: + +```php +namespace App\Policy; + +class ArticlesTablePolicy +{ + public function scopeIndex($user, $query) + { + return $query->where(['Articles.user_id' => $user->getIdentifier()]); + } +} +``` + +## Policy Pre-conditions + +Use `BeforePolicyInterface` when you want common checks across every operation +in a policy: + +```php +namespace App\Policy; + +use Authorization\IdentityInterface; +use Authorization\Policy\BeforePolicyInterface; +use Authorization\Policy\ResultInterface; + +class ArticlesPolicy implements BeforePolicyInterface +{ + public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null + { + if ($identity->getOriginalData()->is_admin) { + return true; + } + + return null; + } +} +``` + +The `before()` hook can return: + +- `true` to allow the action immediately. +- `false` to deny the action immediately. +- `null` to let the normal authorization method run. + +## Scope Pre-conditions + +Scopes support similar pre-conditions through `BeforeScopeInterface`: + +```php +namespace App\Policy; + +use Authorization\Policy\BeforeScopeInterface; + +class ArticlesTablePolicy implements BeforeScopeInterface +{ + public function beforeScope($user, $query, $action) + { + if ($user->getOriginalData()->is_trial_user) { + return $query->where(['Articles.is_paid_only' => false]); + } + + return null; + } +} +``` + +Returning a modified resource short-circuits the normal scope method. Returning +`null` allows the scope method to run normally. + +## Applying Policies + +See [Applying Policy Scopes](/component#applying-policy-scopes) for controller +examples. diff --git a/docs/en/policies.rst b/docs/en/policies.rst deleted file mode 100644 index eac94e9d..00000000 --- a/docs/en/policies.rst +++ /dev/null @@ -1,167 +0,0 @@ -Policies -######## - -Policies are classes that resolve permissions for a given object. You can create -policies for any class in your application that you wish to apply permissions -checks to. - -Creating Policies -================= - -You can create policies in your ``src/Policy`` directory. Policy classes don't -have a common base class or interface they are expected to implement. -Application classes are then 'resolved' to a matching policy class. See the -:doc:`policy-resolvers` section for how policies can be resolved. - -Generally you'll want to put your policies in **src/Policy** and use the -``Policy`` class suffix. For now we'll create a policy class for the `Article` -entity in our application. In **src/Policy/ArticlePolicy.php** put the -following content:: - - id === $article->user_id; - } - -Policy methods must return ``true`` or a ``Result`` objects to indicate success. -All other values will be interpreted as failure. - -Policy methods will receive ``null`` for the ``$user`` parameter when handling -unauthenticated users. If you want to automatically fail policy methods for -anonymous users you can use the ``IdentityInterface`` typehint. - -.. _policy-result-objects: - -Policy Result Objects -===================== - -In addition to booleans, policy methods can return a ``Result`` object. -``Result`` objects allow more context to be provided on why the policy -passed/failed:: - - use Authorization\Policy\Result; - - public function canUpdate(IdentityInterface $user, Article $article): Result - { - if ($user->id === $article->user_id) { - return new Result(true); - } - // Results let you define a 'reason' for the failure. - return new Result(false, 'not-owner'); - } - -Any return value that is not ``true`` or a ``ResultInterface`` object will be -considered a failure. - -Policy Scopes -============= - -In addition to policies being able to define pass/fail authorization checks, -they can also define 'scopes'. Scope methods allow you to modify another object -applying authorization conditions. A perfect use case for this is restricting -a list view to the current user:: - - namespace App\Policy; - - class ArticlesTablePolicy - { - public function scopeIndex($user, $query) - { - return $query->where(['Articles.user_id' => $user->getIdentifier()]); - } - } - -Policy Pre-conditions -===================== - -In some policies you may wish to apply common checks across all operations in -a policy. This is useful when you need to deny all actions to the provided -resource. To use pre-conditions you need to implement the ``BeforePolicyInterface`` -in your policy:: - - namespace App\Policy; - - use Authorization\IdentityInterface; - use Authorization\Policy\BeforePolicyInterface; - use Authorization\Policy\ResultInterface; - - class ArticlesPolicy implements BeforePolicyInterface - { - public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null - { - if ($identity->getOriginalData()->is_admin) { - return true; - } - // fall through - } - } - -Before hooks are expected to return one of three values: - -- ``true`` The user is allowed to proceed with the action. -- ``false`` The user is not allowed to proceed with the action. -- ``null`` The before hook did not make a decision, and the authorization method - will be invoked. - -Scope Pre-conditions -==================== - -Like policies, scopes can also define pre-conditions. These are useful when you -want to apply common conditions to all scopes in a policy. To use pre-conditions -on scopes you need to implement the ``BeforeScopeInterface`` in your scope policy:: - - namespace App\Policy; - - use Authorization\Policy\BeforeScopeInterface; - - class ArticlesTablePolicy implements BeforeScopeInterface - { - public function beforeScope($user, $query, $action) - { - if ($user->getOriginalData()->is_trial_user) { - return $query->where(['Articles.is_paid_only' => false]); - } - // fall through - } - } - -Before scope hooks are expected to return the modified resource object, or if -``null`` is returned then the scope method will be invoked as normal. - - -Applying Policies ------------------ - -See :ref:`applying-policy-scopes` for how to apply policies in your controller -actions. diff --git a/docs/en/policy-resolvers.md b/docs/en/policy-resolvers.md new file mode 100644 index 00000000..812faaa9 --- /dev/null +++ b/docs/en/policy-resolvers.md @@ -0,0 +1,144 @@ +# Policy Resolvers + +Policy resolvers map resource objects to their corresponding policy classes. +The plugin includes a few resolvers out of the box, and you can implement your +own by implementing `Authorization\Policy\ResolverInterface`. + +Built-in resolvers: + +- `MapResolver` maps resource class names to policy class names, policy + objects, or callables. +- `OrmResolver` applies convention-based policy resolution for common ORM + objects. +- `ResolverCollection` combines multiple resolvers and checks them in order. + +## Using MapResolver + +`MapResolver` lets you map resource classes to policy classes, policy +instances, or factory callables: + +```php +use Authorization\Policy\MapResolver; + +$mapResolver = new MapResolver(); + +// Map a resource class to a policy classname +$mapResolver->map(Article::class, ArticlePolicy::class); + +// Map a resource class to a policy instance +$mapResolver->map(Article::class, new ArticlePolicy()); + +// Map a resource class to a factory callable +$mapResolver->map(Article::class, function ($resource, $mapResolver) { + // Return a policy object. +}); +``` + +## Using OrmResolver + +`OrmResolver` is the convention-based resolver for CakePHP ORM objects. It +assumes: + +1. Policies live in `App\Policy`. +2. Policy classes use the `Policy` suffix. + +`OrmResolver` can resolve policies for: + +- Entities, using the entity class name. +- Tables, using the table class name. +- Queries, using the class returned by `repository()`. + +These rules are applied: + +1. The resource class name is converted into a policy class name. For example, + `App\Model\Entity\Bookmark` maps to `App\Policy\BookmarkPolicy`. +2. Plugin resources first check for an application override such as + `App\Policy\Bookmarks\BookmarkPolicy` for + `Bookmarks\Model\Entity\Bookmark`. +3. If no application override exists, the plugin policy is checked, such as + `Bookmarks\Policy\BookmarkPolicy`. + +For tables, `App\Model\Table\ArticlesTable` maps to +`App\Policy\ArticlesTablePolicy`. Queries use their repository table to derive +the policy class. + +Customize `OrmResolver` through its constructor: + +```php +use Authorization\Policy\OrmResolver; + +$appNamespace = 'App'; +$overrides = [ + 'Blog' => 'Cms', +]; + +$resolver = new OrmResolver($appNamespace, $overrides); +``` + +## Using Multiple Resolvers + +`ResolverCollection` combines resolvers and checks them sequentially: + +```php +use Authorization\Policy\ResolverCollection; +use Authorization\Policy\MapResolver; +use Authorization\Policy\OrmResolver; + +$ormResolver = new OrmResolver(); +$mapResolver = new MapResolver(); + +$resolver = new ResolverCollection([$mapResolver, $ormResolver]); +``` + +This lets you check the explicit map first and fall back to ORM conventions. + +## Creating a Resolver + +Implement `Authorization\Policy\ResolverInterface` and define +`getPolicy($resource)` when you need custom resolution logic. + +One example is bridging the Authorization plugin with legacy controller-based +authorization during migration from `AuthComponent`. First create a catch-all +policy: + +```php +// in src/Policy/ControllerHookPolicy.php +namespace App\Policy; + +class ControllerHookPolicy +{ + public function __call(string $name, array $arguments) + { + /** @var ?\Authorization\Identity $user */ + [$user, $controller] = $arguments; + + return $controller->isAuthorized($user?->getOriginalData()); + } +} +``` + +Then create a resolver for controllers: + +```php +// in src/Policy/ControllerResolver.php +namespace App\Policy; + +use Authorization\Policy\Exception\MissingPolicyException; +use Authorization\Policy\ResolverInterface; +use Cake\Controller\Controller; + +class ControllerResolver implements ResolverInterface +{ + public function getPolicy($resource) + { + if ($resource instanceof Controller) { + return new ControllerHookPolicy(); + } + + throw new MissingPolicyException([get_class($resource)]); + } +} +``` + +You can register this resolver directly or combine it with others through +`ResolverCollection`. diff --git a/docs/en/policy-resolvers.rst b/docs/en/policy-resolvers.rst deleted file mode 100644 index ac72e084..00000000 --- a/docs/en/policy-resolvers.rst +++ /dev/null @@ -1,148 +0,0 @@ -Policy Resolvers -################ - -Mapping resource objects to their respective policy classes is a behavior -handled by a policy resolver. We provide a few resolvers to get you started, but -you can create your own resolver by implementing the -``Authorization\Policy\ResolverInterface``. The built-in resolvers are: - -* ``MapResolver`` allows you to map resource names to their policy class names, or - to objects and callables. -* ``OrmResolver`` applies conventions based policy resolution for common ORM - objects. -* ``ResolverCollection`` allows you to aggregate multiple resolvers together, - searching them sequentially. - -Using MapResolver -================= - -``MapResolver`` lets you map resource class names to policy classnames, policy -objects, or factory callables:: - - use Authorization\Policy\MapResolver; - - $mapResolver = new MapResolver(); - - // Map a resource class to a policy classname - $mapResolver->map(Article::class, ArticlePolicy::class); - - // Map a resource class to a policy instance. - $mapResolver->map(Article::class, new ArticlePolicy()); - - // Map a resource class to a factory function - $mapResolver->map(Article::class, function ($resource, $mapResolver) { - // Return a policy object. - }); - -Using OrmResolver -================= - -The ``OrmResolver`` is a conventions based policy resolver for CakePHP's ORM. The -OrmResolver applies the following conventions: - -#. Policies live in ``App\Policy`` -#. Policy classes end with the ``Policy`` class suffix. - -The OrmResolver can resolve policies for the following object types: - -* Entities - Using the entity classname. -* Tables - Using the table classname. -* Queries - Using the return of the query's ``repository()`` to get a classname. - -In all cases the following rules are applied: - -#. The resource classname is used to generate a policy class name. e.g - ``App\Model\Entity\Bookmark`` will map to ``App\Policy\BookmarkPolicy`` -#. Plugin resources will first check for an application policy e.g - ``App\Policy\Bookmarks\BookmarkPolicy`` for ``Bookmarks\Model\Entity\Bookmark``. -#. If no application override policy can be found, a plugin policy will be - checked. e.g. ``Bookmarks\Policy\BookmarkPolicy`` - -For table objects the class name tranformation would result in -``App\Model\Table\ArticlesTable`` mapping to ``App\Policy\ArticlesTablePolicy``. -Query objects will have their ``repository()`` method called, and a policy will be -generated based on the resulting table class. - -The OrmResolver supports customization through its constructor:: - - use Authorization\Policy\OrmResolver; - - // Change when using a custom application namespace. - $appNamespace = 'App'; - - // Map policies in one namespace to another. - // Here we have mapped policies for classes in the ``Blog`` namespace to be - // found in the ``Cms`` namespace. - $overrides = [ - 'Blog' => 'Cms', - ]; - $resolver = new OrmResolver($appNamespace, $overrides); - -Using Multiple Resolvers -======================== - -``ResolverCollection`` allows you to aggregate multiple resolvers together:: - - use Authorization\Policy\ResolverCollection; - use Authorization\Policy\MapResolver; - use Authorization\Policy\OrmResolver; - - $ormResolver = new OrmResolver(); - $mapResolver = new MapResolver(); - - // Check the map resolver, and fallback to the orm resolver if - // a resource is not explicitly mapped. - $resolver = new ResolverCollection([$mapResolver, $ormResolver]); - -Creating a Resolver -=================== - -You can implement your own resolver by implementing the -``Authorization\Policy\ResolverInterface`` which requires defining the -``getPolicy($resource)`` method. - -An example scenario where a custom resolver would be useful is when bridging the -authorization plugin with controller based access controls when migrating from -the ``AuthComponent``. First we need to create a catch-all policy that will call -our controller method:: - - // in src/Policy/ControllerHookPolicy.php - namespace App\Policy; - - class ControllerHookPolicy - { - public function __call(string $name, array $arguments) - { - /** @var ?\Authorization\Identity $user */ - [$user, $controller] = $arguments; - - return $controller->isAuthorized($user?->getOriginalData()); - } - } - -Our policy class uses ``__call`` so that it can handle all of the actions in our -controller. Our policy calls the ``isAuthorized()`` method on our controller -giving us backwards compatibility with our existing logic. Next, we'll create -a policy resolver that will resolve controllers to our custom policy:: - - // in src/Policy/ControllerResolver.php - namespace App\Policy; - - use Authorization\Policy\ResolverInterface; - use Authorization\Policy\Exception\MissingPolicyException; - use Cake\Controller\Controller; - - class ControllerResolver implements ResolverInterface - { - public function getPolicy($resource) - { - if ($resource instanceof Controller) { - return new ControllerHookPolicy(); - } - - throw new MissingPolicyException([get_class($resource)]); - } - } - -With our policy and resolver created, we can add the resolver to our application -directly or combine it with other resolvers using the ``ResolverCollection``. diff --git a/docs/en/request-authorization-middleware.md b/docs/en/request-authorization-middleware.md new file mode 100644 index 00000000..a48cedc0 --- /dev/null +++ b/docs/en/request-authorization-middleware.md @@ -0,0 +1,98 @@ +# Request Authorization Middleware + +This middleware is useful when you want to authorize requests themselves, for +example each controller and action, against a role-based access system or some +other process that controls access to routes. + +Add it after `AuthorizationMiddleware`, `AuthenticationMiddleware`, and +`RoutingMiddleware`. + +The authorization logic for the request is implemented in a request policy. You +can keep all request-level logic there or delegate to an ACL or RBAC layer. + +## Using It + +Create a policy for request objects in `src/Policy/RequestPolicy.php`: + +```php +namespace App\Policy; + +use Authorization\Policy\RequestPolicyInterface; +use Authorization\Policy\ResultInterface; +use Cake\Http\ServerRequest; + +class RequestPolicy implements RequestPolicyInterface +{ + public function canAccess($identity, ServerRequest $request): bool|ResultInterface + { + if ($request->getParam('controller') === 'Articles' + && $request->getParam('action') === 'index' + ) { + return true; + } + + return false; + } +} +``` + +Map the request class to the policy inside +`Application::getAuthorizationService()`: + +```php +use App\Policy\RequestPolicy; +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Middleware\RequestAuthorizationMiddleware; +use Authorization\Policy\MapResolver; +use Cake\Http\ServerRequest; +use Psr\Http\Message\ServerRequestInterface; + +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $mapResolver = new MapResolver(); + $mapResolver->map(ServerRequest::class, RequestPolicy::class); + + return new AuthorizationService($mapResolver); +} +``` + +Then load `RequestAuthorizationMiddleware` after `AuthorizationMiddleware`: + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new AuthorizationMiddleware($this)); + $middlewareQueue->add(new RequestAuthorizationMiddleware()); + + return $middlewareQueue; +} +``` + +## Controller Usage + +If fallback routing is enabled, asset 404s and missing controllers can also hit +`RequestAuthorizationMiddleware`. In that case you can attach it only to your +application controller: + +```php +public function initialize(): void +{ + parent::initialize(); + + $this->middleware(function ($request, $handler): ResponseInterface { + $config = [ + 'unauthorizedHandler' => [ + // ... + ], + ]; + $middleware = new RequestAuthorizationMiddleware($config); + + return $middleware->process($request, $handler); + }); +} +``` + +If you do this, settings such as `DebugKit.ignoreAuthorization` may also need +to be enabled. diff --git a/docs/en/request-authorization-middleware.rst b/docs/en/request-authorization-middleware.rst deleted file mode 100644 index aa3663bb..00000000 --- a/docs/en/request-authorization-middleware.rst +++ /dev/null @@ -1,106 +0,0 @@ -Request Authorization Middleware -################################ - -This middleware is useful when you want to authorize your requests, for example -each controller and action, against a role based access system or any other kind -of authorization process that controls access to certain actions. - -This **must** be added after the Authorization, Authentication and -RoutingMiddleware in the Middleware Queue! - -The logic of handling the request authorization will be implemented in the -request policy. You can add all your logic there or just pass the information -from the request into an ACL or RBAC implementation. - -Using it -======== - -Create a policy for handling the request object. The plugin ships with an -interface we can implement. Start by creating **src/Policy/RequestPolicy.php** -and add:: - - namespace App\Policy; - - use Authorization\Policy\RequestPolicyInterface; - use Cake\Http\ServerRequest; - use Authorization\Policy\ResultInterface; - - class RequestPolicy implements RequestPolicyInterface - { - /** - * Method to check if the request can be accessed - * - * @param \Authorization\IdentityInterface|null $identity Identity - * @param \Cake\Http\ServerRequest $request Server Request - * @return \Authorization\Policy\ResultInterface|bool - */ - public function canAccess($identity, ServerRequest $request): bool|ResultInterface - { - if ($request->getParam('controller') === 'Articles' - && $request->getParam('action') === 'index' - ) { - return true; - } - - return false; - } - } - -Next, map the request class to the policy inside -``Application::getAuthorizationService()``, in **src/Application.php** :: - - use App\Policy\RequestPolicy; - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Middleware\RequestAuthorizationMiddleware; - use Authorization\Policy\MapResolver; - use Cake\Http\ServerRequest; - - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface { - $mapResolver = new MapResolver(); - $mapResolver->map(ServerRequest::class, RequestPolicy::class); - return new AuthorizationService($mapResolver); - } - -Ensure you're loading the RequestAuthorizationMiddleware **after** the -AuthorizationMiddleware:: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { - // other middleware... - // $middlewareQueue->add(new AuthenticationMiddleware($this)); - - // Add authorization (after authentication if you are using that plugin too). - $middlewareQueue->add(new AuthorizationMiddleware($this)); - $middlewareQueue->add(new RequestAuthorizationMiddleware()); - - return $middlewareQueue; - } - -Controller Usage -================ - -When having fallback routing activated, all asset based 404s as well as not existing controllers would also -trigger the RequestAuthorizationMiddleware. -In this case, it is possible to use this middleware only for your (App)controller:: - - // src/Controller/AppController.php - public function initialize(): void - { - parent::initialize(); - - $this->middleware(function ($request, $handler): ResponseInterface { - $config = [ - 'unauthorizedHandler' => [ - ... - ], - ]; - $middleware = new RequestAuthorizationMiddleware($config); - - return $middleware->process($request, $handler); - }); - } - -Also note, that in this case you will e.g. need ``'DebugKit.ignoreAuthorization'`` set to ``true``. diff --git a/docs/es/conf.py b/docs/es/conf.py deleted file mode 100644 index 4691ece6..00000000 --- a/docs/es/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'es' diff --git a/docs/es/contents.rst b/docs/es/contents.rst deleted file mode 100644 index 0938d2e8..00000000 --- a/docs/es/contents.rst +++ /dev/null @@ -1,15 +0,0 @@ -Contents -######## - -.. toctree:: - :maxdepth: 2 - :caption: CakePHP Authorization - - /index - /policies -.. /policy-resolvers -.. /middleware -.. /checking-authorization -.. /component -.. /request-authorization-middleware -.. /2-0-migration-guie diff --git a/docs/es/index.md b/docs/es/index.md new file mode 100644 index 00000000..5b97caef --- /dev/null +++ b/docs/es/index.md @@ -0,0 +1,56 @@ +# Inicio Rápido + +## Instalación + +Instale el plugin con [Composer](https://getcomposer.org/) desde el directorio +raíz de su proyecto CakePHP: + +```bash +php composer.phar require "cakephp/authorization:^3.0" +``` + +Cargue el plugin en `src/Application.php`: + +```php +$this->addPlugin('Authorization'); +``` + +## Empezando + +Authorization se integra como middleware y, opcionalmente, como componente. + +```php +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Psr\Http\Message\ServerRequestInterface; +``` + +```php +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +``` + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this)); +``` + +```php +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); +} +``` + +Después, cargue el componente: + +```php +$this->loadComponent('Authorization.Authorization'); +``` + +## Otras lecturas + +- [Policy](./policies) diff --git a/docs/es/index.rst b/docs/es/index.rst deleted file mode 100644 index d762a748..00000000 --- a/docs/es/index.rst +++ /dev/null @@ -1,88 +0,0 @@ -Inicio Rápido -############# - -Instalación -=========== - -Instale el plugin con `composer `__ desde el directorio -ROOT de su proyecto CakePHP (donde se encuentra el archivo **composer.json**) - -.. code-block:: shell - - php composer.phar require "cakephp/authorization:^2.0" - -Cargue el complemento agregando la siguiente declaración en el archivo -``src/Application.php`` de su proyecto:: - - $this->addPlugin('Authorization'); - -Empezando -========= - -El plugin Authorization se integra en su aplicación como una capa middleware y, opcionalmente, -un componente para facilitar la verificación de la autorización. Primero, apliquemos el middleware. -En **src/Application.php** agregue lo siguiente a las importaciones de la clase:: - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ResponseInterface; - use Psr\Http\Message\ServerRequestInterface; - -Agregue la ``AuthorizationServiceProviderInterface`` a las interfaces -implementadas en su aplicación:: - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - -Luego agregue lo siguiente a su método ``middleware()``:: - - // Add authorization (after authentication if you are using that plugin too). - $middleware->add(new AuthorizationMiddleware($this)); - -``AuthorizationMiddleware`` llamará a un método hook en su aplicación cuando comience -a manejar la request. Este método hook permite a su aplicación definir el ``AuthorizationService`` -que quiere usar. Agregue el siguiente método a su **src/Application.php**:: - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - -Esto configura :doc:`/policy-resolvers` que hará coincidir las entidades ORM -con sus clases policy. - -A continuación, agregue el ``AuthorizationComponent`` a ``AppController``. En -**src/Controller/AppController.php** agregue lo siguiente al método ``initialize()``:: - - $this->loadComponent('Authorization.Authorization'); - -Al cargar :doc:`/component`, podremos verificar la autorización -por acción más fácilmente. Por ejemplo, podemos hacer:: - - public function edit($id = null) - { - $article = $this->Article->get($id); - $this->Authorization->authorize($article, 'update'); - - // Rest of action - } - -Al llamar ``authorize`` podemos usar nuestro :doc:`/policies` para hacer cumplir -las reglas de control de acceso de nuestra aplicación. Puede verificar los permisos -en cualquier lugar utilizando :doc:`identity stored in the request `. - - -Otras lecturas -============== - -* :doc:`/policies` - -.. * :doc:`/policy-resolvers` -.. * :doc:`/middleware` -.. * :doc:`/component` -.. * :doc:`/checking-authorization` -.. * :doc:`/request-authorization-middleware` diff --git a/docs/es/policies.md b/docs/es/policies.md new file mode 100644 index 00000000..e3b09d57 --- /dev/null +++ b/docs/es/policies.md @@ -0,0 +1,64 @@ +# Policy + +Policy son clases que resuelven los permisos para un objeto determinado. + +## Creando una Policy + +Puede crear una policy en `src/Policy`. + +```php +id == $article->user_id; +} +``` + +## Objetos Policy Result + +```php +use Authorization\Policy\Result; + +public function canUpdate(IdentityInterface $user, Article $article) +{ + if ($user->id == $article->user_id) { + return new Result(true); + } + + return new Result(false, 'not-owner'); +} +``` + +## Alcances Policy + +```php +namespace App\Policy; + +class ArticlesTablePolicy +{ + public function scopeIndex($user, $query) + { + return $query->where(['Articles.user_id' => $user->getIdentifier()]); + } +} +``` diff --git a/docs/es/policies.rst b/docs/es/policies.rst deleted file mode 100644 index 7d31d7d2..00000000 --- a/docs/es/policies.rst +++ /dev/null @@ -1,134 +0,0 @@ -Policy -###### - -Policy son clases que resuelven los permisos para un objeto determinado. Puede -crear una policy para cualquier clase de su aplicación a la que desee aplicar -comprobaciones de permisos. - -Creando una Policy -================== - -Puede crear una policy en su directorio ``src/Policy``. Las clases policy -no tienen una clase o interfaz base común que se espera implementar. -Las clases de la aplicación se 'resuelven' en una clase policy coincidente. -Consulte la sección :doc:`policy-resolvers` para saber cómo se pueden resolver las policy. - -Por lo general, querrá poner sus policy en **src/Policy** y usar el sufijo de clase -``Policy``. Por ahora crearemos una clase policy para la entidad `Article` en nuestra -aplicación. En **src/Policy/ArticlePolicy.php** ponga el siguiente contenido:: - - id == $article->user_id; - } - -Los métodos policy deben devolver objetos ``true`` o un objeto ``Result`` para indicar el éxito ('pass'). -Todos los demás valores se interpretarán como un fallo ('fail'). - -Los métodos policy recibirán un ``null`` en el parámetro ``$user`` al manejar usuarios -no autenticados. Si desea que los métodos policy automáticamente den 'fail' para usuarios -anónimos, puede usar la sugerencia de tipo ``IdentityInterface``. - -.. _policy-result-objects: - -Objetos Policy Result -===================== - -Además de los valores booleanos, los métodos policy pueden devolver un objeto ``Result``. -Los objetos ``Result`` permiten que se proporcione más contexto sobre por qué el método -policy dió 'pass'/'fail':: - - use Authorization\Policy\Result; - - public function canUpdate(IdentityInterface $user, Article $article) - { - if ($user->id == $article->user_id) { - return new Result(true); - } - // Results let you define a 'reason' for the failure. - return new Result(false, 'not-owner'); - } - -Cualquier valor devuelto que no sea ``true`` o un objeto ``ResultInterface`` -se considerará un 'fail'. - -Alcances Policy ---------------- - -Además de que las policy pueden definir verificaciones de autorización 'pass'/'fail', -también pueden definir 'alcances'. Los métodos de alcance le permiten modificar otro -objeto aplicando condiciones de autorización. Un caso de uso perfecto para esto es -restringir una list view al usuario actual:: - - namespace App\Policy; - - class ArticlesTablePolicy - { - public function scopeIndex($user, $query) - { - return $query->where(['Articles.user_id' => $user->getIdentifier()]); - } - } - -Condiciones Previas de la Policy --------------------------------- - -En algunas policy, es posible que desee aplicar comprobaciones comunes en todas las -operaciones de una policy. Esto es útil cuando necesita denegar todas las acciones al -recurso proporcionado. Para utilizar las condiciones previas, debe implementar ``BeforePolicyInterface`` -en su policy:: - - namespace App\Policy; - - use Authorization\IdentityInterface; - use Authorization\Policy\BeforePolicyInterface; - use Authorization\Policy\ResultInterface; - - class ArticlesPolicy implements BeforePolicyInterface - { - public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null - { - if ($identity->getOriginalData()->is_admin) { - return true; - } - // fall through - } - } - -De los hooks 'before' se espera que devuelvan uno de tres valores: - -- ``true`` El usuario puede proceder con la acción. -- ``false`` El usuario no puede proceder con la acción. -- ``null`` El hook 'before' no tomó una decisión y se invocará - el método de autorización. diff --git a/docs/fr/2-0-migration-guide.md b/docs/fr/2-0-migration-guide.md new file mode 100644 index 00000000..6450aca6 --- /dev/null +++ b/docs/fr/2-0-migration-guide.md @@ -0,0 +1,12 @@ +# Guide de Migration vers 2.0 + +Authorization 2.0 apporte de nouvelles fonctionnalités ainsi que quelques +ruptures de compatibilité. + +## Ruptures de Compatibilité + +`IdentityInterface` a reçu des typehints. Si votre application implémente cette +interface, vous devez mettre à jour vos signatures. + +`IdentityInterface` ajoute aussi `canResult()`, qui renvoie toujours un +`ResultInterface`, tandis que `can()` renvoie désormais toujours un booléen. diff --git a/docs/fr/2-0-migration-guide.rst b/docs/fr/2-0-migration-guide.rst deleted file mode 100644 index 2123d641..00000000 --- a/docs/fr/2-0-migration-guide.rst +++ /dev/null @@ -1,20 +0,0 @@ -Guide de Migration vers 2.0 -########################### - -Authorization 2.0 contient de nouvelles fonctionnalités et quelques changements -entraînant une rupture de compatibilité. - -Ruptures de Compatibilité -========================= - -Le typehinting a été ajouté dans ``IdentityInterface``. Si vous avez implémenté -``IdentityInterface``, il faudra que vous mettiez à jour l'implémentation de -votre application pour qu'elle reflète les nouveaux typehints. - -En plus des typehints, une nouvelle méthode ``canResult()`` a été ajoutée à -``IdentityInterface``. Cette méthode renvoie toujours un objet -``ResultInterface`` tandis que ``can()`` renvoie toujours un booléen. Dans 1.x, -la méthode ``can()`` renvoyait un booléen ou un ``ResultInterface`` selon ce que -renvoyait la policy. Cela rendait très difficile de savoir ce que renvoyait -``can()``. Les nouvelles méthodes et le typehint supplémentaire rendent -``IdentityInterface`` plus simple et plus pratique à utiliser. diff --git a/docs/fr/checking-authorization.md b/docs/fr/checking-authorization.md new file mode 100644 index 00000000..e7d51b83 --- /dev/null +++ b/docs/fr/checking-authorization.md @@ -0,0 +1,36 @@ +# Vérifier une Autorisation + +Une fois le [middleware](./middleware) installé et une `identity` ajoutée à la +requête, vous pouvez commencer à vérifier les autorisations. + +L'`identity` peut être transmise à vos modèles, services ou templates afin de +faire des vérifications partout dans l'application. + +## Vérifier l'Autorisation pour une Seule Ressource + +```php +$user = $this->request->getAttribute('identity'); + +if ($user->can('delete', $article)) { + // Effectuer la suppression +} +``` + +Si vos policies renvoient un objet résultat : + +```php +$result = $user->canResult('delete', $article); +if ($result->getStatus()) { + // Procéder à l'effacement +} +``` + +## Appliquer des Conditions de Portée + +```php +$user = $this->request->getAttribute('identity'); +$query = $user->applyScope('index', $query); +``` + +Dans les contrôleurs, le [component](./component) permet d'automatiser les +vérifications qui doivent lever une exception en cas d'échec. diff --git a/docs/fr/checking-authorization.rst b/docs/fr/checking-authorization.rst deleted file mode 100644 index 28acf9c9..00000000 --- a/docs/fr/checking-authorization.rst +++ /dev/null @@ -1,58 +0,0 @@ -Vérifier une Autorisation -######################### - -Une fois que vous avez appliqué le :doc:`/middleware` à votre application et -ajouté une ``identity`` à votre requête, vous pouvez commencer à vérifier les -autorisations. Le middleware enveloppe l'\ ``identity`` dans chaque requête par -un ``IdentityDecorator`` qui ajoute les méthodes liées à l'autorisation. - -Vous pouvez passer l'\ ``identity`` à vos modèles, services ou templates, ce qui -vous permet de vérifier facilement l'autorisation depuis n'importe quel endroit -de votre application. Pour savoir comment personnaliser ou remplacer le -décorateur par défaut, consultez la section :ref:`identity-decorator`. - -Vérifier l'Autorisation pour Une Seule Ressource -================================================ - -La méthode ``can`` vous permet de vérifier l'autorisation sur une seule -ressource. Typiquement, ce sera une entity de l'ORM, ou un objet du domaine de -l'application. Vos :doc:`/policies` fournissent la logique pour prendre la -décision d'autorisation:: - - // Obtenir l'identity à partir de la requête - $user = $this->request->getAttribute('identity'); - - // Vérifier l'autorisation sur $article - if ($user->can('delete', $article)) { - // Faire l'opération delete - } - -Si vos policies renvoient des :ref:`policy-result-objects`, pensez à vérifier -leur statut avec ``canResult()`` qui renvoie l'instance Result:: - - // Assuming our policy returns a result. - $result = $user->canResult('delete', $article); - if ($result->getStatus()) { - // Procéder à l'effacement - } - -Appliquer des Conditions de Portée (Scope) -========================================== - -Quand vous avez besoin de vérifier l'autorisation sur une collection d'objets, -telle qu'une requête (*query*) paginée, vous serez souvent amené à ne vouloir -récupérer que les enregistrements auxquel l'utilisateur courant a accès. Le -plugin implémente ce concept sous le nom de 'scopes'. Les policies de scope vous -permettent de limiter (*scope*) une query ou un result set et de renvoyer la -liste modifiée ou l'objet query:: - - // Obtenir l'identity à partir de la requête HTML - $user = $this->request->getAttribute('identity'); - - // Appliquer les conditions de permissions à la query de façon à ne - // retourner que les enregistrements auxquels l'utilisateur actuel a accès. - $query = $user->applyScope('index', $query); - -Dans les actions du controller, vous pouvez utiliser le :doc:`/component` pour -traiter sur-le-champ les vérifications d'autorisation susceptibles de lever une -exception en cas d'échec. diff --git a/docs/fr/component.md b/docs/fr/component.md new file mode 100644 index 00000000..b13cdb29 --- /dev/null +++ b/docs/fr/component.md @@ -0,0 +1,82 @@ +# AuthorizationComponent + +`AuthorizationComponent` fournit des méthodes de convenance pour vérifier les +permissions depuis les contrôleurs. Il s'appuie sur le middleware déjà chargé. + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +## Vérifications Automatiques d'Autorisation + +```php +$this->Authorization->authorizeModel('index', 'add'); +``` + +Pour rendre certaines actions publiques : + +```php +$this->loadComponent('Authorization.Authorization', [ + 'skipAuthorization' => [ + 'login', + ] +]); +``` + +## Vérifier l'Autorisation + +```php +public function edit($id) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article); +} +``` + +Vous pouvez préciser une action de policy : + +```php +$this->Authorization->authorize($article, 'update'); +``` + +Ou récupérer un booléen : + +```php +if ($this->Authorization->can($article, 'update')) { + // Faire quelque chose +} +``` + +## Utilisateurs Anonymes + +`can()` et `authorize()` supportent les utilisateurs anonymes. Les policies +reçoivent `null` comme utilisateur si personne n'est connecté. + +## Appliquer les Scopes des Policies + +```php +$query = $this->Authorization->applyScope($this->Articles->find()); +``` + +Mappez des actions vers d'autres noms de méthodes : + +```php +$this->Authorization->mapActions([ + 'index' => 'list', + 'delete' => 'remove', + 'add' => 'insert', +]); +``` + +## Sauter l'Autorisation + +```php +public function view($id) +{ + $this->Authorization->skipAuthorization(); +} +``` diff --git a/docs/fr/component.rst b/docs/fr/component.rst deleted file mode 100644 index 308c7d41..00000000 --- a/docs/fr/component.rst +++ /dev/null @@ -1,154 +0,0 @@ -AuthorizationComponent -###################### - -Le ``AuthorizationComponent`` propose quelques méthodes de convenance, basées -sur des conventions, pour vérifier les permissions depuis vos controllers. Il -rend transparents l'obtention de l'utilisateur et l'appel aux méthodes ``can`` -ou ``applyScope``. Vous devez utiliser le Middleware pour pouvoir utiliser -l'AuthorizationComponent, donc vérifiez qu'il est effectivement en place. Pour -utiliser le composant, commençons par le charger:: - - // Dans votre AppController - public function initialize() - { - parent::initialize(); - $this->loadComponent('Authorization.Authorization'); - } - -Vérifications Automatiques d'Autorisation -========================================= - -``AuthorizationComponent`` peut être configuré pour appliquer automatiquement -des règles d'autorisation selon la classe du modèle par défaut associé au -controller et le nom de l'action en cours. Dans l'exemple suivant, les -autorisations seront vérifiées automatiquement dans les actions ``index`` et -``add``:: - - $this->Authorization->authorizeModel('index', 'add'); - -Vous pouvez aussi le configurer de sorte que certaines actions sautent (*skip*) -l'autorisation. Cela rendra ces actions **publiques**, accessibles à tous les -utilisateurs. Par défaut, toutes les actions nécessitent une autorisation et une -``AuthorizationRequiredException`` sera levée si la vérification d'autorisation -est activée. - -L'autorisation peut être sautée pour des actions individuelles:: - - $this->loadComponent('Authorization.Authorization', [ - 'skipAuthorization' => [ - 'login', - ] - ]); - -Vérifier l'Autorisation -======================= - -Dans vos actions de controller ou vos méthodes callback, vous pouvez vérifier -l'autorisation en utilisant le composant:: - - // Dans le controller Articles. - public function edit($id) - { - $article = $this->Articles->get($id); - $this->Authorization->authorize($article); - // Le reste de la méthode edit. - } - -Ci-dessus, nous voyons un article autorisé pour l'utilisateur courant. Puisque -nous n'avons pas spécifié l'action à vérifier, c'est le paramètre ``action`` de -la requête qui sera utilisé. Vous pouvez spécifier une action de policy en -second paramètre:: - - // Utilisation d'une méthode de policy autre que l'action en cours dans le controller. - $this->Authorization->authorize($article, 'update'); - -La méthode ``authorize()`` soulèvera une -``Authorization\Exception\ForbiddenException`` si la permission est refusée. -Si vous voulez vérifier l'autorisation et obtenir un booléen comme résultat, -utilisez la méthode ``can()``:: - - if ($this->Authorization->can($article, 'update')) { - // Faire quelque chose sur l'article. - } - -Utilisateurs Anonymes -===================== - -Certaines ressources de votre application peuvent être accessibles aux -utilisateurs non connectés. Savoir si un utilisateur, connecté ou pas, peut ou -non accéder à une ressource relève du domaine des policies. Avec le composant, -vous pouvez vérifier l'autorisation pour les utilisateurs anonymes. Les deux -méthodes ``can()`` et ``authorize()`` supportent les utilisateurs anonymes. Vos -policies peuvent s'attendre à recevoir ``null`` pour le paramètre 'user' si -l'utilisateur n'est pas connecté. - -Appliquer les Périmètres (Scopes) des Policies -============================================== - -Vous pouvez aussi appliquer des scopes de policy avec le composant:: - -$query = $this->Authorization->applyScope($this->Articles->find()); - -Si l'action courante n'a pas d'utilisateur connecté, cela lèvera une -``MissingIdentityException``. - -Si vous voulez mapper des actions vers différentes méthodes d'autorisation, -utilisez l'option ``actionMap``:: - - // Dans la méthode initialize() de votre controller: - $this->Authorization->mapActions([ - 'index' => 'list', - 'delete' => 'remove', - 'add' => 'insert', - ]); - - // ou mapper des actions individuellement. - $this->Authorization - ->mapAction('index','list') - ->mapAction('delete', 'remove') - ->mapAction('add', 'insert'); - -Exemple:: - - //ArticlesController.php - - public function index() - { - $query = $this->Articles->find(); - - // cela appliquera le scope `list` puisque l'appel - // est fait depuis l'action `index` du controller. - $this->Authorization->applyScope($query); - ... - } - - public function delete($id) - { - $article = $this->Articles->get($id); - - // l'autorisation sera accordée selon l'action `remove` de l'entity - // puisque l'appel est fait depuis l'action `delete` du controller. - $this->Authorization->authorize($article); - ... - } - - public function add() - { - // l'autorisation sera accordée selon l'action `insert` du model - // puisque l'appel est fait depuis l'action `add` du controller. - $this->Authorization->authorizeModel(); - ... - } - -Sauter l'Autorisation -===================== - -Vous pouvez sauter l'autorisation depuis l'intérieur d'une action:: - - //ArticlesController.php - - public function view($id) - { - $this->Authorization->skipAuthorization(); - ... - } diff --git a/docs/fr/conf.py b/docs/fr/conf.py deleted file mode 100644 index b02032ef..00000000 --- a/docs/fr/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'fr' diff --git a/docs/fr/contents.rst b/docs/fr/contents.rst deleted file mode 100644 index 38b9ef3c..00000000 --- a/docs/fr/contents.rst +++ /dev/null @@ -1,15 +0,0 @@ -Sommaire -######## - -.. toctree:: - :maxdepth: 2 - :caption: Authorization dans CakePHP - - /index - /policies - /policy-resolvers - /middleware - /checking-authorization - /component - /request-authorization-middleware - /2-0-migration-guide diff --git a/docs/fr/index.md b/docs/fr/index.md new file mode 100644 index 00000000..8de561cf --- /dev/null +++ b/docs/fr/index.md @@ -0,0 +1,109 @@ +# Prise en main rapide + +## Installation + +Installez le plugin avec [Composer](https://getcomposer.org/) depuis le +répertoire racine de votre projet CakePHP, là où se trouve `composer.json` : + +```bash +php composer.phar require "cakephp/authorization:^3.0" +``` + +La version 3 du plugin Authorization est compatible avec CakePHP 5. + +Chargez le plugin dans `src/Application.php` : + +```php +$this->addPlugin('Authorization'); +``` + +## Pour commencer + +Le plugin Authorization s'intègre comme middleware et, de manière optionnelle, +comme composant pour simplifier les vérifications d'autorisation. + +Dans `src/Application.php`, ajoutez les imports suivants : + +```php +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Psr\Http\Message\ServerRequestInterface; +``` + +Ajoutez `AuthorizationServiceProviderInterface` aux interfaces implémentées par +votre classe `Application` : + +```php +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +``` + +Puis adaptez la méthode `middleware()` : + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) + ->add(new AssetMiddleware()) + ->add(new RoutingMiddleware($this)) + ->add(new BodyParserMiddleware()) + ->add(new AuthenticationMiddleware($this)) + ->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; +} +``` + +`AuthorizationMiddleware` doit être ajouté après `AuthenticationMiddleware` +afin que la requête contienne bien une `identity`. + +Ajoutez ensuite la méthode suivante : + +```php +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); +} +``` + +Cela configure les [résolveurs de policy](./policy-resolvers) de base pour +faire correspondre les entités ORM à leurs classes de policy. + +Chargez ensuite `AuthorizationComponent` dans +`src/Controller/AppController.php` : + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +Le [component](./component) permet d'autoriser facilement une ressource dans +une action : + +```php +public function edit($id = null) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article, 'update'); +} +``` + +En appelant `authorize()`, vous utilisez vos [policies](./policies) pour +appliquer les règles d'accès de l'application. Vous pouvez aussi vérifier des +permissions partout où vous avez accès à [l'identity de la requête](./checking-authorization). + +## Pour aller plus loin + +- [Policies](./policies) +- [Résolveurs de Policy](./policy-resolvers) +- [Middleware Authorization](./middleware) +- [AuthorizationComponent](./component) +- [Vérifier une Autorisation](./checking-authorization) +- [Middleware d'Autorisation de Requête](./request-authorization-middleware) diff --git a/docs/fr/index.rst b/docs/fr/index.rst deleted file mode 100644 index 4ab92747..00000000 --- a/docs/fr/index.rst +++ /dev/null @@ -1,113 +0,0 @@ -Prise en main rapide -#################### - -Installation -============ - -Installez le plugin avec `composer `__ depuis le -répertoire racine de votre project CakePHP (là où se trouve le fichier -**composer.json**). - -.. code-block:: shell - - php composer.phar require cakephp/authorization - -La version 3 du Plugin Authorization est compatible avec CakePHP 5. - -Chargez le plugin en ajoutant la ligne suivante dans le fichier -``src/Application.php`` de votre projet:: - - $this->addPlugin('Authorization'); - -Pour commencer -============== - -Le plugin Authorization s'intègre dans votre application en tant que middleware, -et sur option comme composant (*component*) pour faciliter la vérification des -autorisations. Commençons par mettre en place le middleware. Dans -**src/Application.php**, ajoutez les imports de classes suivants:: - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ResponseInterface; - use Psr\Http\Message\ServerRequestInterface; - -Ajoutez ``AuthorizationServiceProviderInterface`` aux interfaces implémentées par -votre classe Application:: - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - -Puis modifiez votre méthode ``middleware()`` pour la faire ressembler à ceci:: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue - { - // Middleware fourni par CakePHP - $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) - ->add(new AssetMiddleware()) - ->add(new RoutingMiddleware($this)) - ->add(new BodyParserMiddleware()) - - // Si vous utilisez Authentication, il doit se trouver *avant* Authorization. - ->add(new AuthenticationMiddleware($this)); - - // Ajoutez le AuthorizationMiddleware *après* les middlewares - // routing, body parser et authentication. - ->add(new AuthorizationMiddleware($this)); - - return $middlewareQueue(); - } - -Le placement du ``AuthorizationMiddleware`` est important. Il faut l'ajouter -*après* le middleware authentication. Cela garantit que la requête ait une -indtance ``identity`` utilisable pour les vérifications d'autorisations. - -Le ``AuthorizationMiddleware`` appellera une méthode crochet (*hook*) de votre -application au démarrage du traitement de la requête. Cette méthode permet à -votre application de définir le ``AuthorizationService`` qu'elle veut utiliser. -Ajoutez la méthode suivante à votre **src/Application.php**:: - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - -Cela configure les :doc:`/policy-resolvers` basiques qui confronteront les -entities de l'ORM avec leurs classes de policies (politiques d'autorisations). - -Ensuite, ajoutons le ``AuthorizationComponent`` à ``AppController``. Dans -**src/Controller/AppController.php**, ajoutez ceci à la méthode -``initialize()``:: - - $this->loadComponent('Authorization.Authorization'); - -En chargeant le :doc:`/component` nous pourrons plus facilement vérifier les -autorisations action par action. Par exemple, nous pouvons faire ceci:: - - public function edit($id = null) - { - $article = $this->Article->get($id); - $this->Authorization->authorize($article, 'update'); - - // Le reste de l'action - } - -En appelant ``authorize``, nous pouvons utiliser nos :doc:`/policies` pour -renforcer les règles de contrôle d'accès à notre application. Vous pouvez -vérifier les permissions depuis n'importe quel endroit en utilisant -:doc:`l'identity stockée dans la requête `. - - -Pour Aller Plus Loin -==================== - -* :doc:`/policies` -* :doc:`/policy-resolvers` -* :doc:`/middleware` -* :doc:`/component` -* :doc:`/checking-authorization` -* :doc:`/request-authorization-middleware` diff --git a/docs/fr/middleware.md b/docs/fr/middleware.md new file mode 100644 index 00000000..e8fbe9d2 --- /dev/null +++ b/docs/fr/middleware.md @@ -0,0 +1,105 @@ +# Middleware Authorization + +Le plugin Authorization s'intègre à votre application sous la forme d'un +middleware. `AuthorizationMiddleware` : + +- décore l'`identity` de la requête avec `can`, `canResult` et `applyScope` si + nécessaire ; +- vérifie qu'une autorisation a bien été effectuée ou contournée. + +Exemple de base : + +```php +namespace App; + +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Cake\Http\BaseApplication; +use Psr\Http\Message\ServerRequestInterface; + +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +{ + public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface + { + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); + } + + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + $middlewareQueue->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; + } +} +``` + +## Décorateur d'Identity + +Par défaut, l'`identity` de la requête est enveloppée par +`Authorization\IdentityDecorator`. + +```php +$originalUser = $user->getOriginalData(); +``` + +Si vous utilisez le plugin Authentication, `Authorization\Identity` sera utilisé +et implémentera également `Authentication\IdentityInterface`. + +### Utiliser votre classe User comme Identity + +Si votre classe utilisateur implémente déjà `Authorization\IdentityInterface`, +vous pouvez éviter le décorateur : + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'identityDecorator' => function ($auth, $user) { + return $user->setAuthorization($auth); + } +])); +``` + +## S'assurer que l'Autorisation est Appliquée + +Par défaut, une `AuthorizationRequiredException` est levée si une requête avec +`identity` n'a pas été autorisée ni contournée. + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'requireAuthorizationCheck' => false +])); +``` + +## Gérer les Requêtes Non Autorisées + +Les gestionnaires intégrés sont : + +- `Exception` +- `Redirect` +- `CakeRedirect` + +Exemple : + +```php +use Authorization\Exception\MissingIdentityException; + +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'unauthorizedHandler' => [ + 'className' => 'Authorization.Redirect', + 'url' => '/pages/accesinterdit', + 'queryParam' => 'redirectUrl', + 'exceptions' => [ + MissingIdentityException::class, + OtherException::class, + ], + ], +])); +``` + +Vous pouvez aussi créer votre propre gestionnaire en étendant +`RedirectHandler` si vous devez ajouter un message flash ou une logique +supplémentaire. diff --git a/docs/fr/middleware.rst b/docs/fr/middleware.rst deleted file mode 100644 index b7ea99c9..00000000 --- a/docs/fr/middleware.rst +++ /dev/null @@ -1,316 +0,0 @@ -Middleware Authorization -######################## - -Le plugin Authorization s'intègre dans votre application en tant que middleware. -Le ``AuthorizationMiddleware`` assume les responsablités suivantes: - -* Décorer l'\ 'identity' de la requête avec un décorateur qui ajoute les méthodes - ``can``, ``canResult``, et ``applyScope`` si nécessaire. -* S'assurer que l'autorisation a été vérifiée ou contournée dans la requête. - -Pour utiliser le middleware, implémentez -``AuthorizationServiceProviderInterface`` dans votre classe d'application. Puis -passez votre instance d'application au middleware et ajoutez le middleware à la -middleware queue. - -Voici un exemple basique:: - - namespace App; - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Cake\Http\BaseApplication; - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - { - public function getAuthorizationService(ServerRequestInterface $request, ResponseInterface $response) - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - - public function middleware($middlewareQueue) - { - // autres middlewares - $middlewareQueue->add(new AuthorizationMiddleware($this)); - - return $middlewareQueue; - } - } - -Le service d'autorisation a besoin d'un résolveur de policy. Pour savoir quels -sont les résolveurs disponibles et comment les utiliser, consultez la -documentation des :doc:`/policies`. - -.. _identity-decorator: - -Décorateur d'Identity -===================== - -Par défaut, l'\ ``identity`` dans une requête sera décorée (envelopée) par -``Authorization\IdentityDecorator``. La classe du décorateur intercepte les -appels aux méthodes, les accès à la manière des tableaux et les accès aux -propriétés vers l'objet décoré. Utilisez ``getOriginalData()`` pour accéder -directement à l'objet sous-jacent:: - - $originalUser = $user->getOriginalData(); - -Si votre application utilise le plugin `cakephp/authentication -`_ alors c'est la classe -``Authorization\Identity`` qui sera utilisée. Cette classe implémente -``Authentication\IdentityInterface`` en plus de -``Authorization\IdentityInterface``. Cela vous permet d'utiliser le component et -le helper des bibliothèques de ``Authentication`` pour obtenir l'identity -décorée. - -Utiliser votre class User en tant qu'Identity ---------------------------------------------- - -Si vous avez déjà une classe ``User`` ou une classe d'identité, vous pouvez vous -passer du décorateur en implémentant ``Authorization\IdentityInterface`` et en -utilisant l'option ``identityDecorator`` du middleware. Pour commencer, mettons -à jour notre classe ``User``:: - - namespace App\Model\Entity; - - use Authorization\AuthorizationServiceInterface; - use Authorization\IdentityInterface; - use Authorization\Policy\ResultInterface; - use Cake\ORM\Entity; - - class User extends Entity implements IdentityInterface - { - /** - * @inheritDoc - */ - public function can(string $action, mixed $resource): bool - { - return $this->authorization->can($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function canResult(string $action, mixed $resource): ResultInterface - { - return $this->authorization->canResult($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed - { - return $this->authorization->applyScope($this, $action, $resource, ...$optionalArgs); - } - - /** - * @inheritDoc - */ - public function getOriginalData(): \ArrayAccess|array - { - return $this; - } - - /** - * Setter to be used by the middleware. - */ - public function setAuthorization(AuthorizationServiceInterface $service) - { - $this->authorization = $service; - - return $this; - } - - // Autres méthodes - } - -Maintenant que votre user implémente l'interface nécessaire, mettons à jour la -configuration de notre middleware:: - - // Dans votre méthode Application::middleware() - - // Authorization - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'identityDecorator' => function ($auth, $user) { - return $user->setAuthorization($auth); - } - ])); - -Vous n'avez plus à changer les typehints, et vous pouvez commencer à utiliser -les policies d'autorisation partout où vous avez accès à votre user. - -Si vous utilisez aussi le plugin Authentication, assurez-vous d'implémenter les -deux interfaces.:: - - use Authorization\IdentityInterface as AuthorizationIdentity; - use Authentication\IdentityInterface as AuthenticationIdentity; - - class User extends Entity implements AuthorizationIdentity, AuthenticationIdentity - { - ... - - /** - * Méthode Authentication\IdentityInterface - * - * @return string - */ - public function getIdentifier() - { - return $this->id; - } - - ... - } - -S'assurer que Authorization est Appliqué ----------------------------------------- - -Par défaut, le ``AuthorizationMiddleware`` s'assurera que chaque requête -contenant une ``identity`` a aussi passé ou contourné l'autorisation d'accès. Si -l'autorisation d'accès n'est pas vérifiée, il soulèvera une -``AuthorizationRequiredException``. -Cette exception est soulevée **après** la fin des actions de votre -middleware/controller, donc vous ne pouvez pas vous y fier pour prévenir des -accès non autorisés. Toutefois cela peut être une aide utile pendant le -développement et les tests. Vous pouvez désactiver ce comportement grâce à une -option:: - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'requireAuthorizationCheck' => false - ])); - -Gérer les Requêtes Non Autorisées ---------------------------------- - -Par défaut, le middleware fait suivre les exceptions d'autorisation lancées par -l'application. Vous pouvez configurer des gestionnaires pour les requêtes non -autorisées et exécuter une action personnalisée, par exemple rediriger -l'utilisateur vers la page de connexion. - -Les gestionnaires intégrés sont: - -* ``Exception`` - ce gestionnaire fera suivre l'exception, c'est le comportement - par défaut du middleware. -* ``Redirect`` - ce gestionnaire redirigera la requête vers l'URL indiquée. -* ``CakeRedirect`` - gestionnaire de redirection supportant le Router CakePHP. - -Les deux gestionnaires de redirection partagent les mêmes options de -configuration: - -* ``url`` - URL vers laquelle rediriger (``CakeRedirect`` supporte la syntaxe du - Router CakePHP). -* ``exceptions`` - une liste de classes d'exceptions à rediriger. Par défaut - seule ``MissingIdentityException`` est redirigée. -* ``queryParam`` - l'URL à laquelle la requête a tenté d'accéder sera attachée - à un paramètre query de l'URL de redirection (par défaut ``redirect``). -* ``statusCode`` - le code de statut HTTP d'une redirection, par défaut ``302``. - -Par exemple:: - - use Authorization\Exception\MissingIdentityException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'Authorization.Redirect', - 'url' => '/pages/accesinterdit', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - OtherException::class, - ], - ], - ])); - -Tous les gestionnaires prennent comme paramètre l'exception qui a été soulevée. -Cette exception sera toujours une instance de -``Authorization\Exception\Exception``. -Dans cet exemple, le gestionnaire ``Authorization.Redirect`` vous permet -simplement de préciser quelles exceptions vous souhaitez écouter. - -Ainsi, dans cet exemple où nous utilisons le gestionnaire -``Authorization.Redirect``, nous pouvons ajouter au tableau ``exceptions`` -d'autres exceptions basées sur ``Authorization\Exception\Exception`` si nous -voulons qu'elles soient traitées convenablement:: - - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class - ], - -Consultez la `source de RedirectHandler `__ - -Les options de configuration sont passées à la méthode ``handle()`` du -gestionnaire comme dernier paramètre. - -Ajouter un message flash après avoir été redirigé par une requête non autorisée -------------------------------------------------------------------------------- - -Pour l'instant, il n'y a pas de moyen idéal pour ajouter un message flash lors -d'une redirection suite à une requête non autorisée. - -Par conséquent vous devez créer votre propre Handler, qui ajoutera le message -flash (ou toute autre logique que vous voudriez exécuter lors d'une -redirection). - -Comment créer un UnauthorizedHandler personnalisé -------------------------------------------------- - -#. Créez le fichier ``src/Middleware/UnauthorizedHandler/CustomRedirectHandler.php``:: - - getFlash()->error( "Vous n'êtes pas autorisé à accéder à cette ressource" ); - return $response; - } - } - -#. Dites au AuthorizationMiddleware d'utiliser votre nouveau Handler - personnalisé:: - - // dans votre src/Application.php - - use Authorization\Exception\MissingIdentityException; - use Authorization\Exception\ForbiddenException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'CustomRedirect', // <--- c'est ici ! - 'url' => '/users/login', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class - ], - 'custom_param' => true, - ], - ])); - -Comme vous le voyez, vous pouvez conserver la même configuration que si vous -utilisiez le nom de classe ``Authorization.Redirect``. - -Cela est dû au fait que notre Handler étend le RedirectHandler présent dans le -plugin. Ainsi, il contient toutes les fonctionnalités d'origin + notre propre -logique dans la fonction ``handle()``. - -Le ``custom_param`` apparaît dans le tableau ``$options`` qui vous sera donné -dans la fonction ``handle()`` à l'intérieur de votre ``CustomRedirectHandler`` -si vous souhaitez ajouter quelques paramètres de configuration supplémentaires à -vos fonctionnalités personnalisées. - -Vous pouvez consulter les classes `CakeRedirectHandler `__ ou `RedirectHandler `__ -pour voir à quoi un Handler pourrait/devrait ressembler. diff --git a/docs/fr/policies.md b/docs/fr/policies.md new file mode 100644 index 00000000..12086cbe --- /dev/null +++ b/docs/fr/policies.md @@ -0,0 +1,114 @@ +# Policies + +Les policies sont des classes qui résolvent les permissions pour une ressource +donnée. Vous pouvez créer des policies pour toute classe de votre application +qui doit être soumise à des vérifications d'autorisation. + +## Créer des Policies + +Placez vos policies dans `src/Policy`. Les classes de policy n'ont pas de +classe de base obligatoire. Les classes de l'application sont résolues vers une +policy correspondante par un résolveur. Consultez +[Résolveurs de Policy](./policy-resolvers). + +Exemple dans `src/Policy/ArticlePolicy.php` : + +```php +id == $article->user_id; +} +``` + +Une méthode de policy doit renvoyer `true` ou un objet `Result`. Toute autre +valeur est interprétée comme un échec. + +Le paramètre `$user` peut être `null` pour les utilisateurs non authentifiés. + +## Objets Result d'une Policy + +```php +use Authorization\Policy\Result; + +public function canUpdate(IdentityInterface $user, Article $article) +{ + if ($user->id == $article->user_id) { + return new Result(true); + } + + return new Result(false, 'non-proprietaire'); +} +``` + +Toute valeur autre que `true` ou un `ResultInterface` est considérée comme un +échec. + +## Portées de Policy + +Les scopes permettent de modifier une query ou une autre ressource pour n'y +laisser que ce que l'utilisateur courant peut voir : + +```php +namespace App\Policy; + +class ArticlesTablePolicy +{ + public function scopeIndex($user, $query) + { + return $query->where(['Articles.user_id' => $user->getIdentifier()]); + } +} +``` + +## Pré-conditions de Policy + +```php +namespace App\Policy; + +use Authorization\IdentityInterface; +use Authorization\Policy\BeforePolicyInterface; +use Authorization\Policy\ResultInterface; + +class ArticlesPolicy implements BeforePolicyInterface +{ + public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null + { + if ($identity->getOriginalData()->is_admin) { + return true; + } + + return null; + } +} +``` + +`before()` peut renvoyer : + +- `true` pour autoriser immédiatement. +- `false` pour refuser immédiatement. +- `null` pour laisser la méthode normale de policy décider. diff --git a/docs/fr/policies.rst b/docs/fr/policies.rst deleted file mode 100644 index 41a5b5d8..00000000 --- a/docs/fr/policies.rst +++ /dev/null @@ -1,142 +0,0 @@ -Policies -######## - -Les stratégies (*policies*) sont des classes qui résolvent les permissions pour -un objet donné. Vous pouvez créer des policies pour n'importe quelle classe de -votre application à laquelle vous souhaitez appliquer des vérifications de -permissions. - -Créer des Policies -================== - -Vous pouvez créer des policies dans votre répertoire ``src/Policy``. Les classes -de policy n'ont pas de classe de base ni d'interface commune qu'elles devraient -implémenter. Les classes de l'application sont 'résolues' avec une classe de -policy qui leur correspond. Pour savoir comment les policies peuvent être -résolues, consultez la section :doc:`policy-resolvers`. - -La plupart du temps, vous placerez vos policies dans **src/Policy** et -utiliserez le suffixe de classe ``Policy``. Pour l'instant nous allons créer une -classe de policy pour l'entité `Article` de notre application. Collez le -contenu suivant dans **src/Policy/ArticlePolicy.php**:: - - id == $article->user_id; - } - -Les méthodes de policy doivent renvoyer ``true`` ou un objet ``Result`` pour -indiquer qu'elles ont réussi. Toutes les autres valeurs s'interprètent comme des -échecs. - -Les méthodes de policy recevront ``null`` pour le paramètre ``$user`` lorsqu'il -s'agit d'utilisateurs non authentifiés. Si vous voulez que les méthodes de -policy échouent automatiquement pour les utilisateurs anonymes, vous pouvez -utiliser les typehints de ``IdentityInterface``. - -.. _policy-result-objects: - -Objets Result d'une Policy -========================== - -À part les booléens, les méthodes de policy peuvent renvoyer un objet -``Result``. Les objets ``Result`` permettent de donner plus de contexte sur les -raisons pour lesquelles la policy est passée ou a échoué:: - - use Authorization\Policy\Result; - - public function canUpdate(IdentityInterface $user, Article $article) - { - if ($user->id == $article->user_id) { - return new Result(true); - } - // Les Results vous autorisent à définir la 'raison' de l'échec. - return new Result(false, 'non-propriétaire'); - } - -Toute valeur renvoyée qui n'est ni ``true`` ni un objet ``ResultInterface`` sera -considérée comme un échec. - -Portées de Policy (Scope) -------------------------- - -En plus de définir les validations ou échecs d'autorisation, les policies -peuvent définir des 'scopes'. Avec les méthodes *scope*, vous pouvez modifier un -autre objet sous certaines conditions d'accès. La vue d'une liste restreinte à -l'utilisateur courant en est un parfait exemple:: - - namespace App\Policy; - - class ArticlesTablePolicy - { - public function scopeIndex($user, $query) - { - return $query->where(['Articles.user_id' => $user->getIdentifier()]); - } - } - -Pré-conditions de Policy ------------------------- - -Dans certaines policies, vous voudrez peut-être appliquer des conditions à -toutes les opérations de la policy. Cela peut être utile pour interdire toutes -les actions sur la ressource demandée. Pour utiliser les pré-conditions, votre -policy doit implémenter ``BeforePolicyInterface``:: - - namespace App\Policy; - - use Authorization\IdentityInterface; - use Authorization\Policy\BeforePolicyInterface; - use Authorization\Policy\ResultInterface; - - class ArticlesPolicy implements BeforePolicyInterface - { - public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null - { - if ($identity->getOriginalData()->is_admin) { - return true; - } - // continuer - } - } - -Les méthodes *before* sont censées renvoyer une de ces trois valeurs: - -- ``true`` L'utilisateur est autorisé à effectuer l'action. -- ``false`` L'utilisateur n'est pas autorisé à effectuer l'action. -- ``null`` La méthode *before* n'a pas pris de décision, et la méthode - d'autorisation doit être appelée. diff --git a/docs/fr/policy-resolvers.md b/docs/fr/policy-resolvers.md new file mode 100644 index 00000000..76d0abe2 --- /dev/null +++ b/docs/fr/policy-resolvers.md @@ -0,0 +1,77 @@ +# Résolveurs de Policy + +Les résolveurs de policy font correspondre les ressources à leurs classes de +policy. Le plugin fournit plusieurs résolveurs prêts à l'emploi et vous pouvez +créer le vôtre en implémentant `Authorization\Policy\ResolverInterface`. + +Résolveurs intégrés : + +- `MapResolver` +- `OrmResolver` +- `ResolverCollection` + +## Utiliser MapResolver + +```php +use Authorization\Policy\MapResolver; + +$mapResolver = new MapResolver(); +$mapResolver->map(Article::class, ArticlePolicy::class); +$mapResolver->map(Article::class, new ArticlePolicy()); +$mapResolver->map(Article::class, function ($resource, $mapResolver) { + // Renvoyer un objet policy. +}); +``` + +## Utiliser OrmResolver + +`OrmResolver` repose sur les conventions suivantes : + +1. Les policies se trouvent dans `App\Policy`. +2. Les classes de policy se terminent par `Policy`. + +Il peut résoudre des policies pour : + +- les entités ; +- les tables ; +- les queries. + +Exemples de correspondance : + +- `App\Model\Entity\Bookmark` devient `App\Policy\BookmarkPolicy` +- `App\Model\Table\ArticlesTable` devient `App\Policy\ArticlesTablePolicy` + +Les ressources de plugin vérifient d'abord une éventuelle policy de +l'application, puis une policy du plugin lui-même. + +Configuration personnalisée : + +```php +use Authorization\Policy\OrmResolver; + +$appNamespace = 'App'; +$overrides = [ + 'Blog' => 'Cms', +]; + +$resolver = new OrmResolver($appNamespace, $overrides); +``` + +## Utiliser ResolverCollection + +```php +use Authorization\Policy\MapResolver; +use Authorization\Policy\OrmResolver; +use Authorization\Policy\ResolverCollection; + +$resolver = new ResolverCollection([ + new MapResolver(), + new OrmResolver(), +]); +``` + +## Créer un Resolver + +Implémentez `Authorization\Policy\ResolverInterface` et définissez +`getPolicy($resource)` lorsque vous avez besoin d'une logique de résolution +personnalisée. diff --git a/docs/fr/policy-resolvers.rst b/docs/fr/policy-resolvers.rst deleted file mode 100644 index dbc1c19c..00000000 --- a/docs/fr/policy-resolvers.rst +++ /dev/null @@ -1,105 +0,0 @@ -Résolveurs de Policy -#################### - -Le résolveur de policy se charge de faire correspondre des classes de policy à -chaque objet ressource. Nous fournissons quelques résolveurs pour vous permettre -de commencer, mais vous pouvez créer votre propre résolveur en implémentant -``Authorization\Policy\ResolverInterface``. Les résolveurs intégrés sont: - -* ``MapResolver`` vous permet de faire correspondre des noms de ressources aux - noms de leurs classes de policy, ou à des objets ou des callables. -* ``OrmResolver`` résout une policy en appliquant des conventions pour les - objets ORM habituels. -* ``ResolverCollection`` vous permet d'agréger plusieurs résolveurs, et de les - exécuter les uns à la suite des autres. - -Utiliser MapResolver -==================== - -``MapResolver`` fait correspondre des noms de classes de ressources à des noms -de classes de policy, des objets policy ou des callables:: - - use Authorization\Policy\MapResolver; - - $mapResolver = new MapResolver(); - - // Mappe une classe de ressource à un nom de classe de policy - $mapResolver->map(Article::class, ArticlePolicy::class); - - // Mappe une classe de ressource à une instance de policy. - $mapResolver->map(Article::class, new ArticlePolicy()); - - // Mappe une classe de ressource à une fonction callable - $mapResolver->map(Article::class, function ($resource, $mapResolver) { - // Renvoie un objet policy. - }); - -Utiliser OrmResolver -==================== - -Le ``OrmResolver`` est un résolveur de policy basé sur des conventions pour -l'ORM de CakePHP. L'OrmResolver applique les conventions suivantes: - -#. Les policies se trouvent dans ``App\Policy`` -#. Les classes de policy se terminent avec le suffixe de classe ``Policy``. - -Le OrmResolver peut résoudre des policies pour les types d'objets suivants: - -* Entities - En utilisant le nom de classe de l'entity. -* Tables - En utilisant le nom de classe de la table. -* Queries - En utilisant le résultat de la méthode ``repository()`` de la query - pour obtenir un nom de classe. - -Les règles suivantes s'appliquent dans tous les cas: - -#. On utilise le nom de classe de la ressource pour générer le nom de classe - d'une policy. Par exemple ``App\Model\Entity\Bookmark`` correspondra à - ``App\Policy\BookmarkPolicy``. -#. Les ressources de plugins rechercheront d'abord une policy d'application, par - exemple ``App\Policy\Bookmarks\BookmarkPolicy`` pour - ``Bookmarks\Model\Entity\Bookmark``. -#. Si on ne trouve aucune policy d'application en priorité, on recherche une - policy du plugin. Par exemple ``Bookmarks\Policy\BookmarkPolicy``. - -Pour les objets tables, la transformation du nom de table ferait correspondre -``App\Model\Table\ArticlesTable`` à ``App\Policy\ArticlesTablePolicy``. -Pour les objets query, la méthode ``repository()`` sera appelée, et une policy -sera générée à partir de la classe de table obtenue. - -Le OrmResolver peut être personnalisé par son constructeur:: - - use Authorization\Policy\OrmResolver; - - // Changer pour utiliser un namespace d'application personnalisé. - $appNamespace = 'App'; - - // Fait correspondre des policies d'un namespace à un autre. - // Ici nous avons mappé des policies pour les classes du namespace ``Blog`` - // pour qu'elles soient recherchées dans le namespace ``Cms``. - $overrides = [ - 'Blog' => 'Cms', - ]; - $resolver = new OrmResolver($appNamespace, $overrides) - -Utiliser ResolverCollection -=========================== - -``ResolverCollection`` vous permet d'agréger plusieurs résolveurs:: - - use Authorization\Policy\ResolverCollection; - use Authorization\Policy\MapResolver; - use Authorization\Policy\OrmResolver; - - $ormResolver = new OrmResolver(); - $mapResolver = new MapResolver(); - - // Vérifie le MapResolver, et se rabat sur l'OrmResolver si une ressource - // n'est pas mappée explicitement. - $resolver = new ResolverCollection([$mapResolver, $ormResolver]); - -Créer un Resolver -================= - -Vous pouvez écrire votre propre résolveur en implémentant -``Authorization\Policy\ResolverInterface``, qui nécessite de définir la méthode -``getPolicy($resource)``. diff --git a/docs/fr/request-authorization-middleware.md b/docs/fr/request-authorization-middleware.md new file mode 100644 index 00000000..e0fe3bbf --- /dev/null +++ b/docs/fr/request-authorization-middleware.md @@ -0,0 +1,65 @@ +# Middleware d'Autorisation de Requête + +Ce middleware permet d'autoriser la requête elle-même, par exemple selon le +contrôleur, l'action, ou tout autre système ACL ou RBAC. + +Ajoutez-le après `AuthorizationMiddleware`, `AuthenticationMiddleware` et +`RoutingMiddleware`. + +## Comment l'utiliser + +Créez `src/Policy/RequestPolicy.php` : + +```php +namespace App\Policy; + +use Authorization\Policy\RequestPolicyInterface; +use Cake\Http\ServerRequest; + +class RequestPolicy implements RequestPolicyInterface +{ + public function canAccess($identity, ServerRequest $request) + { + if ($request->getParam('controller') === 'Articles' + && $request->getParam('action') === 'index' + ) { + return true; + } + + return false; + } +} +``` + +Mappez ensuite la classe de requête à cette policy : + +```php +use App\Policy\RequestPolicy; +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Middleware\RequestAuthorizationMiddleware; +use Authorization\Policy\MapResolver; +use Cake\Http\ServerRequest; +use Psr\Http\Message\ServerRequestInterface; + +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $mapResolver = new MapResolver(); + $mapResolver->map(ServerRequest::class, RequestPolicy::class); + + return new AuthorizationService($mapResolver); +} +``` + +Puis chargez `RequestAuthorizationMiddleware` après `AuthorizationMiddleware` : + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new AuthorizationMiddleware($this)); + $middlewareQueue->add(new RequestAuthorizationMiddleware()); + + return $middlewareQueue; +} +``` diff --git a/docs/fr/request-authorization-middleware.rst b/docs/fr/request-authorization-middleware.rst deleted file mode 100644 index 38da2a1b..00000000 --- a/docs/fr/request-authorization-middleware.rst +++ /dev/null @@ -1,79 +0,0 @@ -Middleware d'Autorisation de Requête -#################################### - -Ce middleware est utile pour autoriser vos requêtes, par exemple chaque -controller et action, en fonction d'un système d'accès basé sur des rôles ou -n'importe quel autre type de processus d'autorisation qui contrôle l'accès à -certaines actions. - -Il **doit** être ajouté après Authorization, Authentication et RoutingMiddleware -dans la Middleware Queue ! -La logique de gestion de l'autorisation de la requête sera implémentée dans la -policy de la requête. Vous pouvez ajouter toute votre logique à cet endroit ou -simplement passer l'information de la requête vers une implémentation ACL ou -RBAC. - -Comment l'utiliser -================== - -Créez une policy pour gérer l'objet requête. Le plugin est livré avec une -interface que nous pouvons implémenter. Commencez par créer -**src/Policy/RequestPolicy.php** et ajoutez-y:: - - namespace App\Policy; - - use Authorization\Policy\RequestPolicyInterface; - use Cake\Http\ServerRequest; - - class RequestPolicy implements RequestPolicyInterface - { - /** - * Méthode pour vérifier si on peut accéder à la requête - * - * @param \Authorization\IdentityInterface|null $identity Identity - * @param \Cake\Http\ServerRequest $request Requête du serveur - * @return bool - */ - public function canAccess($identity, ServerRequest $request) - { - if ($request->getParam('controller') === 'Articles' - && $request->getParam('action') === 'index' - ) { - return true; - } - - return false; - } - } - -Ensuite, mappez la classe de la requête vers la policy à l'intérieur de -``Application::getAuthorizationService()``, dans **src/Application.php** :: - - use App\Policy\RequestPolicy; - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Middleware\RequestAuthorizationMiddleware; - use Authorization\Policy\MapResolver; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ResponseInterface; - use Cake\Http\ServerRequest; - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface { - $mapResolver = new MapResolver(); - $mapResolver->map(ServerRequest::class, RequestPolicy::class); - return new AuthorizationService($mapResolver); - } - -Assurez-vous de charger RequestAuthorizationMiddleware **après** -AuthorizationMiddleware:: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { - // autres middlewares... - // $middlewareQueue->add(new AuthenticationMiddleware($this)); - - // Ajoutez l'autorisation (après authentication si vous utilisez aussi ce plugin). - $middlewareQueue->add(new AuthorizationMiddleware($this)); - $middlewareQueue->add(new RequestAuthorizationMiddleware()); - } diff --git a/docs/ja/2-0-migration-guide.md b/docs/ja/2-0-migration-guide.md new file mode 100644 index 00000000..ac47285a --- /dev/null +++ b/docs/ja/2-0-migration-guide.md @@ -0,0 +1,11 @@ +# 2.0 Migration Guide + +Authorization 2.0 には新しい機能と破壊的変更が含まれています。 + +## 破壊的変更 + +`IdentityInterface` に型宣言が追加されました。アプリケーションで実装して +いる場合は、シグネチャを更新してください。 + +`canResult()` も追加され、常に `ResultInterface` を返すようになりました。 +`can()` は常に bool を返します。 diff --git a/docs/ja/2-0-migration-guide.rst b/docs/ja/2-0-migration-guide.rst deleted file mode 100644 index 14655a1a..00000000 --- a/docs/ja/2-0-migration-guide.rst +++ /dev/null @@ -1,16 +0,0 @@ -2.0 Migration Guide -################### - -Authorization 2.0 には新しい機能と、少しの破壊的変更が含まれています。 - -破壊的変更 -================ - -``IdentityInterface`` にタイプヒントが追加されました。 -もし ``IdentityInterface`` を実装している場合は、新しい typehints を反映させるためにアプリケーションの実装を更新する必要があります。 - -タイプヒントを加えて、 ``IdentityInterface`` に ``canResult()`` メソッドが追加されました。 -このメソッドは常に ``ResultInterface`` オブジェクトを返し ``can()`` は常にboolを返します。 -1.xバージョンの時は ``can()`` は ``bool`` と ``ResultInterface`` が返却されていました。 -このため、 ``can()`` の戻り値を知ることは非常に困難でした。 -新しいメソッドと追加の型付けにより、 ``IdentityInterface`` はよりシンプルに、より信頼性の高いものとして使用できるようになりました。 \ No newline at end of file diff --git a/docs/ja/checking-authorization.md b/docs/ja/checking-authorization.md new file mode 100644 index 00000000..79fda5df --- /dev/null +++ b/docs/ja/checking-authorization.md @@ -0,0 +1,33 @@ +# 認可の確認 + +[認可ミドルウェア](./middleware) を適用して request に `identity` を +追加したら、認可チェックを開始できます。 + +## 単一リソースの認可チェック + +```php +$user = $this->request->getAttribute('identity'); + +if ($user->can('delete', $article)) { + // 削除処理 +} +``` + +結果オブジェクトを返す policy の場合: + +```php +$result = $user->canResult('delete', $article); +if ($result->getStatus()) { + // 削除する +} +``` + +## スコープ条件を適用する + +```php +$user = $this->request->getAttribute('identity'); +$query = $user->applyScope('index', $query); +``` + +controller では [AuthorizationComponent](./component) を使って失敗時に +例外を投げるチェックを簡単に行えます。 diff --git a/docs/ja/checking-authorization.rst b/docs/ja/checking-authorization.rst deleted file mode 100644 index ef99e435..00000000 --- a/docs/ja/checking-authorization.rst +++ /dev/null @@ -1,50 +0,0 @@ -認可の確認 -###################### - -アプリケーションに :doc:`/middleware` を適用して、リクエストに ``identity`` を追加したら、認可のチェックを開始できます。 -ミドルウェアは各リクエストの ``identity`` を ``IdentityDecorator`` でラップし、認可に関連するメソッドを追加します。 - -モデル、サービス、テンプレートに ``identity`` を渡すことで、 -アプリケーションの任意の場所で簡単に認可を確認することができます。 -デフォルトのデコレーターをカスタマイズしたり置き換えたりする方法については、 :ref:`identity-decorator` のセクションを参照してください。 - -単一リソースの認可チェック -============================================ - -``can`` メソッドを使用すると、1つのリソースの認可を確認することができます。 -通常、これはORMエンティティ、またはアプリケーションドメインオブジェクトである。 -あなたの :doc:`/policies` は認可を決定するためのロジックを提供します。:: - - // リクエストからIDを取得する - $user = $this->request->getAttribute('identity'); - - // $article の権限をチェック - if ($user->can('delete', $article)) { - // 削除の操作... - } - -ポリシーが :ref:`policy-result-objects` を返す場合、 ``canResult()`` が結果インスタンスを返すので、そのステータスを必ずチェックしてください。:: - - // ポリシーが結果を返すと仮定して - $result = $user->canResult('delete', $article); - if ($result->getStatus()) { - // 削除する - } - -条件適用の範囲 -========================= - -ページ分割されたクエリのようなオブジェクトのコレクションに権限チェックを適用する必要がある場合、 -現在のユーザがアクセスできるレコードのみを取得したいことがよくあるでしょう。 -このプラグインは、この概念を 'scope' として実装しています。 -スコープ・ポリシーでは、クエリや結果セットを 'scope' し、 -更新されたリストやクエリ・オブジェクトを返すことができます:: - - // リクエストから 'identity' を取得する - $user = $this->request->getAttribute('identity'); - - // クエリに許可条件を適用し、 - // 現在のユーザーがアクセスできるレコードのみを返すようにします。 - $query = $user->applyScope('index', $query); - -:doc:`/component` をコントローラのアクションで使用することで、失敗時に例外が発生する認証チェックを効率化することができます。 diff --git a/docs/ja/component.md b/docs/ja/component.md new file mode 100644 index 00000000..8464fdf7 --- /dev/null +++ b/docs/ja/component.md @@ -0,0 +1,80 @@ +# AuthorizationComponent + +`AuthorizationComponent` は controller で権限確認を行うための +ヘルパーメソッドを提供します。 + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +## 自動認可の確認 + +```php +$this->Authorization->authorizeModel('index', 'add'); +``` + +公開アクションを設定する場合: + +```php +$this->loadComponent('Authorization.Authorization', [ + 'skipAuthorization' => [ + 'login', + ] +]); +``` + +## Authorization を確認する + +```php +public function edit($id) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article); +} +``` + +別の policy アクション名を使う場合: + +```php +$this->Authorization->authorize($article, 'update'); +``` + +bool が欲しい場合: + +```php +if ($this->Authorization->can($article, 'update')) { + // 何らかの処理 +} +``` + +## 匿名ユーザー + +`can()` と `authorize()` は匿名ユーザーも扱えます。未ログインの場合、 +policy には `null` が渡されます。 + +## ポリシースコープを適用する + +```php +$query = $this->Authorization->applyScope($this->Articles->find()); +``` + +```php +$this->Authorization->mapActions([ + 'index' => 'list', + 'delete' => 'remove', + 'add' => 'insert', +]); +``` + +## 認可をスキップする + +```php +public function view($id) +{ + $this->Authorization->skipAuthorization(); +} +``` diff --git a/docs/ja/component.rst b/docs/ja/component.rst deleted file mode 100644 index 1cef81c8..00000000 --- a/docs/ja/component.rst +++ /dev/null @@ -1,132 +0,0 @@ -AuthorizationComponent -###################### - -``AuthorizationComponent`` は、コントローラのパーミッションをチェックするための、 -いくつかの規約に基づいたヘルパーメソッドを公開しています。 -ユーザーの取得や、 ``can`` や ``applyScope`` メソッドの呼び出しが抽象化されています。 -AuthorizationComponent を使用するには、ミドルウェアを使用する必要があるので、ミドルウェアが適用されていることを確認してください。コンポーネントを使用するには、まず、次のようにコンポーネントをロードします。:: - - // In your AppController - public function initialize() - { - parent::initialize(); - $this->loadComponent('Authorization.Authorization'); - } - -自動認可の確認 -============================== - -``AuthorizationComponent`` は、コントローラのデフォルトのモデルクラスと現在のアクション名に基づいて、自動的に認可を適用するように設定することができます。 -次の例では、 ``index`` と ``add`` のアクションが許可されます。:: - - $this->Authorization->authorizeModel('index', 'add'); - -また、認証をスキップするアクションを設定することも可能です。 -これにより、すべてのユーザーがアクセスできる、**public** なアクションが作成されます。デフォルトでは、すべてのアクションは認証が必要で、認証チェックが有効な場合は ``AuthorizationRequiredException`` がスローされます。 -Authorizationは個々のアクションをスキップできます。:: - - $this->loadComponent('Authorization.Authorization', [ - 'skipAuthorization' => [ - 'login', - ] - ]); - -Authorization を確認する -====================== - -コントローラのアクションやコールバックメソッドで、 認証をチェックするにはコンポーネント:: - - // ArticlesControllerの中 - public function edit($id) - { - $article = $this->Articles->get($id); - $this->Authorization->authorize($article); - // 残りの編集についての処理 - } - -上では、現在のユーザーに対して記事が認証されていることがわかります。 -チェックするアクションを指定していないので、リクエストの ``action`` が使われます。 -第2パラメータでポリシーアクションを指定することができます。:: - - // 現在のコントローラアクションに一致しないポリシーメソッドを使用します。 - $this->Authorization->authorize($article, 'update'); - -``authorize()`` は、拒否されると ``Authorization\Exception\ForbiddenException`` を投げます。 もし、Bool値を取得したいなら ``can()`` を使用してください :: - - if ($this->Authorization->can($article, 'update')) { - // 記事に関する処理 - } - -匿名のユーザー -=============== - -アプリケーション内の一部のリソースは、ログインしていないユーザーもアクセスできる場合があります。 -未認証のユーザーがリソースにアクセスできるかどうかは、ポリシーの領域である。 -このコンポーネントを通して、匿名ユーザーの認可を確認することができます。 -``can()`` と ``authorize()`` の両方が匿名ユーザーをサポートします。ユーザーがログインしていない場合、ポリシーは 'user' パラメータに ``null`` を期待することができます。 - -ポリシースコープを適用する -====================== - -また、ポリシースコープを適用するには、コンポーネント:: - -$query = $this->Authorization->applyScope($this->Articles->find()); - -現在のアクションにログインしているユーザがいない場合、 ``MissingIdentityException`` が発生します。 - -アクションを異なる認証方式にマッピングしたい場合は、 ``actionMap`` オプションを使用します:: - - // In your controller initialize() method: - $this->Authorization->mapActions([ - 'index' => 'list', - 'delete' => 'remove', - 'add' => 'insert', - ]); - - // or map actions individually. - $this->Authorization - ->mapAction('index','list') - ->mapAction('delete', 'remove') - ->mapAction('add', 'insert'); - -例:: - - //ArticlesController.php - - public function index() - { - $query = $this->Articles->find(); - - // これは `index` コントローラアクションで呼び出される際に `list` スコープを適用します。 - $this->Authorization->applyScope($query); - ... - } - - public function delete($id) - { - $article = $this->Articles->get($id); - - // これは、 `delete` コントローラアクションで呼び出される `remove` エンティティアクションに対して認可を行うものです。 - $this->Authorization->authorize($article); - ... - } - - public function add() - { - // これは `add` コントローラアクションで呼び出される `insert` モデルアクションに対して認可を行います。 - $this->Authorization->authorizeModel(); - ... - } - - 認可をスキップする -====================== - -アクションの内部で認証を省略することもできます。:: - - //ArticlesController.php - - public function view($id) - { - $this->Authorization->skipAuthorization(); - ... - } diff --git a/docs/ja/conf.py b/docs/ja/conf.py deleted file mode 100644 index 5871da64..00000000 --- a/docs/ja/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'ja' diff --git a/docs/ja/contents.rst b/docs/ja/contents.rst deleted file mode 100644 index d869a265..00000000 --- a/docs/ja/contents.rst +++ /dev/null @@ -1,15 +0,0 @@ -Contents -######## - -.. toctree:: - :maxdepth: 2 - :caption: CakePHP Authorization - - /index - /policies - /policy-resolvers - /middleware - /checking-authorization - /component - /request-authorization-middleware - /2-0-migration-guide diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 00000000..b431dc27 --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,98 @@ +# クイックスタート + +## インストール + +CakePHP プロジェクトのルートディレクトリで +[Composer](https://getcomposer.org/) を使ってプラグインを追加します。 + +```bash +php composer.phar require "cakephp/authorization:^3.0" +``` + +Authorization 3.x は CakePHP 5 に対応しています。 + +`src/Application.php` でプラグインを読み込みます。 + +```php +$this->addPlugin('Authorization'); +``` + +## はじめに + +Authorization プラグインはミドルウェアとして組み込まれ、必要に応じて +コンポーネントとしても利用できます。 + +```php +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Psr\Http\Message\ServerRequestInterface; +``` + +`Application` に `AuthorizationServiceProviderInterface` を追加します。 + +```php +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +``` + +`middleware()` に Authorization を追加します。 + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) + ->add(new AssetMiddleware()) + ->add(new RoutingMiddleware($this)) + ->add(new BodyParserMiddleware()) + ->add(new AuthenticationMiddleware($this)) + ->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; +} +``` + +`AuthorizationMiddleware` は `AuthenticationMiddleware` の後に置く必要が +あります。 + +```php +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); +} +``` + +これは [ポリシーリゾルバー](./policy-resolvers) を構成するための基本設定です。 + +`AppController` で component を読み込みます。 + +```php +public function initialize(): void +{ + parent::initialize(); + $this->loadComponent('Authorization.Authorization'); +} +``` + +```php +public function edit($id = null) +{ + $article = $this->Articles->get($id); + $this->Authorization->authorize($article, 'update'); +} +``` + +`authorize()` を使うことで、[Policies](./policies) によるアクセス制御を +適用できます。 + +## より詳しく + +- [Policies](./policies) +- [ポリシーリゾルバー](./policy-resolvers) +- [認可ミドルウェア](./middleware) +- [AuthorizationComponent](./component) +- [認可の確認](./checking-authorization) +- [リクエスト認証ミドルウェア](./request-authorization-middleware) diff --git a/docs/ja/index.rst b/docs/ja/index.rst deleted file mode 100644 index 58da8a00..00000000 --- a/docs/ja/index.rst +++ /dev/null @@ -1,105 +0,0 @@ -クイック スタート -########### - -インストール -============ - -プラグインをインストールするには、CakePHPから `composer `__ を -プロジェクトのルートディレクトリ (**composer.json** がある場所) で使用します。 - - -.. code-block:: shell - - php composer.phar require "cakephp/authorization:^2.0" - -Authorization Plugin(認可プラグイン)のバージョン2はCakePHP4に対応しています。 - -プラグインを読み込むにはApplication.phpに下記の一業を追加します。 -``src/Application.php`` -:: - - $this->addPlugin('Authorization'); - -はじめに -=============== - -Authorization pluginは、ミドルウェア層としてアプリケーションに組み込まれ、 -オプションのコンポーネントで認可の確認を容易に行え行えます。 -はじめに **src/Application.php** に以下のクラス使用を明示してください。:: - - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ResponseInterface; - use Psr\Http\Message\ServerRequestInterface; - -Applicationに ``AuthorizationServiceProviderInterface`` を追加してください:: - - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - -そして、Applicationの ``middleware()`` を以下のようにします(AuthorizationMiddlewareを加える):: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue - { - // Middleware provided by CakePHP - $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'))) - ->add(new AssetMiddleware()) - ->add(new RoutingMiddleware($this)) - ->add(new BodyParserMiddleware()) - - // If you are using Authentication it should be *before* Authorization. - ->add(new AuthenticationMiddleware($this)); - - // Add the AuthorizationMiddleware *after* routing, body parser - // and authentication middleware. - ->add(new AuthorizationMiddleware($this)); - - return $middlewareQueue(); - } - -``AuthorizationMiddleware`` は ``AuthenticationMiddleware`` の **後** に置く必要があります。 -これにより、認可の確認で使用する ``identity`` が使用できるようになります。 - -``AuthorizationMiddleware`` はリクエスト処理が開始した際に、フックメソッドを呼び出します。 -このフックメソッドで、使用したい ``AuthorizationService`` を定義できます。 -以下のメソッドを **src/Application.php** に追加してください。:: - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $resolver = new OrmResolver(); - - return new AuthorizationService($resolver); - } - -これは、ORMエンティティとPolicyクラスをマッチングする、基本的な :doc:`/policy-resolvers` を設定するものです。 - -次に、 ``AppController`` に ``AuthorizationComponent`` を加えます。 -**src/Controller/AppController.php** の ``initialize()`` に下記を追加してください。:: - - $this->loadComponent('Authorization.Authorization'); - -:doc:`/component` を読み込むことで、以下のような認可チェックを行うことができます。 - - public function edit($id = null) - { - $article = $this->Article->get($id); - $this->Authorization->authorize($article, 'update'); - - // Rest of action - } - -``authorize`` を呼び出すことで、 :doc:`/policies` を使用して、アクセス制御ルールを強制することができます。 -:doc:`identity stored in the request ` を使えばどこでも権限を確認することができます。 - - -より詳しく -=============== - -* :doc:`/policies` -* :doc:`/policy-resolvers` -* :doc:`/middleware` -* :doc:`/component` -* :doc:`/checking-authorization` -* :doc:`/request-authorization-middleware` diff --git a/docs/ja/middleware.md b/docs/ja/middleware.md new file mode 100644 index 00000000..d5ca7a68 --- /dev/null +++ b/docs/ja/middleware.md @@ -0,0 +1,89 @@ +# 認可ミドルウェア + +Authorization はミドルウェアとしてアプリケーションに適用されます。 +`AuthorizationMiddleware` は次の役割を持ちます。 + +- request の `identity` に `can`、`canResult`、`applyScope` を追加する +- authorization が実行または明示的にスキップされたことを確認する + +```php +namespace App; + +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\AuthorizationServiceProviderInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Policy\OrmResolver; +use Cake\Http\BaseApplication; +use Psr\Http\Message\ServerRequestInterface; + +class Application extends BaseApplication implements AuthorizationServiceProviderInterface +{ + public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface + { + $resolver = new OrmResolver(); + + return new AuthorizationService($resolver); + } + + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + $middlewareQueue->add(new AuthorizationMiddleware($this)); + + return $middlewareQueue; + } +} +``` + +## Identity Decorator + +デフォルトでは request の `identity` は +`Authorization\IdentityDecorator` でラップされます。 + +```php +$originalUser = $user->getOriginalData(); +``` + +### User クラスを identity として使う + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'identityDecorator' => function ($auth, $user) { + return $user->setAuthorization($auth); + } +])); +``` + +## 認可を確実に適用する + +```php +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'requireAuthorizationCheck' => false +])); +``` + +## 不正なリクエストへの対処 + +組み込みハンドラー: + +- `Exception` +- `Redirect` +- `CakeRedirect` + +```php +use Authorization\Exception\MissingIdentityException; + +$middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'unauthorizedHandler' => [ + 'className' => 'Authorization.Redirect', + 'url' => '/pages/unauthorized', + 'queryParam' => 'redirectUrl', + 'exceptions' => [ + MissingIdentityException::class, + OtherException::class, + ], + ], +])); +``` + +必要であれば `RedirectHandler` を継承して独自ハンドラーを作成できます。 diff --git a/docs/ja/middleware.rst b/docs/ja/middleware.rst deleted file mode 100644 index 3b9ee954..00000000 --- a/docs/ja/middleware.rst +++ /dev/null @@ -1,256 +0,0 @@ -認可ミドルウェア -######################## - -認可はミドルウェアとしてアプリケーションに適用されます。 -``AuthorizationMiddleware`` は次のような役割があります。: - -* リクエストの '識別要素' を必要に応じて ``can``, ``canResult``, ``applyScope`` メソッドで装飾します。 -* リクエストの確認/回避を確実にする - -アプリケーションにミドルウェアを反映させるにはApplicationクラスに ``AuthorizationServiceProviderInterface`` を追加します。 -ミドルウェアをキューに追加するのもお忘れなく。 -基本的な例:: - - namespace App; - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Policy\OrmResolver; - use Cake\Http\BaseApplication; - class Application extends BaseApplication implements AuthorizationServiceProviderInterface - { - public function getAuthorizationService(ServerRequestInterface $request, ResponseInterface $response) - { - $resolver = new OrmResolver(); - return new AuthorizationService($resolver); - } - public function middleware($middlewareQueue) - { - // other middleware - $middlewareQueue->add(new AuthorizationMiddleware($this)); - return $middlewareQueue; - } - } - -認可Serviceにはポリシーリゾルバが必要です。 -詳しくは :doc:`/policies` をご覧ください。 - -.. _identity-decorator: - -Identity Decorator -================== - -デフォルトの ``identity`` (リクエスト) は ``Authorization\IdentityDecorator`` でデコレートされます。 -デコレータクラスは、メソッド呼び出し、配列アクセス、プロパティアクセスをデコレートされたIDオブジェクトにプロキシします。 -アクセスするのに基礎となる ``getOriginalData()`` を直接使う:: - - $originalUser = $user->getOriginalData(); - -もしあなたのアプリケーションで、 `cakephp/authentication -`_ プラグインが使われていたら、 -``Authorization\Identity`` クラスを使用しています。 -このクラスは ``Authorization\IdentityInterface`` に ``Authentication\IdentityInterface``を加えて実装します。 -これにより、 ``Authentication`` のコンポーネントとヘルパーを使用して、デコレートされたIDが取得できます。 - -Userクラスを識別子として使用する -------------------------------------- - -``User`` クラスか識別子クラスがある場合、 ``Authorization\IdentityInterface`` を実装して、 -``identityDecorator`` ミドルウェアオプションを使用していた場合、デコレータを省略することができます。 -最初に ``User`` クラスを変更します:: - - namespace App\Model\Entity; - use Authorization\AuthorizationServiceInterface; - use Authorization\IdentityInterface; - use Authorization\Policy\ResultInterface; - use Cake\ORM\Entity; - class User extends Entity implements IdentityInterface - { - /** - * @inheritDoc - */ - public function can(string $action, mixed $resource): bool - { - return $this->authorization->can($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function canResult(string $action, mixed $resource): ResultInterface - { - return $this->authorization->canResult($this, $action, $resource); - } - - /** - * @inheritDoc - */ - public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed - { - return $this->authorization->applyScope($this, $action, $resource, ...$optionalArgs); - } - - /** - * @inheritDoc - */ - public function getOriginalData(): \ArrayAccess|array - { - return $this; - } - - /** - * Setter to be used by the middleware. - */ - public function setAuthorization(AuthorizationServiceInterface $service) - { - $this->authorization = $service; - return $this; - } - // Other methods - } - -必要なインターフェースは実装したので、ミドルウェアの設定を更新しましょう:: - - // Application::middleware() メソッド内で - // Authorization - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'identityDecorator' => function ($auth, $user) { - return $user->setAuthorization($auth); - } - ])); - -既存のタイプヒントを変更する必要がなくなり、ユーザーへのアクセスが可能な場所であれば、どこでも認可ポリシーを使い始めることができます。 -Authentication(認証)プラグインを使っているなら、両方のインターフェイスを実装します。:: - - use Authorization\IdentityInterface as AuthorizationIdentity; - use Authentication\IdentityInterface as AuthenticationIdentity; - class User extends Entity implements AuthorizationIdentity, AuthenticationIdentity - { - ... - - /** - * Authentication\IdentityInterface method - * - * @return string - */ - public function getIdentifier() - { - return $this->id; - } - ... - } - -認可を確実に適用する ---------------------------------- - -デフォルトでは、 ``AuthorizationMiddleware`` は ``identity`` を含む各リクエストに対して、認可のチェックと回避を行います。 -認可が確認できなかった場合 ``AuthorizationRequiredException`` を投げます。 -この例外はミドルウェア/コントローラーが動作した **後に** 発生するため、不正アクセスの防止に使えません。 -しかし、開発やテストの時は補助として使うことができます。 -この動作は、オプションで無効にすることができます:: - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'requireAuthorizationCheck' => false - ])); - -不正なリクエストへの対処 ------------------------------- - -デフォルトでは、アプリケーションがスローする認証例外は、ミドルウェアによって再スローされます。 -不正なリクエストへの対処を設定し、ユーザーをログインページにリダイレクトさせるなど、 -カスタムアクションを実行することができます。: - -* ``Exception`` - このハンドラーは例外を再スローします。これはミドルウェアのデフォルトの動作です。 -* ``Redirect`` - このハンドラーは、指定されたURLにリクエストをリダイレクトします。 -* ``CakeRedirect`` - CakePHPルーターをサポートするハンドラーをリダイレクトします。 - 両方のリダイレクトハンドラーは同じ構成オプションを共有します: -* ``url`` - リダイレクトするURL (``CakeRedirect`` はCakePHPルーター構文をサポートします。). -* ``exceptions`` - リダイレクトする必要がある例外クラスのリスト。デフォルトでは - ``MissingIdentityException`` のみがリダイレクトされます。 -* ``queryParam`` - アクセスされたリクエストURLは、リダイレクトURLクエリパラメータにアタッチされます。 - (デフォルトは ``redirect``) -* ``statusCode`` - リダイレクトのHTTPステータスコードで、デフォルトは ``302`` - です。 - -例:: - - use Authorization\Exception\MissingIdentityException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'Authorization.Redirect', - 'url' => '/pages/unauthorized', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - OtherException::class, - ], - ], - ])); - -すべてのハンドラは、パラメータとして与えられたスローされた例外オブジェクトを取得します。 -この例外はいつも ``Authorization\Exception\Exception`` のインスタンスです。 -この例では、 ``Authorization.Redirect`` ハンドラで、どの例外をリスニングするかを指定するオプションが提供されているだけです。 -この例では ``Authorization.Redirect`` ハンドラを使用していますが、 -他の ``AuthorizationException`` ベースの例外を優雅に処理したい場合は、 -``execeptions`` 配列に追加することができます。:: - - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class - ], - -`RedirectHandler source `__ を見てください。 -設定オプションはハンドラの ``handle()`` メソッドに最後のパラメータとして渡されます。 - -不正なリクエストでリダイレクトされた後のフラッシュメッセージの追加 ----------------------------------------------------------------------- - -現在、不正なリダイレクトにフラッシュメッセージを追加するストレートな方法はありません。 -したがって、フラッシュメッセージ (またはリダイレクト時に発生させたいその他のロジック) を -追加する独自のハンドラを作成する必要があります。 - -どうやってカスタムUnauthorizedHandlerを作成するか -------------------------------------------------- - -#. ``src/Middleware/UnauthorizedHandler/CustomRedirectHandler.php`` ファイルを作成:: - - getFlash()->error( 'You are not authorized to access that location' ); - return $response; - } - } -#. AuthorizationMiddlewareに、新しいカスタムハンドラを使用するように指示します。:: - - // src/Application.php内で - - use Authorization\Exception\MissingIdentityException; - use Authorization\Exception\ForbiddenException; - - $middlewareQueue->add(new AuthorizationMiddleware($this, [ - 'unauthorizedHandler' => [ - 'className' => 'CustomRedirect', // <--- see here - 'url' => '/users/login', - 'queryParam' => 'redirectUrl', - 'exceptions' => [ - MissingIdentityException::class, - ForbiddenException::class - ], - 'custom_param' => true, - ], - ])); - -クラス名として ``Authorization.Redirect`` を使用した場合と同じ設定パラメータがあることがおわかりいただけると思います。 -これは、プラグインに存在する RedirectHandler をベースに私たちのハンドラを拡張しているからです。したがって、すべての機能は ``handle()`` 関数内に存在し、私たち自身の機能は ``handle()`` 内に存在します。 - -カスタムパラメータを追加したい場合は、 ``CustomRedirectHandler`` 内の ``handle()`` 関数で指定した ``$options`` 配列に ``custom_param`` が含まれます。 -こちらもご覧ください `CakeRedirectHandler `__ or `RedirectHandler `__ diff --git a/docs/ja/policies.md b/docs/ja/policies.md new file mode 100644 index 00000000..ada31f0a --- /dev/null +++ b/docs/ja/policies.md @@ -0,0 +1,87 @@ +# Policies + +Policies は与えられたオブジェクトの権限を解決するクラスです。 + +## Policies を作成する + +`src/Policy` ディレクトリに policy を作成します。解決方法については +[ポリシーリゾルバー](./policy-resolvers) を参照してください。 + +```php +id == $article->user_id; +} +``` + +## ポリシーの Result オブジェクト + +```php +use Authorization\Policy\Result; + +public function canUpdate(IdentityInterface $user, Article $article) +{ + if ($user->id == $article->user_id) { + return new Result(true); + } + + return new Result(false, 'not-owner'); +} +``` + +## ポリシーのスコープ + +```php +namespace App\Policy; + +class ArticlesTablePolicy +{ + public function scopeIndex($user, $query) + { + return $query->where(['Articles.user_id' => $user->getIdentifier()]); + } +} +``` + +## ポリシーの前提条件 + +```php +namespace App\Policy; + +use Authorization\IdentityInterface; +use Authorization\Policy\BeforePolicyInterface; +use Authorization\Policy\ResultInterface; + +class ArticlesPolicy implements BeforePolicyInterface +{ + public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null + { + if ($identity->getOriginalData()->is_admin) { + return true; + } + + return null; + } +} +``` diff --git a/docs/ja/policies.rst b/docs/ja/policies.rst deleted file mode 100644 index 6912c0d3..00000000 --- a/docs/ja/policies.rst +++ /dev/null @@ -1,127 +0,0 @@ -Policies -######## - -Policiesは与えられたオブジェクトの権限を解決するクラスです。 -アプリケーション内の任意のクラスに対して、権限の確認を適用するPoliciesを作成することができます。 - -Policies を作成する -=================== - -``src/Policy`` ディレクトリにPoliciesを作成します。 -クラスが用意されていないため、自分で作成する必要があります。 -アプリケーションのクラスはポリシークラスによって、'resolved'(解決)されます。 -ポリシーの解決については :doc:`policy-resolvers` セクションをご覧ください。 - -ポリシーは **src/Policy** において、クラスの末尾に ``Policy`` をつけます。 -一旦例として、 `Article` エンティティのためのポリシーを作成します。 -**src/Policy/ArticlePolicy.php** に以下を追加します。 -:: - - id == $article->user_id; - } - -ポリシーメソッドは ``true`` か ``Result(真偽の結果)`` を返す必要があります。 -それ以外は失敗と解釈します。 - -認証していない場合、 ``$user`` は ``null`` が代入されます。 -匿名ユーザーに対するポリシーメソッドを失敗させたい場合、 ``IdentityInterface`` のType Hintingを使用することができます。. - -.. _policy-result-objects: - -ポリシーのResultオブジェクト -============================ - -ポリシーメソッドの返り値はBool以外にも ``Result`` オブジェクトを返すことができます。 -``Result`` オブジェクトはポリシーの成功・失敗について、より多くの情報を提供することができます。:: - - use Authorization\Policy\Result; - - public function canUpdate(IdentityInterface $user, Article $article) - { - if ($user->id == $article->user_id) { - return new Result(true); - } - // 失敗の理由を定義できる。 - return new Result(false, 'not-owner'); - } - -戻り値が ``true`` か ``ResultInterface`` 以外は失敗と解釈されます。 - -ポリシーのスコープ ------------------- - -ポリシーは認可の可否だけでなく、「スコープ」を定義することもできます。 -スコープメソッドは認可の条件を適用して他のオブジェクトを変更することができます。 -リストの取得を現在のユーザーに限定するときに最適です。:: - - namespace App\Policy; - - class ArticlesTablePolicy - { - public function scopeIndex($user, $query) - { - return $query->where(['Articles.user_id' => $user->getIdentifier()]); - } - } - -ポリシーの前提条件 ---------------------- - -ポリシーによっては、ポリシー内のすべての操作に共通のチェックを適用したい場合があります。 -全てのアクションを拒否する必要があるときに便利です。 -前提条件として ``BeforePolicyInterface`` をポリシーに追加する必要があります。:: - - namespace App\Policy; - - use Authorization\IdentityInterface; - use Authorization\Policy\BeforePolicyInterface; - use Authorization\Policy\ResultInterface; - - class ArticlesPolicy implements BeforePolicyInterface - { - public function before(?IdentityInterface $identity, mixed $resource, string $action): ResultInterface|bool|null - { - if ($identity->getOriginalData()->is_admin) { - return true; - } - // fall through - } - } - -3つの値を返却するbeforeが必要です。: - -- ``true`` 実行を許可します。 -- ``false`` 実行を拒否します。 -- ``null`` 判断できないので、authorizationメソッドが呼び出されます。 diff --git a/docs/ja/policy-resolvers.md b/docs/ja/policy-resolvers.md new file mode 100644 index 00000000..895f1183 --- /dev/null +++ b/docs/ja/policy-resolvers.md @@ -0,0 +1,64 @@ +# ポリシーリゾルバー + +ポリシーリゾルバーは、リソースオブジェクトと対応する policy クラスを +結びつけます。必要に応じて独自の resolver を実装できます。 + +組み込みの resolver: + +- `MapResolver` +- `OrmResolver` +- `ResolverCollection` + +## MapResolver を使う + +```php +use Authorization\Policy\MapResolver; + +$mapResolver = new MapResolver(); +$mapResolver->map(Article::class, ArticlePolicy::class); +$mapResolver->map(Article::class, new ArticlePolicy()); +$mapResolver->map(Article::class, function ($resource, $mapResolver) { + // Return a policy object. +}); +``` + +## OrmResolver を使う + +`OrmResolver` は CakePHP ORM 用の規約ベース resolver です。 + +1. policy は `App\Policy` に置きます。 +2. policy クラス名は `Policy` で終わります。 + +例: + +- `App\Model\Entity\Bookmark` -> `App\Policy\BookmarkPolicy` +- `App\Model\Table\ArticlesTable` -> `App\Policy\ArticlesTablePolicy` + +```php +use Authorization\Policy\OrmResolver; + +$appNamespace = 'App'; +$overrides = [ + 'Blog' => 'Cms', +]; + +$resolver = new OrmResolver($appNamespace, $overrides); +``` + +## ResolverCollection を使う + +```php +use Authorization\Policy\MapResolver; +use Authorization\Policy\OrmResolver; +use Authorization\Policy\ResolverCollection; + +$resolver = new ResolverCollection([ + new MapResolver(), + new OrmResolver(), +]); +``` + +## Resolver を作成する + +独自の実装が必要な場合は `Authorization\Policy\ResolverInterface` を +実装して `getPolicy($resource)` を定義します。 diff --git a/docs/ja/policy-resolvers.rst b/docs/ja/policy-resolvers.rst deleted file mode 100644 index d7d9cdc2..00000000 --- a/docs/ja/policy-resolvers.rst +++ /dev/null @@ -1,90 +0,0 @@ -ポリシーリゾルバー -################ - -リソースオブジェクトをそれぞれのポリシークラスにマッピングすることは、ポリシーリゾルバによって処理される動作である。 -いくつかのリゾルバを用意していますが、 ``AuthorizationPolicyResolverInterface`` を実装することで、独自のリゾルバを作成することができます。 -内部のリゾルバ: - -* ``MapResolver`` では、リソース名をそのポリシークラス名、またはオブジェクトやcallableにマッピングすることができます。 -* ``OrmResolver`` は、一般的なORMオブジェクトに対して、規約に基づいたポリシー解決を適用します。 -* ``ResolverCollection`` では、複数のリゾルバを集約して、順番に検索することができます。 - -MapResolver を使う -================= - -``MapResolver`` では、リソースクラス名をポリシークラス名、ポリシーオブジェクト、 -またはファクトリーコーラブルにマッピングすることができます。:: - - use Authorization\Policy\MapResolver; - - $mapResolver = new MapResolver(); - - // ポリシークラス名とリソースクラスの対応付け - $mapResolver->map(Article::class, ArticlePolicy::class); - - // policyインスタンスとリソースクラスの対応付け - $mapResolver->map(Article::class, new ArticlePolicy()); - - // ファクトリークラスとリソースクラスの対応付け - $mapResolver->map(Article::class, function ($resource, $mapResolver) { - // Return a policy object. - }); - -OrmResolverを使う -================= - -``OrmResolver`` は、CakePHP の ORM 用の規約ベースのポリシーリゾルバである。OrmResolver は以下の規約を適用する。: - -#. ポリシーは ``App\Policy`` に保存されます。 -#. ポリシークラスは末尾が ``Policy`` で終わります。 - -OrmResolverは、以下のオブジェクトタイプのポリシーを解決することができます。: - -* Entities - エンティティクラス名の使用 -* Tables - テーブルクラスの使用 -* Queries - クエリの ``repository()`` の戻り値を使用して、クラス名を取得する。 - -すべての場合において、以下のルールが適用されます。: - -#. リソースクラス名は、ポリシークラス名を生成するために使用されます。例えば、 ``AppModelEntity Filter`` は ``AppPolicy Filter`` にマップされます。 -#. プラグインリソースは、まずアプリケーションポリシーを確認する。 例えば ``App\Policy\Bookmarks\BookmarkPolicy`` は ``Bookmarks\Model\Entity\Bookmark``に向けて。 -#. アプリケーションオーバーライドポリシーが見つからない場合、プラグインポリシーがチェックされます。例えば ``Bookmarks\Policy\BookmarkPolicy`` - -テーブルオブジェクトの場合、クラス名の変換で ``AppModelTableTable`` が ``AppPolicyArticlesTablePolicy`` にマッピングされることになります。 -クエリーオブジェクトは ``repository()`` メソッドを呼び出され、その結果得られるテーブルクラスに基づいてポリシーが生成されます。 - -OrmResolver は、そのコンストラクタでカスタマイズをサポートします。:: - - use Authorization\Policy\OrmResolver; - - // カスタムアプリケーションの名前空間を使用する場合の変更。 - $appNamespace = 'App'; - - // Map policies in one namespace to another. - // Here we have mapped policies for classes in the ``Blog`` namespace to be - // found in the ``Cms`` namespace. - $overrides = [ - 'Blog' => 'Cms', - ]; - $resolver = new OrmResolver($appNamespace, $overrides) - -ResolverCollectionを使う -======================== - -``ResolverCollection`` では、複数のリゾルバをまとめることができます。:: - - use Authorization\Policy\ResolverCollection; - use Authorization\Policy\MapResolver; - use Authorization\Policy\OrmResolver; - - $ormResolver = new OrmResolver(); - $mapResolver = new MapResolver(); - - // マップリゾルバをチェックし、リソースが明示的にマップされていない場合は、オームリゾルバにフォールバックする。 - $resolver = new ResolverCollection([$mapResolver, $ormResolver]); - -Resolver を作成する -=================== - -独自のリゾルバを実装するには、 ``AuthorizationPolicyResolverInterface`` を実装し、 -``getPolicy($resource)`` メソッドを定義する必要があります。 diff --git a/docs/ja/request-authorization-middleware.md b/docs/ja/request-authorization-middleware.md new file mode 100644 index 00000000..e2489081 --- /dev/null +++ b/docs/ja/request-authorization-middleware.md @@ -0,0 +1,62 @@ +# リクエスト認証ミドルウェア + +このミドルウェアは、controller や action 単位で request 自体を認可したい +場合に使います。 + +`AuthorizationMiddleware` の後に追加してください。 + +## 使用方法 + +`src/Policy/RequestPolicy.php` を作成します。 + +```php +namespace App\Policy; + +use Authorization\Policy\RequestPolicyInterface; +use Cake\Http\ServerRequest; + +class RequestPolicy implements RequestPolicyInterface +{ + public function canAccess($identity, ServerRequest $request) + { + if ($request->getParam('controller') === 'Articles' + && $request->getParam('action') === 'index' + ) { + return true; + } + + return false; + } +} +``` + +`Application::getAuthorizationService()` で request を policy にマップします。 + +```php +use App\Policy\RequestPolicy; +use Authorization\AuthorizationService; +use Authorization\AuthorizationServiceInterface; +use Authorization\Middleware\AuthorizationMiddleware; +use Authorization\Middleware\RequestAuthorizationMiddleware; +use Authorization\Policy\MapResolver; +use Cake\Http\ServerRequest; +use Psr\Http\Message\ServerRequestInterface; + +public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface +{ + $mapResolver = new MapResolver(); + $mapResolver->map(ServerRequest::class, RequestPolicy::class); + + return new AuthorizationService($mapResolver); +} +``` + +```php +public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue +{ + $middlewareQueue->add(new AuthorizationMiddleware($this)); + $middlewareQueue->add(new RequestAuthorizationMiddleware()); + + return $middlewareQueue; +} +``` diff --git a/docs/ja/request-authorization-middleware.rst b/docs/ja/request-authorization-middleware.rst deleted file mode 100644 index 737732f4..00000000 --- a/docs/ja/request-authorization-middleware.rst +++ /dev/null @@ -1,73 +0,0 @@ -リクエスト認証ミドルウェア -################################ - -このミドルウェアは、例えば各コントローラーやアクションなどのリクエストを、 -ロールベースアクセスシステムや、 -特定のアクションへのアクセスを制御する他の種類の認可プロセスに対して認可したい場合に便利です。 -Authorization や RoutingMiddleware の **後に** 追加する必要があります - -リクエスト認可を処理するロジックは、リクエストポリシーに実装される。 -そこですべてのロジックを追加することもできるし、リクエストからの情報をACLやRBACの実装に渡すだけでいい。 - -使用方法 -======== - -リクエストオブジェクトを処理するためのポリシーを作成します。プラグインは実装可能なインターフェイスを同梱しています。 -まず、 **src/Policy/RequestPolicy.php** を作成し、以下を追加します。:: - - namespace App\Policy; - - use Authorization\Policy\RequestPolicyInterface; - use Cake\Http\ServerRequest; - - class RequestPolicy implements RequestPolicyInterface - { - /** - * Method to check if the request can be accessed - * - * @param \Authorization\IdentityInterface|null $identity Identity - * @param \Cake\Http\ServerRequest $request Server Request - * @return bool - */ - public function canAccess($identity, ServerRequest $request) - { - if ($request->getParam('controller') === 'Articles' - && $request->getParam('action') === 'index' - ) { - return true; - } - - return false; - } - } - -次に、 **src/Application.php** の ``Application::getAuthorizationService()`` 内で、リクエストクラスをポリシーにマッピングします。:: - - use App\Policy\RequestPolicy; - use Authorization\AuthorizationService; - use Authorization\AuthorizationServiceInterface; - use Authorization\AuthorizationServiceProviderInterface; - use Authorization\Middleware\AuthorizationMiddleware; - use Authorization\Middleware\RequestAuthorizationMiddleware; - use Authorization\Policy\MapResolver; - use Authorization\Policy\OrmResolver; - use Psr\Http\Message\ResponseInterface; - use Cake\Http\ServerRequest; - - - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface { - $mapResolver = new MapResolver(); - $mapResolver->map(ServerRequest::class, RequestPolicy::class); - return new AuthorizationService($mapResolver); - } - -RequestAuthorizationMiddlewareの読み込みが、AuthorizationMiddlewareの **後** であることを確認する。:: - - public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { - // other middleware... - // $middlewareQueue->add(new AuthenticationMiddleware($this)); - - // authorizationを加える (authenticationの後にね). - $middlewareQueue->add(new AuthorizationMiddleware($this)); - $middlewareQueue->add(new RequestAuthorizationMiddleware()); - } diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..44d78852 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2104 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@cakephp/docs-skeleton": "git+ssh://git@github.com:cakephp/docs-skeleton.git#node-package", + "vitepress": "^2.0.0-alpha.16" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cakephp/docs-skeleton": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#301764679a61f4e419e28e98bec9800de0fd4bce", + "bin": { + "cakedocs": "bin/cakedocs.js" + }, + "peerDependencies": { + "vitepress": "^2.0.0-alpha.15" + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.2.tgz", + "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.2.tgz", + "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==", + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz", + "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.76", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.76.tgz", + "integrity": "sha512-lLRlA8yaf+1L5VCPRvR9lynoSklsddKHEylchmZJKdj/q2xVQ1ZAEJ8SCQlv9cbgtMefnlyM98U+8Si2aoFZPA==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz", + "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.4", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.22.0", + "vite": "^7.3.1", + "vue": "^3.5.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..f1407730 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "", + "main": "config.js", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@cakephp/docs-skeleton": "git+ssh://git@github.com:cakephp/docs-skeleton.git#node-package", + "vitepress": "^2.0.0-alpha.16" + } +} diff --git a/docs/public/favicon/apple-touch-icon.png b/docs/public/favicon/apple-touch-icon.png new file mode 100644 index 00000000..c6d073d7 Binary files /dev/null and b/docs/public/favicon/apple-touch-icon.png differ diff --git a/docs/public/favicon/favicon-96x96.png b/docs/public/favicon/favicon-96x96.png new file mode 100644 index 00000000..6642e0cd Binary files /dev/null and b/docs/public/favicon/favicon-96x96.png differ diff --git a/docs/public/favicon/favicon.ico b/docs/public/favicon/favicon.ico new file mode 100644 index 00000000..405aa94c Binary files /dev/null and b/docs/public/favicon/favicon.ico differ diff --git a/docs/public/favicon/favicon.svg b/docs/public/favicon/favicon.svg new file mode 100644 index 00000000..805ef4b8 --- /dev/null +++ b/docs/public/favicon/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/docs/public/favicon/site.webmanifest b/docs/public/favicon/site.webmanifest new file mode 100644 index 00000000..4f23fb31 --- /dev/null +++ b/docs/public/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "CakePHP", + "short_name": "CakePHP", + "icons": [ + { + "src": "/favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/favicon/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/docs/public/favicon/web-app-manifest-192x192.png b/docs/public/favicon/web-app-manifest-192x192.png new file mode 100644 index 00000000..b5df2990 Binary files /dev/null and b/docs/public/favicon/web-app-manifest-192x192.png differ diff --git a/docs/public/favicon/web-app-manifest-512x512.png b/docs/public/favicon/web-app-manifest-512x512.png new file mode 100644 index 00000000..6a522de3 Binary files /dev/null and b/docs/public/favicon/web-app-manifest-512x512.png differ diff --git a/docs/public/fonts/cakedingbats-webfont.eot b/docs/public/fonts/cakedingbats-webfont.eot new file mode 100644 index 00000000..0800d1e7 Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.eot differ diff --git a/docs/public/fonts/cakedingbats-webfont.svg b/docs/public/fonts/cakedingbats-webfont.svg new file mode 100644 index 00000000..d2afda5e --- /dev/null +++ b/docs/public/fonts/cakedingbats-webfont.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/public/fonts/cakedingbats-webfont.ttf b/docs/public/fonts/cakedingbats-webfont.ttf new file mode 100644 index 00000000..78ad6c88 Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.ttf differ diff --git a/docs/public/fonts/cakedingbats-webfont.woff b/docs/public/fonts/cakedingbats-webfont.woff new file mode 100644 index 00000000..a95e1b38 Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.woff differ diff --git a/docs/public/fonts/cakedingbats-webfont.woff2 b/docs/public/fonts/cakedingbats-webfont.woff2 new file mode 100644 index 00000000..2cd9fdd0 Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.woff2 differ diff --git a/docs/public/history-panel-use.mp4 b/docs/public/history-panel-use.mp4 new file mode 100644 index 00000000..7f33c5d7 Binary files /dev/null and b/docs/public/history-panel-use.mp4 differ diff --git a/docs/public/history-panel.png b/docs/public/history-panel.png new file mode 100644 index 00000000..72e5d0f7 Binary files /dev/null and b/docs/public/history-panel.png differ diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 00000000..829c8e98 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,27 @@ + + + + diff --git a/docs/public/mail-panel.mp4 b/docs/public/mail-panel.mp4 new file mode 100644 index 00000000..e9f3375a Binary files /dev/null and b/docs/public/mail-panel.mp4 differ diff --git a/docs/public/mail-previewer.mp4 b/docs/public/mail-previewer.mp4 new file mode 100644 index 00000000..6958de49 Binary files /dev/null and b/docs/public/mail-previewer.mp4 differ