https://github.com/topaxi/ng-ability
Access Control for Angular
https://github.com/topaxi/ng-ability
access acl angular can cancan permissions
Last synced: about 1 month ago
JSON representation
Access Control for Angular
- Host: GitHub
- URL: https://github.com/topaxi/ng-ability
- Owner: topaxi
- License: mit
- Created: 2018-11-05T21:05:18.000Z (over 7 years ago)
- Default Branch: main
- Last Pushed: 2026-04-30T12:18:24.000Z (about 1 month ago)
- Last Synced: 2026-04-30T14:15:00.810Z (about 1 month ago)
- Topics: access, acl, angular, can, cancan, permissions
- Language: TypeScript
- Homepage: https://topaxi.github.io/ng-ability/
- Size: 408 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-angular - ng-ability - Define access control lists in Angular. (Security and Authentication / Role-Based Access Control)
- awesome-angular - ng-ability - Define access control lists in Angular. (Security and Authentication / Role-Based Access Control)
README
# ng-ability
Define access control lists in Angular.
## Installation
```bash
npm install --save ng-ability
```
## Usage
### 1. Define an ability context
The ability context provides the current user (or other subject) to your ability checks:
```typescript
import { Injectable, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'
import { AbilityContext } from 'ng-ability';
@Injectable({ providedIn: 'root' })
export class AbilityUserContext implements AbilityContext {
readonly #auth = inject(AuthService);
readonly abilityContext = toSignal(this.#auth.getCurrentUser());
}
```
If your auth service uses signals, you can directly use them:
```typescript
@Injectable({ providedIn: 'root' })
export class AbilityUserContext implements AbilityContext {
readonly #auth = inject(AuthService);
// If auth.currentUser is already a Signal
readonly abilityContext = this.#auth.currentUser;
}
```
### 2. Define abilities
Define abilities for pages, models, and other data:
```typescript
import { AbilityFor, Ability } from 'ng-ability';
// Define ability for Article instance objects, the string 'Article'
// and graphql like objects using a matching function
@AbilityFor(Article, 'Article', article => article.__typename === 'Article')
export class ArticleAbility implements Ability {
can(currentUser: User | null, action: string, article: Article) {
if (currentUser != null && currentUser.admin) {
// Admins can do anything
return true;
}
switch (action) {
case 'view': // Everyone can view articles
return true;
case 'create': // Every user can create new articles
return currentUser != null;
case 'edit': // Users can only edit their own articles
return currentUser != null && currentUser.id === article.authorId;
default:
return false;
}
}
}
@AbilityFor('AdminArea')
export class AdminAreaAbility implements Ability {
can(currentUser: User | null, action: string) {
switch (action) {
case 'view': // Only admins can view the admin area
return currentUser != null && currentUser.admin;
default:
return false;
}
}
}
```
### 3. Register abilities
**Standalone (recommended):**
```typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAbilities } from 'ng-ability';
import { AbilityUserContext } from './ability-user-context';
import { ArticleAbility } from './abilities/article.ability';
import { AdminAreaAbility } from './abilities/admin-area.ability';
bootstrapApplication(AppComponent, {
providers: [
provideAbilities(AbilityUserContext, [ArticleAbility, AdminAreaAbility]),
],
});
```
**NgModule:**
```typescript
import { NgModule } from '@angular/core';
import { NgAbilityModule } from 'ng-ability';
@NgModule({
imports: [
NgAbilityModule.withAbilities(AbilityUserContext, [
ArticleAbility,
AdminAreaAbility,
]),
],
})
export class AppModule {}
```
### 4. Check abilities
**In templates** using the `can` pipe (import `CanPipe` or `NgAbilityModule`):
```html
@if ('Article' | can: 'create') {
I can create new articles!
}
@if (latestArticle | can: 'edit') {
Edit latest article
} @else {
Latest article is not editable :(
}
```
When an entity can be identified on its own — for example via `instanceof` or a
field like `__typename` in GraphQL responses — passing it directly is enough.
If the entity cannot be inferred, you can pass an explicit matcher as the pipe
value and the entity as a third argument:
```html
@if ('Article' | can: 'read' : draftArticle) {
I can read this draft article!
}
```
Alternatively, you can use the `*can` structural directive (import `CanDirective` or `NgAbilityModule`):
```html
I can create new articles!
Edit latest article
Latest article is not editable :(
```
**In code** using the `NgAbilityService`:
```typescript
import { Component, inject } from '@angular/core';
import { NgAbilityService } from 'ng-ability';
@Component({ ... })
export class AppComponent {
readonly #ability = inject(NgAbilityService);
editArticle(article: Article) {
// When the entity can be inferred (e.g., via instanceof)
if (this.#ability.can(article, 'edit')) {
// edit article...
}
}
createNewArticle() {
// When checking a string matcher without an entity
if (this.#ability.can('Article', 'create')) {
// create article...
}
}
editDraftArticle(draftArticle: unknown) {
// When explicit matcher is needed
if (this.#ability.can('Article', 'edit', draftArticle)) {
// edit draft article...
}
}
}
```
### 5. Type-safe action strings (optional)
By default, the `action` parameter accepts any string. You can register known
actions per matcher via declaration merging on the `AbilityActions` interface to
get autocompletion:
```typescript
// e.g. in src/ability-actions.d.ts
declare module 'ng-ability' {
interface AbilityActions {
Article: 'view' | 'create' | 'edit';
AdminArea: 'view';
}
}
```
With this in place, calls using a registered matcher key will suggest the
corresponding actions:
```typescript
ability.can('Article', 'edit'); // autocomplete suggests 'view' | 'create' | 'edit'
ability.can('AdminArea', 'view'); // autocomplete suggests 'view'
ability.can(article, 'edit'); // non-string matcher: suggests all registered actions
```
Arbitrary action strings are still accepted — the type narrows suggestions
without rejecting unknown actions.
#### Using `AbilityActionsOf` with class-based matchers
When using class constructors as matchers, you can implement the `AbilityActionsOf`
interface to narrow the allowed actions based on your `AbilityActions` declaration:
```typescript
import { AbilityActionsOf } from 'ng-ability';
// Declare your actions mapping
declare module 'ng-ability' {
interface AbilityActions {
Article: 'view' | 'create' | 'edit';
}
}
// Link your class to the 'Article' actions
export class Article implements AbilityActionsOf<'Article'> {
constructor(
public id: number,
public title: string,
public authorId: number,
) {}
}
```
Now when you pass the `Article` class constructor to ability checks, TypeScript
will narrow the allowed actions to `'view' | 'create' | 'edit'`:
```typescript
// In components or services
ability.can(Article, 'view'); // ✓ valid
ability.can(Article, 'edit'); // ✓ valid
ability.can(Article, 'delete'); // ✗ TypeScript error: 'delete' is not assignable
// With an instance
const myArticle = new Article(1, 'Hello', 42);
ability.can(Article, 'edit', myArticle); // ✓ valid with narrowed actions
```
**Why use `AbilityActionsOf`?**
Without `AbilityActionsOf`, passing a class constructor accepts any action string,
since TypeScript can't infer which actions apply to that class. By implementing
`AbilityActionsOf`, you create a type-level link between your class and its
registered actions, enabling:
- **Type safety**: Catch typos and invalid actions at compile time
- **Autocompletion**: IDE suggests only valid actions for that class
- **Self-documenting code**: The class declaration shows which actions are supported
This is especially useful when:
- You have many entity classes with different actions
- You want to prevent mistakes like checking `ability.can(User, 'edit')` when `User` only supports `'view'`
- You're passing class constructors instead of string matchers
### 6. Global abilities (optional)
Global abilities are special abilities that act as gatekeepers for your permission
system. They are checked **before** any specific abilities, and **all** global
abilities must return `true` for the permission check to proceed.
**When to use global abilities:**
- Enforce read-only mode across your entire application
- Implement maintenance mode restrictions
- Check license or feature flags
- Verify user status (banned, suspended, etc.)
- Multi-tenant isolation checks
**Example:**
```typescript
import { AbilityFor, Ability, GlobalAbility } from 'ng-ability';
// Global ability to enforce read-only mode
@AbilityFor(GlobalAbility)
export class ReadOnlyModeAbility implements Ability {
can(currentUser: User | null, action: string): boolean {
// Block all write operations when user is in read-only mode
if (currentUser?.readOnly && action !== 'read') {
return false;
}
// Allow the check to continue to specific abilities
return true;
}
}
// Regular ability
@AbilityFor('Article')
export class ArticleAbility implements Ability {
can(currentUser: User | null, action: string): boolean {
// This will only be checked if all global abilities return true
return currentUser != null;
}
}
```
Register global abilities like any other ability:
```typescript
bootstrapApplication(AppComponent, {
providers: [
provideAbilities(AbilityUserContext, [
ReadOnlyModeAbility, // Global ability
ArticleAbility, // Regular abilities
// ... other abilities
]),
],
});
```
**Type safety:**
The type system enforces that abilities are either global OR specific:
```typescript
// ✓ Valid
@AbilityFor(GlobalAbility)
export class GlobalCheck implements Ability { ... }
// ✗ Invalid: Cannot mix GlobalAbility with matchers
@AbilityFor(GlobalAbility, 'Article') // TypeScript error!
```
**How it works:**
1. When you call `can('Article', 'write')`, all global abilities are checked first
2. If any global ability returns `false`, the check fails immediately
3. Only if all global abilities return `true` does the check proceed to `ArticleAbility`
### 7. Route guards
Protect routes using ability-based guards:
```typescript
import { canActivateAbility, canActivateChildAbility, canMatchAbility } from 'ng-ability'
const routes: Routes = [
{ path: 'articles', canActivate: [canActivateAbility('Article', 'read')], component: ArticlesComponent },
{ path: 'admin', canMatch: [canMatchAbility('Admin', 'access')], component: AdminComponent },
]
```
When 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:
```typescript
import { ABILITY_UNAUTHORIZED_HANDLER, redirectAbilityUnauthorizedHandler } from 'ng-ability'
{ provide: ABILITY_UNAUTHORIZED_HANDLER, useValue: redirectAbilityUnauthorizedHandler('/error/403') }
```
See the [Route Guards guide](https://topaxi.github.io/ng-ability/guide/route-guards) for full details.
## Development
### Build
```bash
npm run build
```
### Running unit tests
```bash
npm test
```