---
title: "Permissions — Angular"
description: "A document can be opened with less than full power. Every plugin tells you what the user may do, so your UI only offers what works."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/concepts/permissions"
---

# Permissions

A document can be opened with less than full power: a link that lets people read and comment, a
signing session that only fills in fields, a reviewer who may not download. The engine refuses
everything else. This page is about building a UI that **shows only what works**, instead of buttons
that fail.

## Every verb has a check

Every call that can be refused has a check next to it, named `can` and the verb. It answers one
question: would this work right now, for this user?

```ts
annotation.canCreate(); // may they draw new annotations?
annotation.canUpdate(ref); // may they change this one?
selection.canCopy(); // may selected text leave the document?
form.canFill(ref); // may they fill in this field?
documents.canDownload(); // may they download the file?
```

A check already includes everything its call looks at: the user's permissions, the flags on the
document (a locked annotation, a read-only field) and what's selected. You never combine them
yourself, and you never read permission names: which permission a call needs is the plugin's
business, not yours.

> **The rule:** show a button when its check is true. Read the check in your template, like any
> other state: the answer changes when the selection changes or the user gets new permissions.

**`annotation-actions.ts`**

```typescript
import { Component, inject, input } from '@angular/core';
import { EpdfAnnotation, type AnnotationRef } from '@embedpdf/angular/annotation';
import { DrawingTools } from './drawing-tools';

@Component({
  selector: 'app-annotation-actions',
  imports: [DrawingTools],
  template: `
    @if (annotation.canCreate()) {
      <app-drawing-tools />
    }
    <button [disabled]="!annotation.canDelete(annotationRef())">Delete</button>
  `,
})
export class AnnotationActions {
  readonly annotationRef = input.required<AnnotationRef>();

  protected readonly annotation = inject(EpdfAnnotation);
}
```

Each control below shows only when its check says yes. Pick another role, and the document opens
again with its permissions:

**`checks.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import {
  EpdfDocument,
  EpdfDocumentGate,
  EpdfDocuments,
  EpdfViewer,
  provideEmbedPdf,
  saveFile,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  copySelection,
  EpdfSelection,
  EpdfSelectionLayer,
  withSelection,
} from '@embedpdf/angular/selection';
import { EpdfSearch, EpdfSearchLayer, withSearch } from '@embedpdf/angular/search';
import { localEngine } from '@embedpdf/engine';

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// What each role may do, as permissions.
const roles = {
  viewer: ['doc.open', 'doc.render', 'doc.text.select'],
  reviewer: ['doc.open', 'doc.render', 'doc.text.select', 'doc.text.copy', 'doc.text.search'],
  owner: [
    'doc.open',
    'doc.render',
    'doc.text.select',
    'doc.text.copy',
    'doc.text.search',
    'doc.download',
  ],
};
type Role = keyof typeof roles;

// Each control shows only when its check says yes.
@Component({
  selector: 'demo-controls',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @if (search.canSearch()) {
        <input
          #query
          class="field"
          type="search"
          aria-label="Search"
          [value]="text()"
          (input)="text.set(query.value)"
        />
      }
      @if (selection.canCopy()) {
        <button
          type="button"
          class="button"
          [disabled]="!selection.hasSelection()"
          (click)="copy()"
        >
          Copy
        </button>
      }
      @if (documents.canDownload()) {
        <button type="button" class="button" (click)="download()">Download</button>
      }
    </div>
    <p class="note">{{ copied() ? 'Copied: “' + copied() + '”' : ' ' }}</p>
  `,
})
export class Controls {
  protected readonly documents = inject(EpdfDocuments);
  protected readonly search = inject(EpdfSearch);
  protected readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);
  protected readonly text = signal('PDF');
  protected readonly copied = signal('');

  constructor() {
    // On load: a search, and some text selected, so every check has something to act on.
    effect(() => {
      const text = this.text();
      untracked(() => {
        if (this.search.canSearch()) void this.search.search({ text });
      });
    });
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) untracked(() => this.selection.select({ page: cover, start: 28, count: 30 }));
    });
  }

  protected async copy() {
    await copySelection(this.selection).catch(() => {}); // the browser may refuse the clipboard
    this.copied.set(await this.selection.readText());
  }

  protected async download() {
    saveFile(await this.documents.download(), 'welcome.pdf');
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSearchLayer,
    EpdfSelectionLayer,
    Controls,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        scope: roles.reviewer,
        initialDocuments: [{ source: welcome, name: 'welcome.pdf' }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withSearch(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './checks.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="toolbar">
      <label class="label">
        Role
        <select #picker class="select" [value]="role()" (change)="pickRole(picker.value)">
          <option value="viewer">viewer: read and select</option>
          <option value="reviewer">reviewer: also search and copy</option>
          <option value="owner">owner: also download</option>
        </select>
      </label>
    </div>
    <ng-container *epdfDocumentGate="let document; fallback: opening">
      <demo-controls />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-search-layer />
          <epdf-selection-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #opening><p class="loading">Opening…</p></ng-template>
  `,
})
export class App {
  private readonly viewer = inject(EpdfViewer);
  private readonly documents = inject(EpdfDocuments);
  private readonly document = inject(EpdfDocument);
  protected readonly role = signal<Role>('reviewer');

  // A document keeps the permissions it opened with, so a new role opens it again.
  protected async pickRole(value: string) {
    const role = value as Role;
    this.role.set(role);
    this.viewer.updateSettings({ scope: roles[role] });
    const id = this.document.id();
    if (!id) return;
    await this.documents.close(id);
    await this.documents.open(welcome, { name: 'welcome.pdf' });
  }
}
```

**`checks.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The role, then only the controls its checks allow */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink);
}

.select,
.field {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 10px;
}

.field {
  appearance: none;
  flex: 1 1 140px;
  min-width: 0;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.select:focus-visible,
.field:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.note {
  min-height: 18px;
  margin: 0 0 14px;
  font-size: 12px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 380px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## One answer per annotation

Checks that take a `ref` answer for that one annotation, field or comment. In a shared document, a
user may hold `annotations:update:self`: change their own annotations, and only look at the rest.
You don't handle that yourself. Someone else's annotation answers `canUpdate(ref) === false`, and
shows the way a locked one does: it can be selected and commented on, but has no handles. Forms work
the same: a field the user may not fill shows as read-only.

## Calling anyway is safe

Checks are there to keep your UI honest; the engine doesn't rely on them. A refused call rejects,
the same way in every plugin:

**`permission-denied.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { isPluginError } from '@embedpdf/angular/runtime';
import { EpdfForm, toFieldRef } from '@embedpdf/angular/form';

@Component({
  selector: 'app-set-total-button',
  template: `<button (click)="setTotal()">Set the total</button>`,
})
export class SetTotalButton {
  private readonly form = inject(EpdfForm);

  protected async setTotal() {
    try {
      await this.form.setValue(toFieldRef('total'), { value: '120.00' });
    } catch (error) {
      if (isPluginError(error, 'permission-denied')) {
        console.log(error.permission); // what was missing, such as 'doc.forms.fill'
      }
    }
  }
}
```

**`refused.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import {
  EpdfDocumentGate,
  EpdfDocuments,
  isPluginError,
  provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Read and select, but no downloading.
const scope = ['doc.open', 'doc.render', 'doc.text.select'];

/** What the last download did: the bytes, or why it was refused. */
type Result =
  | { kind: 'downloaded'; byteLength: number }
  | { kind: 'refused'; code: string; permission: string | null };

@Component({
  selector: 'demo-download-anyway',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button type="button" class="button" (click)="download()">Download anyway</button>
      <output class="result">
        @if (result(); as result) {
          @if (result.kind === 'downloaded') {
            Downloaded {{ result.byteLength }} bytes.
          } @else {
            Refused: <code>{{ result.code }}</code
            >, missing <code>{{ result.permission }}</code>
          }
        }
      </output>
    </div>
  `,
})
export class DownloadAnyway {
  private readonly documents = inject(EpdfDocuments);
  protected readonly result = signal<Result | null>(null);

  constructor() {
    // On load, the call the button makes, so the refusal shows at once.
    void this.download();
  }

  protected async download() {
    try {
      const bytes = await this.documents.download();
      this.result.set({ kind: 'downloaded', byteLength: bytes.byteLength });
    } catch (error) {
      if (isPluginError(error, 'permission-denied')) {
        this.result.set({ kind: 'refused', code: error.code, permission: error.permission });
      }
    }
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, DownloadAnyway],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), scope, initialDocuments: [{ source: welcome }] },
      withStage(),
      withRender(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './refused.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: opening">
      <demo-download-anyway />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #opening><p class="loading">Opening…</p></ng-template>
  `,
})
export class App {}
```

**`refused.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --refused: #b42318;
  --refused-tint: #fef3f2;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The call, and what came back */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.result {
  flex: 1 1 220px;
  min-width: 0;
  font-size: 13px;
  color: var(--refused);
  background: var(--refused-tint);
  border-radius: 9px;
  padding: 8px 12px;
}

.result:empty {
  visibility: hidden;
}

.result code {
  font-weight: 600;
}

/* The pages */
.stage {
  height: 380px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

The local and the cloud engine refuse the same way, so this one check covers both.

What people do with the mouse fails quietly instead: without permission to create, drawing with a
tool paints nothing and saves nothing.

## Where the limits come from

A document gets its permissions when it opens, together with who the user is.

On the local engine you give both. Once in `provideEmbedPdf()`, for every document it opens, and per document
when one needs something else:

**`viewer-permissions.ts`**

```typescript
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { archive, engine, report } from './pdf';

@Component({
  selector: 'app-review-viewer',
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
  providers: [
    provideEmbedPdf(
      {
        engine,
        identity: { userId: 'u_381', displayName: 'Dana Smith' },
        scope: [
          'doc.open',
          'doc.render',
          'doc.text.select',
          'doc.annotate.read',
          'annotations:create:self',
          'annotations:update:self',
          'annotations:delete:self',
        ],
        initialDocuments: [
          { source: report },
          { source: archive, scope: ['doc.open', 'doc.render'] }, // this one is read-only
        ],
      },
      withStage(),
      withRender(),
    ),
  ],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class ReviewViewer {}
```

- **A document's own `scope` replaces the viewer's.** It's the whole list for that document, not an
  addition, so it means exactly what it says. The same goes for `identity`.
- **`identity` is who the user is.** Permissions that say "their own" (`:self`) or "their group"
  (`:group`) are checked against it, so they need one: opening without it fails.
- **A document keeps what it opened with.** Change the viewer's `scope` or `identity` with
  `inject(EpdfViewer).updateSettings({ scope, identity })`, and the documents you open after that get
  the new values.

Code that opens documents itself passes the same options to
`documents.open(source, { scope, identity })`.

Leave `scope` out and the user may do what the file allows, as in Acrobat: everything, unless the
PDF has restrictions of its own, such as no printing. That's `pdf.permissions` plus what no PDF
flag governs: downloading, importing annotations and form data, running and submitting a form,
certifying the document, and putting annotations and fields in any group. Pass the scope your users will really get, so the
checks show you the UI they'll see.

> **Limits that hold on the server**
>
> On the local engine the checks run in the browser, and the browser already has the file. With
> CloudPDF, the server checks every call: a user who may not download never receives the file, and
> one who may not copy gets where the text is, not the text itself.
>
> [See how CloudPDF checks permissions](https://www.cloudpdf.com/docs/headless/angular/concepts/permissions)

## Every check

Each plugin's page has a Permissions section with the permission behind each check.

| Plugin                                                  | Checks                                                                                                                                   |
| :------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| [Documents](../documents/saving#permissions)            | `canDownload()` `canPrint()`                                                                                                             |
| [Rendering](../viewing/render#permissions)              | `canRender()`                                                                                                                            |
| [Page editing](../documents/pages#permissions)          | `canEdit()`, one answer for every page change; `canExtract()` for copying pages out                                                      |
| [Metadata](../documents/metadata#permissions)           | `canUpdate()`                                                                                                                            |
| [Selection](../text/selection#permissions)              | `canSelect()` `canCopy()`                                                                                                                |
| [Search](../text/search#permissions)                    | `canSearch()`, and `canSearch({ snippets: true })` for the words around each match                                                       |
| [Annotations](../annotations/with-code#permissions)     | `canRead()` `canCreate()` `canUpdate(ref)` `canDelete(ref)`; `selection.canGroup()` `selection.canUngroup()`                             |
| [Comments](../annotations/comments#permissions)         | `canReply(ref)` `canSetText(ref)` `canSetStatus(ref)` `canSetMarked(ref)` `canDelete(ref)` `canDeleteThread(ref)`                        |
| [Stamps](../annotations/stamps#permissions)             | `canPlace()` `canCreateFromAnnotations()`                                                                                                |
| [Measurements](../annotations/measurements#permissions) | `canMeasure(page)` `canCalibrate()`                                                                                                      |
| [Redaction](../annotations/redaction#permissions)       | `canMark()` `canUnmark(ref)` `canUpdateLabel(ref)` `canApply()`                                                                          |
| [Forms](../forms#permissions)                           | `canRead()` `canFill(ref)`; [building forms](../forms/building#permissions): `canDesign()` `canRestoreAttribution()` `canWriteScripts()` |
| [Signatures](../forms/signatures#permissions)           | `canSign(field)` `canCertify()` `canFill(field)` `canReadRevision()`                                                                     |

A [command](../ui/commands)'s `enabled` is where these checks go, so every toolbar button and
shortcut follows them.
