{"id":22208406,"url":"https://github.com/topaxi/ng-ability","last_synced_at":"2026-05-04T02:38:29.755Z","repository":{"id":142155394,"uuid":"156282940","full_name":"topaxi/ng-ability","owner":"topaxi","description":"Access Control for Angular","archived":false,"fork":false,"pushed_at":"2026-04-30T12:18:24.000Z","size":418,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-04-30T14:15:00.810Z","etag":null,"topics":["access","acl","angular","can","cancan","permissions"],"latest_commit_sha":null,"homepage":"https://topaxi.github.io/ng-ability/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/topaxi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-11-05T21:05:18.000Z","updated_at":"2026-04-30T12:18:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"fc0bb31f-a569-4c1f-9750-1d0ac9696fe5","html_url":"https://github.com/topaxi/ng-ability","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/topaxi/ng-ability","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/topaxi%2Fng-ability","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/topaxi%2Fng-ability/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/topaxi%2Fng-ability/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/topaxi%2Fng-ability/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/topaxi","download_url":"https://codeload.github.com/topaxi/ng-ability/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/topaxi%2Fng-ability/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32592718,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access","acl","angular","can","cancan","permissions"],"created_at":"2024-12-02T19:18:58.048Z","updated_at":"2026-05-04T02:38:29.748Z","avatar_url":"https://github.com/topaxi.png","language":"TypeScript","funding_links":[],"categories":["Security and Authentication"],"sub_categories":["Role-Based Access Control"],"readme":"# ng-ability\n\nDefine access control lists in Angular.\n\n## Installation\n\n```bash\nnpm install --save ng-ability\n```\n\n## Usage\n\n### 1. Define an ability context\n\nThe ability context provides the current user (or other subject) to your ability checks:\n\n```typescript\nimport { Injectable, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop'\nimport { AbilityContext } from 'ng-ability';\n\n@Injectable({ providedIn: 'root' })\nexport class AbilityUserContext implements AbilityContext\u003cUser\u003e {\n  readonly #auth = inject(AuthService);\n\n  readonly abilityContext = toSignal(this.#auth.getCurrentUser());\n}\n```\n\nIf your auth service uses signals, you can directly use them:\n\n```typescript\n@Injectable({ providedIn: 'root' })\nexport class AbilityUserContext implements AbilityContext\u003cUser\u003e {\n  readonly #auth = inject(AuthService);\n\n  // If auth.currentUser is already a Signal\u003cUser | null\u003e\n  readonly abilityContext = this.#auth.currentUser;\n}\n```\n\n### 2. Define abilities\n\nDefine abilities for pages, models, and other data:\n\n```typescript\nimport { AbilityFor, Ability } from 'ng-ability';\n\n// Define ability for Article instance objects, the string 'Article'\n// and graphql like objects using a matching function\n@AbilityFor(Article, 'Article', article =\u003e article.__typename === 'Article')\nexport class ArticleAbility implements Ability\u003cUser, Article\u003e {\n  can(currentUser: User | null, action: string, article: Article) {\n    if (currentUser != null \u0026\u0026 currentUser.admin) {\n      // Admins can do anything\n      return true;\n    }\n\n    switch (action) {\n      case 'view': // Everyone can view articles\n        return true;\n      case 'create': // Every user can create new articles\n        return currentUser != null;\n      case 'edit': // Users can only edit their own articles\n        return currentUser != null \u0026\u0026 currentUser.id === article.authorId;\n      default:\n        return false;\n    }\n  }\n}\n\n@AbilityFor('AdminArea')\nexport class AdminAreaAbility implements Ability\u003cUser\u003e {\n  can(currentUser: User | null, action: string) {\n    switch (action) {\n      case 'view': // Only admins can view the admin area\n        return currentUser != null \u0026\u0026 currentUser.admin;\n      default:\n        return false;\n    }\n  }\n}\n```\n\n### 3. Register abilities\n\n**Standalone (recommended):**\n\n```typescript\nimport { bootstrapApplication } from '@angular/platform-browser';\nimport { provideAbilities } from 'ng-ability';\nimport { AbilityUserContext } from './ability-user-context';\nimport { ArticleAbility } from './abilities/article.ability';\nimport { AdminAreaAbility } from './abilities/admin-area.ability';\n\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideAbilities(AbilityUserContext, [ArticleAbility, AdminAreaAbility]),\n  ],\n});\n```\n\n**NgModule:**\n\n```typescript\nimport { NgModule } from '@angular/core';\nimport { NgAbilityModule } from 'ng-ability';\n\n@NgModule({\n  imports: [\n    NgAbilityModule.withAbilities(AbilityUserContext, [\n      ArticleAbility,\n      AdminAreaAbility,\n    ]),\n  ],\n})\nexport class AppModule {}\n```\n\n### 4. Check abilities\n\n**In templates** using the `can` pipe (import `CanPipe` or `NgAbilityModule`):\n\n```html\n@if ('Article' | can: 'create') {\n  I can create new articles!\n}\n\n@if (latestArticle | can: 'edit') {\n  \u003cbutton (click)=\"editArticle(latestArticle)\"\u003eEdit latest article\u003c/button\u003e\n} @else {\n  \u003cdiv\u003eLatest article is not editable :(\u003c/div\u003e\n}\n```\n\nWhen an entity can be identified on its own — for example via `instanceof` or a\nfield like `__typename` in GraphQL responses — passing it directly is enough.\nIf the entity cannot be inferred, you can pass an explicit matcher as the pipe\nvalue and the entity as a third argument:\n\n```html\n@if ('Article' | can: 'read' : draftArticle) {\n  I can read this draft article!\n}\n```\n\nAlternatively, you can use the `*can` structural directive (import `CanDirective` or `NgAbilityModule`):\n\n```html\n\u003cdiv *can=\"['Article', 'create']\"\u003e\n  I can create new articles!\n\u003c/div\u003e\n\u003cdiv *can=\"['Article', 'edit', latestArticle]; else noteditable\"\u003e\n  \u003cbutton (click)=\"editArticle(latestArticle)\"\u003eEdit latest article\u003c/button\u003e\n\u003c/div\u003e\n\u003cng-template #noteditable\u003e\n  \u003cdiv\u003eLatest article is not editable :(\u003c/div\u003e\n\u003c/ng-template\u003e\n```\n\n**In code** using the `NgAbilityService`:\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { NgAbilityService } from 'ng-ability';\n\n@Component({ ... })\nexport class AppComponent {\n  readonly #ability = inject(NgAbilityService);\n\n  editArticle(article: Article) {\n    // When the entity can be inferred (e.g., via instanceof)\n    if (this.#ability.can(article, 'edit')) {\n      // edit article...\n    }\n  }\n\n  createNewArticle() {\n    // When checking a string matcher without an entity\n    if (this.#ability.can('Article', 'create')) {\n      // create article...\n    }\n  }\n\n  editDraftArticle(draftArticle: unknown) {\n    // When explicit matcher is needed\n    if (this.#ability.can('Article', 'edit', draftArticle)) {\n      // edit draft article...\n    }\n  }\n}\n```\n\n### 5. Type-safe action strings (optional)\n\nBy default, the `action` parameter accepts any string. You can register known\nactions per matcher via declaration merging on the `AbilityActions` interface to\nget autocompletion:\n\n```typescript\n// e.g. in src/ability-actions.d.ts\ndeclare module 'ng-ability' {\n  interface AbilityActions {\n    Article: 'view' | 'create' | 'edit';\n    AdminArea: 'view';\n  }\n}\n```\n\nWith this in place, calls using a registered matcher key will suggest the\ncorresponding actions:\n\n```typescript\nability.can('Article', 'edit');     // autocomplete suggests 'view' | 'create' | 'edit'\nability.can('AdminArea', 'view');   // autocomplete suggests 'view'\nability.can(article, 'edit');       // non-string matcher: suggests all registered actions\n```\n\nArbitrary action strings are still accepted — the type narrows suggestions\nwithout rejecting unknown actions.\n\n#### Using `AbilityActionsOf` with class-based matchers\n\nWhen using class constructors as matchers, you can implement the `AbilityActionsOf\u003cK\u003e`\ninterface to narrow the allowed actions based on your `AbilityActions` declaration:\n\n```typescript\nimport { AbilityActionsOf } from 'ng-ability';\n\n// Declare your actions mapping\ndeclare module 'ng-ability' {\n  interface AbilityActions {\n    Article: 'view' | 'create' | 'edit';\n  }\n}\n\n// Link your class to the 'Article' actions\nexport class Article implements AbilityActionsOf\u003c'Article'\u003e {\n  constructor(\n    public id: number,\n    public title: string,\n    public authorId: number,\n  ) {}\n}\n```\n\nNow when you pass the `Article` class constructor to ability checks, TypeScript\nwill narrow the allowed actions to `'view' | 'create' | 'edit'`:\n\n```typescript\n// In components or services\nability.can(Article, 'view');       // ✓ valid\nability.can(Article, 'edit');       // ✓ valid\nability.can(Article, 'delete');     // ✗ TypeScript error: 'delete' is not assignable\n\n// With an instance\nconst myArticle = new Article(1, 'Hello', 42);\nability.can(Article, 'edit', myArticle);  // ✓ valid with narrowed actions\n```\n\n**Why use `AbilityActionsOf`?**\n\nWithout `AbilityActionsOf`, passing a class constructor accepts any action string,\nsince TypeScript can't infer which actions apply to that class. By implementing\n`AbilityActionsOf\u003cK\u003e`, you create a type-level link between your class and its\nregistered actions, enabling:\n\n- **Type safety**: Catch typos and invalid actions at compile time\n- **Autocompletion**: IDE suggests only valid actions for that class\n- **Self-documenting code**: The class declaration shows which actions are supported\n\nThis is especially useful when:\n- You have many entity classes with different actions\n- You want to prevent mistakes like checking `ability.can(User, 'edit')` when `User` only supports `'view'`\n- You're passing class constructors instead of string matchers\n\n### 6. Global abilities (optional)\n\nGlobal abilities are special abilities that act as gatekeepers for your permission\nsystem. They are checked **before** any specific abilities, and **all** global\nabilities must return `true` for the permission check to proceed.\n\n**When to use global abilities:**\n\n- Enforce read-only mode across your entire application\n- Implement maintenance mode restrictions\n- Check license or feature flags\n- Verify user status (banned, suspended, etc.)\n- Multi-tenant isolation checks\n\n**Example:**\n\n```typescript\nimport { AbilityFor, Ability, GlobalAbility } from 'ng-ability';\n\n// Global ability to enforce read-only mode\n@AbilityFor(GlobalAbility)\nexport class ReadOnlyModeAbility implements Ability\u003cUser\u003e {\n  can(currentUser: User | null, action: string): boolean {\n    // Block all write operations when user is in read-only mode\n    if (currentUser?.readOnly \u0026\u0026 action !== 'read') {\n      return false;\n    }\n    // Allow the check to continue to specific abilities\n    return true;\n  }\n}\n\n// Regular ability\n@AbilityFor('Article')\nexport class ArticleAbility implements Ability\u003cUser\u003e {\n  can(currentUser: User | null, action: string): boolean {\n    // This will only be checked if all global abilities return true\n    return currentUser != null;\n  }\n}\n```\n\nRegister global abilities like any other ability:\n\n```typescript\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideAbilities(AbilityUserContext, [\n      ReadOnlyModeAbility,  // Global ability\n      ArticleAbility,       // Regular abilities\n      // ... other abilities\n    ]),\n  ],\n});\n```\n\n**Type safety:**\n\nThe type system enforces that abilities are either global OR specific:\n\n```typescript\n// ✓ Valid\n@AbilityFor(GlobalAbility)\nexport class GlobalCheck implements Ability\u003cUser\u003e { ... }\n\n// ✗ Invalid: Cannot mix GlobalAbility with matchers\n@AbilityFor(GlobalAbility, 'Article')  // TypeScript error!\n```\n\n**How it works:**\n\n1. When you call `can('Article', 'write')`, all global abilities are checked first\n2. If any global ability returns `false`, the check fails immediately\n3. Only if all global abilities return `true` does the check proceed to `ArticleAbility`\n\n### 7. Route guards\n\nProtect routes using ability-based guards:\n\n```typescript\nimport { canActivateAbility, canActivateChildAbility, canMatchAbility } from 'ng-ability'\n\nconst routes: Routes = [\n  { path: 'articles', canActivate: [canActivateAbility('Article', 'read')], component: ArticlesComponent },\n  { path: 'admin', canMatch: [canMatchAbility('Admin', 'access')], component: AdminComponent },\n]\n```\n\nWhen a `canActivate`/`canActivateChild` check fails, an `AbilityGuardUnauthorizedError` is thrown by default, which propagates to Angular's navigation error handler. Override `ABILITY_UNAUTHORIZED_HANDLER` globally or per route subtree to redirect instead:\n\n```typescript\nimport { ABILITY_UNAUTHORIZED_HANDLER, redirectAbilityUnauthorizedHandler } from 'ng-ability'\n\n{ provide: ABILITY_UNAUTHORIZED_HANDLER, useValue: redirectAbilityUnauthorizedHandler('/error/403') }\n```\n\nSee the [Route Guards guide](https://topaxi.github.io/ng-ability/guide/route-guards) for full details.\n\n## Development\n\n### Build\n\n```bash\nnpm run build\n```\n\n### Running unit tests\n\n```bash\nnpm test\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftopaxi%2Fng-ability","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftopaxi%2Fng-ability","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftopaxi%2Fng-ability/lists"}