├── .github └── workflows │ └── nuxtjs.yml ├── .gitignore ├── README.md ├── app ├── app.config.ts ├── app.vue ├── components │ ├── AppFooter.vue │ ├── AppHeader.vue │ └── OgImage │ │ └── OgImageDocs.vue ├── error.vue ├── layouts │ └── docs.vue └── pages │ ├── [...slug].vue │ └── index.vue ├── content ├── 1.getting-started │ ├── 0.overview.md │ ├── 1.quickstart.md │ └── _dir.yml ├── 2.basics │ ├── 1.rules.md │ ├── 2.policies.md │ ├── 3.client.md │ └── _dir.yml └── index.yml ├── eslint.config.mjs ├── nuxt.config.ts ├── nuxt.schema.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── social-card.png ├── xefi-dark.svg └── xefi-light.svg ├── renovate.json ├── server ├── api │ └── search.json.get.ts └── tsconfig.json ├── tailwind.config.ts └── tsconfig.json /.github/workflows/nuxtjs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Nuxt site to Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Detect package manager 32 | id: detect-package-manager 33 | run: | 34 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 35 | echo "manager=yarn" >> $GITHUB_OUTPUT 36 | echo "command=install" >> $GITHUB_OUTPUT 37 | exit 0 38 | elif [ -f "${{ github.workspace }}/package.json" ]; then 39 | echo "manager=npm" >> $GITHUB_OUTPUT 40 | echo "command=ci" >> $GITHUB_OUTPUT 41 | exit 0 42 | else 43 | echo "Unable to determine package manager" 44 | exit 1 45 | fi 46 | - name: Setup Node 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: "20" 50 | cache: ${{ steps.detect-package-manager.outputs.manager }} 51 | - name: Install dependencies 52 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 53 | - name: Static HTML export with Nuxt 54 | env: 55 | NUXT_UI_PRO_LICENSE: ${{ secrets.NUXT_UI_PRO_LICENSE }} 56 | NUXT_PUBLIC_SITE_URL: ${{ secrets.NUXT_PUBLIC_SITE_URL }} 57 | NITRO_PRESET: github-pages 58 | run: ${{ steps.detect-package-manager.outputs.manager }} run generate 59 | - name: Upload artifact 60 | uses: actions/upload-pages-artifact@v3 61 | with: 62 | path: ./.output/public 63 | name: "github-pages" 64 | 65 | deploy: 66 | permissions: 67 | pages: write # to deploy to Pages 68 | id-token: write # to verify the deployment originates from an appropriate source 69 | environment: 70 | name: github-pages 71 | url: ${{ steps.deployment.outputs.page_url }} 72 | runs-on: ubuntu-latest 73 | needs: build 74 | steps: 75 | - name: Deploy to GitHub Pages 76 | id: deployment 77 | uses: actions/deploy-pages@v4 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entitled Documentation 2 | 3 | Powered by [Nuxt UI](https://ui.nuxt.co! 4 | 5 | [nuxt-ui-docs-social-card](https://github.com/nuxt-ui-pro/docs/assets/739984/f64e13d9-9ae0-4e03-bf7f-6be4c36cd9ba) 6 | 7 | # Nuxt UI Pro - Docs template 8 | 9 | [![Nuxt UI Pro](https://img.shields.io/badge/Made%20with-Nuxt%20UI%20Pro-00DC82?logo=nuxt.js&labelColor=020420)](https://ui.nuxt.com/pro) 10 | [![Nuxt Studio](https://img.shields.io/badge/Open%20in%20Nuxt%20Studio-18181B?&logo=nuxt.js&logoColor=3BB5EC)](https://nuxt.studio/themes/docs) 11 | 12 | - [Live demo](https://docs-template.nuxt.dev/) 13 | - [Play on Stackblitz](https://stackblitz.com/github/nuxt-ui-pro/docs) 14 | - [Documentation](https://ui.nuxt.com/pro/getting-started) 15 | - [Clone on Nuxt Studio](https://nuxt.studio/templates/docs) 16 | 17 | ## Quick Start 18 | 19 | ```bash [Terminal] 20 | npx nuxi init -t github:nuxt-ui-pro/docs 21 | ``` 22 | 23 | ## Setup 24 | 25 | Make sure to install the dependencies: 26 | 27 | ```bash 28 | # npm 29 | npm install 30 | 31 | # pnpm 32 | pnpm install 33 | 34 | # yarn 35 | yarn install 36 | 37 | # bun 38 | bun install 39 | ``` 40 | 41 | ## Development Server 42 | 43 | Start the development server on `http://localhost:3000`: 44 | 45 | ```bash 46 | # npm 47 | npm run dev 48 | 49 | # pnpm 50 | pnpm run dev 51 | 52 | # yarn 53 | yarn dev 54 | 55 | # bun 56 | bun run dev 57 | ``` 58 | 59 | ## Production 60 | 61 | Build the application for production: 62 | 63 | ```bash 64 | # npm 65 | npm run build 66 | 67 | # pnpm 68 | pnpm run build 69 | 70 | # yarn 71 | yarn build 72 | 73 | # bun 74 | bun run build 75 | ``` 76 | 77 | Locally preview production build: 78 | 79 | ```bash 80 | # npm 81 | npm run preview 82 | 83 | # pnpm 84 | pnpm run preview 85 | 86 | # yarn 87 | yarn preview 88 | 89 | # bun 90 | bun run preview 91 | ``` 92 | 93 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 94 | 95 | ## Nuxt Studio integration 96 | 97 | Add `@nuxthq/studio` dependency to your package.json: 98 | 99 | ```bash 100 | # npm 101 | npm install --save-dev @nuxthq/studio 102 | 103 | # pnpm 104 | pnpm add -D @nuxthq/studio 105 | 106 | # yarn 107 | yarn add -D @nuxthq/studio 108 | 109 | # bun 110 | bun add -d @nuxthq/studio 111 | ``` 112 | 113 | Add this module to your `nuxt.config.ts`: 114 | 115 | ```ts 116 | export default defineNuxtConfig({ 117 | ... 118 | modules: [ 119 | ... 120 | '@nuxthq/studio' 121 | ] 122 | }) 123 | ``` 124 | 125 | Read more on [Nuxt Studio docs](https://nuxt.studio/docs/get-started/setup). 126 | 127 | ## Renovate integration 128 | 129 | Install [Renovate GitHub app](https://github.com/apps/renovate/im/). 130 | -------------------------------------------------------------------------------- /app/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: 'red', 4 | gray: 'slate', 5 | footer: { 6 | bottom: { 7 | left: 'text-sm text-gray-500 dark:text-gray-400', 8 | wrapper: 'border-t border-gray-200 dark:border-gray-800' 9 | } 10 | } 11 | }, 12 | seo: { 13 | siteName: 'Entitled - Documentation' 14 | }, 15 | header: { 16 | logo: { 17 | alt: 'XEFI Logo', 18 | light: '/xefi-light.svg', 19 | dark: '/xefi-dark.svg' 20 | }, 21 | search: true, 22 | colorMode: true, 23 | links: [{ 24 | 'icon': 'i-simple-icons-github', 25 | 'to': 'https://github.com/xefi/python-entitled', 26 | 'target': '_blank', 27 | 'aria-label': 'Entitled on Github' 28 | }] 29 | }, 30 | footer: { 31 | credits: 'Copyright © ' + (new Date().getFullYear()), 32 | colorMode: false, 33 | links: [{ 34 | 'icon': 'i-simple-icons-github', 35 | 'to': 'https://github.com/xefi/python-entitled-docs', 36 | 'target': '_blank', 37 | 'aria-label': 'Doc on GitHub' 38 | }] 39 | }, 40 | toc: { 41 | title: 'Table of Contents', 42 | bottom: { 43 | title: 'Community', 44 | edit: 'https://github.com/xefi/python-entitled-docs/edit/main/content', 45 | links: [{ 46 | icon: 'i-heroicons-star', 47 | label: 'Star on GitHub', 48 | to: 'https://github.com/xefi/python-entitled', 49 | target: '_blank' 50 | }] 51 | } 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /app/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /app/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 51 | -------------------------------------------------------------------------------- /app/components/OgImage/OgImageDocs.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /app/error.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /app/layouts/docs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /app/pages/[...slug].vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 96 | -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 89 | -------------------------------------------------------------------------------- /content/1.getting-started/0.overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Welcome to Entitled! 3 | --- 4 | 5 | Entitled is a Python library that was designed with one core goal in mind : to provide a framework to easily organize, centralize and enforce their authorization logic. 6 | 7 | This library is written in pure Python. While it was initially designed with FastAPI in mind, it can be used in any context. 8 | 9 | Rules in Entitled are written as simple Python yourself. As such they can support any kind of authorization model you wish to implement, be it old-school ACL, RBAC, or others. 10 | 11 | You focus on building the logic however you want, Entitled takes care of the rest. 12 | -------------------------------------------------------------------------------- /content/1.getting-started/1.quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Getting started with Entitled. 3 | --- 4 | 5 | 6 | ## Installation 7 | 8 | Entitled can be installed with Pip, Poetry or uv. Other tools have not been tested. 9 | 10 | Entitled also requires Python >= 3.12. 11 | 12 | ```shell 13 | pip install entitled 14 | poetry install entitled 15 | uv add entitled 16 | ``` 17 | 18 | ## Basic usage 19 | 20 | Entitled revolves around two core elements. : 21 | 22 | - `Rules` are Python functions implementing your authorization logic. 23 | - `Policies` are sets of `Rules` relating to the same type of resource. 24 | 25 | You can declare a new `Policy` on the `Post` type like so: 26 | 27 | ```py 28 | import entitled 29 | 30 | post_policy = entitled.Policy[Post]() 31 | ``` 32 | 33 | A `Policy` provides a decorator to easily register rules. You only need to tack it onto a Python function implementing some authorization logic. You will also need to provide it with a name. 34 | 35 | ```py 36 | @post_policy.rule 37 | def can_edit_post(actor, resource: Post, context) -> bool: 38 | return resource.owner == actor 39 | ``` 40 | 41 | ### > [!WARNING]Rule specification 42 | 43 | To be a valid rule, a function must follow a specific signature defined by Entitled. It must be: 44 | 45 | - An async callable 46 | - Accept an 'actor' positional parameter 47 | - Return a boolean or a `Response`` object 48 | 49 | Rules may define any number of additional parameters. 50 | 51 | ## Making authorization decisions 52 | 53 | ### The Client 54 | 55 | While you can *technically* call Policies directly, you will want to use an instance of `Client` class. 56 | 57 | ```py 58 | import entitled 59 | 60 | auth_client = entitled.Client() 61 | ``` 62 | 63 | This client will serve as your single source of truth for all your authorization decisions. Once it is declared, you can register policies on it using the `register()` method. 64 | 65 | ```python 66 | auth_client.register(post_policy) 67 | ``` 68 | 69 | ::callout{icon="i-heroicons-exclamation-triangle" color="amber"} 70 | Manually registering policies may sound cumbersome... and rightfully so. Entitled provides a mechanism to auto-discover and register policies. More on that later. 71 | :: 72 | 73 | #### Making decisions 74 | 75 | Once your policies are registered, making authorization decisions is extremely simple! The `Client` object provides the `allows` function to evaluate a rule. This function takes as parameters the name of the rule you want to authorize, an actor, a resource and optionally a context dictionary. 76 | 77 | ```py 78 | if auth_client.allows("edit", actor, resource): 79 | # Your application logic... 80 | 81 | ``` 82 | 83 | The `allows` functions returns a boolean indicating whether the actor can perform the given action. 84 | 85 | If you want, you can also make use of the `authorize` methods that will automatically raise an `AuthorizationException` if the user is not allowed to perform the action. Both function are called the exact same way. 86 | 87 | ## Wrapping things up 88 | 89 | That's it for the absolute basics of Entitled! But we barely scratched the surface here, as the library offers many more features, including but not limited to : 90 | 91 | - Resource-bound policies, or how to group your rules around a specific resource type. 92 | - Auto-discovery of policies. 93 | - Answering the question "*What* can this actor do?" 94 | 95 | Check out the detailed documentation! 96 | -------------------------------------------------------------------------------- /content/1.getting-started/_dir.yml: -------------------------------------------------------------------------------- 1 | title: 'Getting Started' 2 | icon: ph:star-duotone 3 | navigation.redirect: /getting-started/overview 4 | -------------------------------------------------------------------------------- /content/2.basics/1.rules.md: -------------------------------------------------------------------------------- 1 | Rules are the basic building blocks of Entitled. Fundamentally, a `Rule` is a 2 | simple wrapper around a (somewhat specific) Python function. 3 | 4 | Semantically, Rules describes "facts" about your application and about the relationships 5 | between the various actors and resources of your app. 6 | 7 | If you have ever used Laravel's authorization API, this API is very much inspired by it. 8 | 9 | ## Writing Rules 10 | 11 | To make a new rule, you just need a Python `Callable` that satisfies three criteria: 12 | 13 | - It must be an async Callable 14 | - It should accept at least one positional parameter for the actor 15 | - It should return either a boolean or a Response object. 16 | 17 | These are the minimum requirements to create a rule. Additionally, if the `Callable` used 18 | is anonymous, you will need to provide a name for the rule: 19 | 20 | ```py 21 | # With a lambda 22 | is_admin = Rule("is_admin", lambda actor: has_admin_status(actor)) 23 | 24 | # With a declared callable 25 | async def is_admin(actor: Actor): 26 | return has_admin_status(actor) 27 | 28 | rule_is_admin = Rule(is_admin) 29 | ``` 30 | 31 | Note : once we tackle policies, you'll be provided a third way to create a rule through policies. 32 | 33 | ## Using Rules 34 | 35 | Rules are `Callable` object themselves that defer to the underlying function, so using them may be ass simple as calling them on the relevant objects 36 | 37 | ```py 38 | if is_admin(user, post): 39 | print("User authorized") 40 | ``` 41 | 42 | However, the `Rule` class also provides several utility functions : 43 | 44 | - `Rule.inspect` will always wrap the result into a `Response` object, supplying a default error message. 45 | - `Rule.allows` does the opposite and systematically returns a boolean (it is equivalent to using `rule.inspect().allowed()`) 46 | - `Rule.denies` is equivalent to `not rule.allows()` 47 | - `Rule.authorize` evaluates the rule, but returns an exception in case if the evaluation would return an error. The exception essentially calls `inspect` and wraps any error message into an `AuthorizationException`. 48 | 49 | ## A Warning 50 | 51 | Rules are meant to be extremely flexible in their definition, but you'll notice that the evaluation provided by the `Rule` class don't retain the signature from the underlying Callable 52 | 53 | In most cases, you may pass any parameters to an evaluation function, and Entitled will match the parameters to the signature of the underlying function, thus preventing any errors for extra parameters. This system is still quite unoptimized : there is one massive footgun that we'll discover when discussing the `Client`. 54 | -------------------------------------------------------------------------------- /content/2.basics/2.policies.md: -------------------------------------------------------------------------------- 1 | Policies provide a way to group your authorization logic around a particular resource. 2 | 3 | A policy is essentially a registry of rules relating to the same resource type. 4 | 5 | ## Declaring a Policy 6 | 7 | The policy constructor does not take any parameter, but take a type argument that will define which resource this Policy refers to. 8 | 9 | That's it! Once you have a policy created, you may start registering rules. 10 | 11 | ```py 12 | post_policy = Policy[Post]() 13 | ``` 14 | 15 | ## Adding Rules to your Policy 16 | 17 | To add a rule to your policy, you have two options: 18 | 19 | You may first take a previously defined `Rule` and call the `register` method on your policy. This method calls for 20 | a name and a Rule object: 21 | 22 | ```py 23 | async def can_edit(actor: User, post: Post): 24 | return post.owner == actor 25 | 26 | post_policy = Policy[Post]() 27 | post_policy.register("update", can_edit) 28 | 29 | ``` 30 | 31 | The other, and arguably more practical option, is to used the `rule` decorator provided by your new policy: 32 | 33 | ```py 34 | post_policy = Policy[Post]() 35 | 36 | @policy.rule 37 | async def update(actor: User, post: Post): 38 | return post.owner == actor 39 | ``` 40 | 41 | This will automatically take your function, create a new rule from it, then register it on your policy. 42 | 43 | ### A warning 44 | 45 | Be aware that registering two rules with the same name or action name will simply overwrite the former rule defined for this name: 46 | 47 | ```py 48 | async def can_edit(actor: User, post: Post): 49 | return post.owner == actor 50 | async def can_read(actor: User, post: Post): 51 | return post.owner == actor || actor in post.guests 52 | 53 | post_policy.register("update", can_edit) 54 | post_policy.register("update", can_read) # Overwrites the previously defined rule 55 | ``` 56 | 57 | ## Using a policy 58 | 59 | The `Policy` class exposes the same evaluation functions as the `Rule` class does, but requires that you pass in an action name as well. Here are few examples: 60 | 61 | ```py 62 | if post_policy.allows('update', actor, post): 63 | # The action is authorized... 64 | else: 65 | # The action is forbidden 66 | ``` 67 | 68 | ```py 69 | response = post_policy.inspect("update", actor, post) 70 | match response: 71 | case Ok(_): 72 | # The action is authorized 73 | case Err(msg): 74 | # The action is forbidden 75 | ``` 76 | 77 | ```py 78 | post_policy.authorize("update", actor, post) 79 | # The action is authorized 80 | ``` 81 | 82 | On top of these calls, a policy also exposes a `grants` call. This one is a bit different as it does not take an action name as parameter, but instead will evaluate all rules in the policy for the given set of parameters provided to the function and return the result as a dictionary. 83 | 84 | For example: 85 | 86 | ```py 87 | policy = Policy[Post]() 88 | @policy.rule 89 | async def create(actor: User): 90 | return True 91 | 92 | @policy.rule 93 | async def view(actor: User, post: Post): 94 | return post.owner == user | user in post.access_list 95 | 96 | @policy.rule 97 | async def update(actor: User, post: Post): 98 | return post.owner == actor 99 | 100 | print(policy.grants(post_owner, post)) 101 | # { 102 | # "create": True, 103 | # "view": True, 104 | # "update": True, 105 | # } 106 | print(policy.grants(post_reader, post)) 107 | # { 108 | # "create": True, 109 | # "view": True, 110 | # "update": False, 111 | # } 112 | ``` 113 | -------------------------------------------------------------------------------- /content/2.basics/3.client.md: -------------------------------------------------------------------------------- 1 | The `Client` object is designed to act as the source of truth and decision-making point of your application. 2 | 3 | As a general rule, you should endeavor to use that Client object to make any authorization decision, not 4 | going through specific policies or rules. 5 | 6 | ## Accessing the Client 7 | 8 | The `Client` class takes a single optional parameters : a path to the folder containing your policies files. 9 | 10 | You may place all of your policies in a single file or split them into multiple files in the same folder, it doesn't matter. Upon initialization, if the `path` parameter is provided, the `Client` will inspect that folder and register any `Policy` instance it finds. 11 | 12 | You may also manually load policies after the initialization of the client, by calling `load_policies()` on it with a path parameter. 13 | 14 | ## Using the Client 15 | 16 | The `Client` class exposes the same evaluation functions as the `Policy` class, with one little twist: 17 | These function must now receive a `resource` positional argument that may be an instance of a class, or the class itself. 18 | 19 | This argument will first be used by the client to determine which policy should be used for this resource, then will 20 | be passed down all the way to the actual rule evaluation. 21 | As such, passing a class when the rule would expect an instance will cause Entitled to correctly determine which rule should be evaluated, but the evaluation itself will fail: 22 | 23 | ```py 24 | policy = Policy[Post]() 25 | 26 | @rule.policy 27 | async def view(actor: Actor, post: Post): 28 | ... 29 | 30 | client = Client() 31 | await client.authorize(actor, Post) # The client will resolve this to the proper policy, but the evaluation itself will fail 32 | 33 | ``` 34 | 35 | Aside from this, the `authorize`, `allows`, `denies`, `inspect` and `grants` functions work like their counterpart on `Policy` and in fact defer to these functions under the hood. 36 | 37 | ## [!WARNING] A potential (and temporary) foot-gun with Client 38 | 39 | As we discussed in the "rules" section, the flexibility allowed when defining `Rules` and when calling evaluation function is still a bit unrefined and may come with a few potholes. 40 | 41 | In particular, one such pothole that arises from the current implementation is that, when defining rules that don't make use of the 'resource' field but require additional fields, `Client` functions will pass the resource object as an argument to the rule evaluation, and if no precaution is taken, will pass it down as the additional parameter. Consider this: 42 | 43 | ```py 44 | policy = Policy[Post]() 45 | 46 | @policy.rule 47 | async def update(user: User, context: dict[str, Any]): 48 | return "some_user_list" in context and user in context["some_user_list"]: 49 | 50 | client.authorize("update", post, context) # current implementation raises an error in this case. 51 | ``` 52 | 53 | A possible workaround in this case to define such rules like this: 54 | 55 | ```py 56 | @policy.rule 57 | async def update(user: User, *, context: dict[str, Any]): 58 | return "some_user_list" in context and user in context["some_user_list"]: 59 | ``` 60 | 61 | This will ensure that the rule only accepts `context` as a keyword argument. You may then call you method like this: 62 | 63 | ```py 64 | client.authorize("update", post, context=context) # This will evaluate correctly 65 | ``` 66 | -------------------------------------------------------------------------------- /content/2.basics/_dir.yml: -------------------------------------------------------------------------------- 1 | title: 'Basics' 2 | icon: heroicons-outline:bookmark-alt 3 | navigation.redirect: /basics/core-concepts 4 | -------------------------------------------------------------------------------- /content/index.yml: -------------------------------------------------------------------------------- 1 | title: "Entitled - A library for authorization enforcement" 2 | description: A fairly simple library with one goal - make enforcing your authorization logic as easy as possible, letting the developpers focus on their model and logic. 3 | navigation: false 4 | hero: 5 | title: "A library for authorization enforcement." 6 | description: "Helps you organize and enforce authorization so you can focus on the decision-making logic." 7 | orientation: horizontal 8 | links: 9 | - label: Get started 10 | icon: i-heroicons-arrow-right-20-solid 11 | trailing: true 12 | to: "/getting-started/overview" 13 | size: lg 14 | - label: Source Code 15 | icon: i-simple-icons-github 16 | size: lg 17 | color: gray 18 | to: https://github.com/xefi/python-entitled 19 | target: _blank 20 | code: | 21 | ```bash [Terminal] 22 | pip install entitled 23 | 24 | # with poetry 25 | poetry install entitled 26 | ``` 27 | features: 28 | title: "Key features" 29 | items: 30 | - title: "Model-agnostic" 31 | icon: "i-mdi-cable-data" 32 | description: "RBAC, ABAC, ACL... you choose the model, Entitled enforces it." 33 | - title: "Easy organization" 34 | icon: "i-mdi-cable-data" 35 | description: "Policies and Rules neatly organize your decision logic." 36 | - title: "Single decision point" 37 | icon: "i-mdi-cable-data" 38 | description: "A single entrypoint for your authorization decisions." 39 | basic_usages: 40 | - title: "Define your resource and actors, on your terms." 41 | code: | 42 | ```python 43 | #resources.py 44 | 45 | class User: 46 | id: int 47 | 48 | class Post: 49 | id: int 50 | owner: User 51 | ``` 52 | - title: "Create a policy for your resource." 53 | code: | 54 | ```python 55 | # policies.py 56 | 57 | import entitled 58 | 59 | post_policy = entitled.Policy[Post]() 60 | ``` 61 | - title: "Rules encapsulate your logic." 62 | code: | 63 | ```python 64 | # policies.py 65 | 66 | @post_policy.rule 67 | async def can_edit_post(actor: User, resource: Post) -> bool: 68 | return resource.owner == actor 69 | ``` 70 | - title: "Enforce your policies." 71 | code: | 72 | ```python 73 | client = entitled.Client() 74 | client.register(post_policy) 75 | 76 | if await client.allows("edit", user, post): 77 | # your business logic 78 | ``` 79 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import withNuxt from './.nuxt/eslint.config.mjs' 3 | 4 | export default withNuxt( 5 | // Your custom configs here 6 | ) 7 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | extends: ['@nuxt/ui-pro'], 4 | 5 | modules: [ 6 | '@nuxt/content', 7 | '@nuxt/eslint', 8 | '@nuxt/fonts', 9 | '@nuxt/image', 10 | '@nuxt/ui', 11 | '@nuxthq/studio', 12 | 'nuxt-og-image' 13 | ], 14 | 15 | hooks: { 16 | // Define `@nuxt/ui` components as global to use them in `.md` (feel free to add those you need) 17 | 'components:extend': (components) => { 18 | const globals = components.filter(c => ['UButton', 'UIcon'].includes(c.pascalName)) 19 | 20 | globals.forEach(c => c.global = true) 21 | } 22 | }, 23 | 24 | colorMode: { 25 | disableTransition: true 26 | }, 27 | 28 | nitro: { 29 | prerender: { 30 | routes: [ 31 | '/' 32 | ], 33 | crawlLinks: true 34 | } 35 | }, 36 | 37 | content: { 38 | highlight: { 39 | langs: ['python'] 40 | } 41 | }, 42 | 43 | routeRules: { 44 | '/api/search.json': { prerender: true } 45 | }, 46 | 47 | devtools: { 48 | enabled: true 49 | }, 50 | 51 | typescript: { 52 | strict: false 53 | }, 54 | 55 | future: { 56 | compatibilityVersion: 4 57 | }, 58 | 59 | eslint: { 60 | config: { 61 | stylistic: { 62 | commaDangle: 'never', 63 | braceStyle: '1tbs' 64 | } 65 | } 66 | }, 67 | 68 | compatibilityDate: '2024-07-11' 69 | }) 70 | -------------------------------------------------------------------------------- /nuxt.schema.ts: -------------------------------------------------------------------------------- 1 | import { field, group } from '@nuxthq/studio/theme' 2 | 3 | export default defineNuxtSchema({ 4 | appConfig: { 5 | ui: group({ 6 | title: 'UI', 7 | description: 'UI Customization.', 8 | icon: 'i-mdi-palette-outline', 9 | fields: { 10 | icons: group({ 11 | title: 'Icons', 12 | description: 'Manage icons used in UI Pro.', 13 | icon: 'i-mdi-application-settings-outline', 14 | fields: { 15 | search: field({ 16 | type: 'icon', 17 | title: 'Search Bar', 18 | description: 'Icon to display in the search bar.', 19 | icon: 'i-mdi-magnify', 20 | default: 'i-heroicons-magnifying-glass-20-solid' 21 | }), 22 | dark: field({ 23 | type: 'icon', 24 | title: 'Dark mode', 25 | description: 'Icon of color mode button for dark mode.', 26 | icon: 'i-mdi-moon-waning-crescent', 27 | default: 'i-heroicons-moon-20-solid' 28 | }), 29 | light: field({ 30 | type: 'icon', 31 | title: 'Light mode', 32 | description: 'Icon of color mode button for light mode.', 33 | icon: 'i-mdi-white-balance-sunny', 34 | default: 'i-heroicons-sun-20-solid' 35 | }), 36 | external: field({ 37 | type: 'icon', 38 | title: 'External Link', 39 | description: 'Icon for external link.', 40 | icon: 'i-mdi-arrow-top-right', 41 | default: 'i-heroicons-arrow-up-right-20-solid' 42 | }), 43 | chevron: field({ 44 | type: 'icon', 45 | title: 'Chevron', 46 | description: 'Icon for chevron.', 47 | icon: 'i-mdi-chevron-down', 48 | default: 'i-heroicons-chevron-down-20-solid' 49 | }), 50 | hash: field({ 51 | type: 'icon', 52 | title: 'Hash', 53 | description: 'Icon for hash anchors.', 54 | icon: 'i-ph-hash', 55 | default: 'i-heroicons-hashtag-20-solid' 56 | }) 57 | } 58 | }), 59 | primary: field({ 60 | type: 'string', 61 | title: 'Primary', 62 | description: 'Primary color of your UI.', 63 | icon: 'i-mdi-palette-outline', 64 | default: 'green', 65 | required: ['sky', 'mint', 'rose', 'amber', 'violet', 'emerald', 'fuchsia', 'indigo', 'lime', 'orange', 'pink', 'purple', 'red', 'teal', 'yellow', 'green', 'blue', 'cyan', 'gray', 'white', 'black'] 66 | }), 67 | gray: field({ 68 | type: 'string', 69 | title: 'Gray', 70 | description: 'Gray color of your UI.', 71 | icon: 'i-mdi-palette-outline', 72 | default: 'slate', 73 | required: ['slate', 'cool', 'zinc', 'neutral', 'stone'] 74 | }) 75 | } 76 | }), 77 | seo: group({ 78 | title: 'SEO', 79 | description: 'SEO configuration.', 80 | icon: 'i-ph-app-window', 81 | fields: { 82 | siteName: field({ 83 | type: 'string', 84 | title: 'Site Name', 85 | description: 'Name used in ogSiteName and used as second part of your page title (My page title - Nuxt UI Pro).', 86 | icon: 'i-mdi-web', 87 | default: [] 88 | }) 89 | } 90 | }), 91 | header: group({ 92 | title: 'Header', 93 | description: 'Header configuration.', 94 | icon: 'i-mdi-page-layout-header', 95 | fields: { 96 | logo: group({ 97 | title: 'Logo', 98 | description: 'Header logo configuration.', 99 | icon: 'i-mdi-image-filter-center-focus-strong-outline', 100 | fields: { 101 | light: field({ 102 | type: 'media', 103 | title: 'Light Mode Logo', 104 | description: 'Pick an image from your gallery.', 105 | icon: 'i-mdi-white-balance-sunny', 106 | default: '' 107 | }), 108 | dark: field({ 109 | type: 'media', 110 | title: 'Dark Mode Logo', 111 | description: 'Pick an image from your gallery.', 112 | icon: 'i-mdi-moon-waning-crescent', 113 | default: '' 114 | }), 115 | alt: field({ 116 | type: 'string', 117 | title: 'Alt', 118 | description: 'Alt to display for accessibility.', 119 | icon: 'i-mdi-alphabet-latin', 120 | default: '' 121 | }) 122 | } 123 | }), 124 | search: field({ 125 | type: 'boolean', 126 | title: 'Search Bar', 127 | description: 'Hide or display the search bar.', 128 | icon: 'i-mdi-magnify', 129 | default: true 130 | }), 131 | colorMode: field({ 132 | type: 'boolean', 133 | title: 'Color Mode', 134 | description: 'Hide or display the color mode button in your header.', 135 | icon: 'i-mdi-moon-waning-crescent', 136 | default: true 137 | }), 138 | links: field({ 139 | type: 'array', 140 | title: 'Links', 141 | description: 'Array of link object displayed in header.', 142 | icon: 'i-mdi-link-variant', 143 | default: [] 144 | }) 145 | } 146 | }), 147 | footer: group({ 148 | title: 'Footer', 149 | description: 'Footer configuration.', 150 | icon: 'i-mdi-page-layout-footer', 151 | fields: { 152 | credits: field({ 153 | type: 'string', 154 | title: 'Footer credits section', 155 | description: 'Text to display as credits in the footer.', 156 | icon: 'i-mdi-circle-edit-outline', 157 | default: '' 158 | }), 159 | colorMode: field({ 160 | type: 'boolean', 161 | title: 'Color Mode', 162 | description: 'Hide or display the color mode button in the footer.', 163 | icon: 'i-mdi-moon-waning-crescent', 164 | default: false 165 | }), 166 | links: field({ 167 | type: 'array', 168 | title: 'Links', 169 | description: 'Array of link object displayed in footer.', 170 | icon: 'i-mdi-link-variant', 171 | default: [] 172 | }) 173 | } 174 | }), 175 | toc: group({ 176 | title: 'Table of contents', 177 | description: 'TOC configuration.', 178 | icon: 'i-mdi-table-of-contents', 179 | fields: { 180 | title: field({ 181 | type: 'string', 182 | title: 'Title', 183 | description: 'Text to display as title of the main toc.', 184 | icon: 'i-mdi-format-title', 185 | default: '' 186 | }), 187 | bottom: group({ 188 | title: 'Bottom', 189 | description: 'Bottom TOC configuration.', 190 | icon: 'i-mdi-table-of-contents', 191 | fields: { 192 | title: field({ 193 | type: 'string', 194 | title: 'Title', 195 | description: 'Text to display as title of the bottom toc.', 196 | icon: 'i-mdi-format-title', 197 | default: '' 198 | }), 199 | edit: field({ 200 | type: 'string', 201 | title: 'Edit Page Link', 202 | description: 'URL of your repository content folder.', 203 | icon: 'i-ph-note-pencil', 204 | default: '' 205 | }), 206 | links: field({ 207 | type: 'array', 208 | title: 'Links', 209 | description: 'Array of link object displayed in bottom toc.', 210 | icon: 'i-mdi-link-variant', 211 | default: [] 212 | }) 213 | } 214 | }) 215 | } 216 | }) 217 | } 218 | }) 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-entitled-doc", 3 | "version": "0.2.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "nuxt build", 8 | "dev": "nuxt dev", 9 | "generate": "nuxt generate", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare", 12 | "lint": "eslint .", 13 | "typecheck": "nuxt typecheck" 14 | }, 15 | "dependencies": { 16 | "@iconify-json/heroicons": "^1.2.0", 17 | "@iconify-json/simple-icons": "^1.2.4", 18 | "@nuxt/content": "^2.13.2", 19 | "@nuxt/fonts": "^0.9.2", 20 | "@nuxt/image": "^1.8.0", 21 | "@nuxt/ui-pro": "^1.4.3", 22 | "nuxt": "^3.13.2", 23 | "nuxt-og-image": "^3.0.4" 24 | }, 25 | "devDependencies": { 26 | "@nuxt/eslint": "^0.5.7", 27 | "@nuxthq/studio": "^2.1.1", 28 | "eslint": "^9.11.1", 29 | "vue-tsc": "^2.1.6" 30 | }, 31 | "resolutions": { 32 | "@nuxtjs/tailwindcss": "nightly" 33 | }, 34 | "packageManager": "pnpm@9.11.0" 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xefi/python-entitled-doc/fa1b459ba214f9196f7c0bc1d7b73397275239c0/public/favicon.ico -------------------------------------------------------------------------------- /public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xefi/python-entitled-doc/fa1b459ba214f9196f7c0bc1d7b73397275239c0/public/social-card.png -------------------------------------------------------------------------------- /public/xefi-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/xefi-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>nuxt/renovate-config-nuxt" 4 | ], 5 | "lockFileMaintenance": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/api/search.json.get.ts: -------------------------------------------------------------------------------- 1 | import { serverQueryContent } from '#content/server' 2 | 3 | export default eventHandler(async (event) => { 4 | return serverQueryContent(event).where({ _type: 'markdown', navigation: { $ne: false } }).find() 5 | }) 6 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import defaultTheme from 'tailwindcss/defaultTheme' 3 | 4 | export default >{ 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | sans: ['DM Sans', ...defaultTheme.fontFamily.sans] 9 | }, 10 | colors: { 11 | green: { 12 | 50: '#EFFDF5', 13 | 100: '#D9FBE8', 14 | 200: '#B3F5D1', 15 | 300: '#75EDAE', 16 | 400: '#00DC82', 17 | 500: '#00C16A', 18 | 600: '#00A155', 19 | 700: '#007F45', 20 | 800: '#016538', 21 | 900: '#0A5331', 22 | 950: '#052e16' 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------