npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@astral/permissions

v1.3.2

Published

Пакет содержит функционал, необходимый для реализации [единого паттерна разграничения доступов на клиенте](https://kaluga-astral.github.io/guides/docs/category/%D1%80%D0%B0%D0%B7%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B4%D0%BE%D1%

Downloads

18

Readme

@astral/permissions

Пакет содержит функционал, необходимый для реализации единого паттерна разграничения доступов на клиенте.

Пакет предоставляет функции:

  • Создание policies
  • Централизованная подготовка данных для формирования доступов
  • Создание permissions
    • Обработка разных причин отказа
  • Создание rules
  • Debug режим для логирования причин отклоненных доступов

PolicyManagerStore

PolicyManagerStore отвечает за:

  • Создание policy
  • Централизованную подготовку данных для формирования доступов

Инициализация

PolicyManagerStore должен являться singletone и создаваться в единой точке доступа к доступам:

/**
 * Содержит все доступы приложения
 */
export class PermissionsStore {
  private readonly policyManager: PolicyManagerStore;

  public readonly administration: AdministrationPolicyStore;

  constructor(billingRepo: BillingRepository, userRepo: UserRepository) {
    makeAutoObservable(this, {}, { autoBind: true });
    
    this.policyManager = createPolicyManagerStore();

    this.administration = createAdministrationPolicyStore(
      this.policyManager,
      userRepo,
    );
  }
}

PolicyManagerStore.createPolicy

PolicyManagerStore.createPolicy предназначен для создания policies.

Пример создания policy:

import { makeAutoObservable } from 'mobx';

import type { UserRepository } from '@example/data';

import { PermissionDenialReason } from '../../../../enums';

import { PolicyManagerStore, Policy } from '@astral/permissions';

export class AdministrationPolicyStore {
  private readonly policy: Policy;

  constructor(
    private readonly policyManager: PolicyManagerStore,
    private readonly userRepo: UserRepository,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });

    this.policy = this.policyManager.createPolicy({
      name: 'administration',
      // Метод для подготовки данных необходимых для формирования доступов AdministrationPolicy
      prepareData: async (): Promise<void> => {
        await Promise.all([this.userRepo.getRolesQuery().async()]);
      },
    });
  }
}

Создание policy без подготовки данных

Если в создаваемом policy нет permissions, для которых необходимо подготовить данные, то используется флаг withoutDataPreparation:

import { makeAutoObservable } from 'mobx';

import type { UserRepository } from '@example/data';

import { PermissionDenialReason } from '../../../../enums';

import { PolicyManagerStore, Policy } from '@astral/permissions';

export class AdministrationPolicyStore {
  private readonly policy: Policy;

  constructor(
    private readonly policyManager: PolicyManagerStore,
    private readonly userRepo: UserRepository,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });

    this.policy = this.policyManager.createPolicy({
      name: 'administration',
      withoutDataPreparation: true,
    });
  }

  public calcOrgManagement = (org: Organization) =>
    this.policy.createPermission((allow, deny) => {
      if (!org.permissions.includes('admin')) {
        return deny(PermissionDenialReason.NoAdmin);
      }

      allow();
    });
}

Создание permissions

Permissions создаются с помощью метода policy.createPermission:

export class AdministrationPolicyStore {
  private readonly policy: Policy;

  constructor(
    private readonly policyManager: PolicyManagerStore,
    private readonly userRepo: UserRepository,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });

    this.policy = this.policyManager.createPolicy({
      name: 'administration',
      prepareData: async (): Promise<void> => {
        await Promise.all([this.userRepo.getRolesQuery().async()]);
      },
    });
  }

  /**
   * Доступ к действиям администратора
   */
  public get administrationActions() {
    return this.policy.createPermission((allow, deny) => {
      if (this.userRepo.getRolesQuery().data?.isAdmin) {
          return allow();
      }

      deny(PermissionDenialReason.NoAdmin);
    });
  }
}

createPermission принимает функцию-стратегию с двумя аргументами:

  • allow - вызов разрешает доступ
  • deny - вызов запрещает доступ. Принимает причину отказа в доступе

createPermission возвращает объект вида:

type Permission = {
  isAllowed: boolean;
  /**
   * Причина отказа в доступе
   */
  reason?: string;
  /**
   * @example permission.hasReason(DenialReason.NoAdmin)
   */
  hasReason: (reason: string) => boolean;
};

Причины отказа в доступе

deny обязательно принимает причину отказа в доступе.

Причины должны описываться в едином enum, но пакет @astral/permissions содержит системные причины отказа SystemDenialReason:

export enum SystemDenialReason {
  /**
   * При расчете доступа произошла ошибка
   * **/
  InternalError = 'internal-error',
  /**
   * Недостаточно данных для формирования доступа
   * **/
  MissingData = 'missing-data',
}

Для того чтобы все причины были в одном enum, необходимо объединить продуктовый enum с причинами из пакета:

import { SystemDenialReason } from '@astral/permissions';

export enum PermissionsDenialReason {
  /**
   * При расчете доступа произошла ошибка
   * **/
  InternalError = SystemDenialReason.InternalError,
  /**
   * Недостаточно данных для формирования доступа
   * **/
  MissingData = SystemDenialReason.MissingData,
  /**
   * Пользователь не является админом
   * **/
  NoAdmin = 'no-admin',
}

Системные причины отказа в доступе

  • Если на момент вычисления доступов не были подготовлены данные (не был вызван prepareData), то permission будет отказан с reason: SystemDenialReason.MissingData.
  • Если при вычислении доступов не был вызван ни allow, ни deny, то permission будет отказан с reason: SystemDenialReason.InternalError.

Обработка причин отказа в доступе

Обрабатывать причины отказа в доступе можно либо через оператор ===:

permissions.books.addingToShelf.reason === PermissionsDenialReason.NoAdmin

Либо через метод hasReason:

permissions.books.addingToShelf.hasReason(PermissionsDenialReason.NoAdmin)

Пример:

export class UIStore {
  public isOpenPayAccount = false;

  constructor(
    private readonly bookId: string,
    private readonly permissions: PermissionsStore,
    private readonly notifyService: Notify,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });
  }

  public addToShelf = () => {
      if (this.permissions.books.addingToShelf.isAllowed) {
        this.notifyService.info(`Книга ${this.bookId} добавлена на полку`);

        return;
      }

      if (this.permissions.books.addingToShelf.hasReason(PermissionsDenialReason.NoPay)) {
        this.openPaymentAccount();

        return;
      }

      if (
        this.permissions.books.addingToShelf.hasReason(PermissionsDenialReason.ExceedReadingCount)
      ) {
        this.notifyService.error(
          'Достигнуто максимальное количество книг на полке',
        );

        return;
      }

      this.notifyService.error(
        'Добавить книгу на полку нельзя. Попробуйте перезагрузить страницу',
      );
  };

  public openPayAccount = () => {
    this.isOpenPayAccount = true;
  };

  public closePayAccount = () => {
    this.isOpenPayAccount = false;
  };
}

Подготовка данных для формирования доступов

Каждый policy при создании посредством PolicyManagerStore.createPolicy указывает метод prepareData, подготавливающий данные для формирования доступов:

import { makeAutoObservable } from 'mobx';

import type { UserRepository } from '@example/data';

import { PermissionDenialReason } from '../../../../enums';

import { PolicyManagerStore, Policy } from '@astral/permissions';

export class AdministrationPolicyStore {
  private readonly policy: Policy;

  constructor(
    private readonly policyManager: PolicyManagerStore,
    private readonly userRepo: UserRepository,
  ) {
    makeAutoObservable(this, {}, { autoBind: true });

    this.policy = this.policyManager.createPolicy({
      name: 'administration',
      // Метод для подготовки данных необходимых для формирования доступов AdministrationPolicy
      prepareData: async (): Promise<void> => {
        await Promise.all([this.userRepo.getRolesQuery().async()]);
      },
    });
  }
}

Далее для централизованной подготовки данных для всех policy, необходимо создать PermissionsStore:

export class PermissionsStore {
  private readonly policyManager: PolicyManagerStore;

  public readonly administration: AdministrationPolicyStore;

  constructor(billingRepo: BillingRepository, userRepo: UserRepository) {
    makeAutoObservable(this, {}, { autoBind: true });
    this.policyManager = createPolicyManagerStore();

    this.administration = createAdministrationPolicyStore(
      this.policyManager,
      userRepo,
    );
  }

  /**
   * Подготавливает данные для формирования доступов
   */
  public prepareData = () => this.policyManager.prepareDataSync();

  public get preparingDataStatus() {
    return this.policyManager.preparingDataStatus;
  }
}

PermissionsStore.prepareData загрузит все необходимые данные для каждого policy, созданного через PolicyManagerStore.createPolicy.

preparingDataStatus будет содержать объект:

type PreparingDataStatus = {
  /**
   * Флаг простаивания запроса, true если prepareData не был выполнен
   */
  isIdle: boolean;
  isSuccess: boolean;
  isLoading: boolean;
  isError: boolean;
  error?: Error;
};

Создание rules

createRule позволяет создавать правила, переиспользуемые между policies:

import { createRule } from '@astral/permissions';

import { getDateYearDiff } from '@example/shared';

import { PermissionDenialReason } from '../../../../enums';

export const calcAcceptableAge = (
  acceptableAge?: number,
  userBirthday?: string,
) =>
  createRule((allow, deny) => {
    if (!acceptableAge) {
      return deny(PermissionDenialReason.MissingData);
    }

    if (!userBirthday) {
      return deny(PermissionDenialReason.MissingUserAge);
    }

    if (
      Math.abs(getDateYearDiff(new Date(userBirthday), new Date())) <
      acceptableAge
    ) {
      return deny(PermissionDenialReason.NotForYourAge);
    }

    allow();
  });

Пример использования:

export class PaymentPolicyStore {
    
  ...

  /**
   * Возможность оплатить товар
   */
  public calcPayment = (acceptableAge: number) =>
    this.policy.processPermission((allow, deny) => {
      // calcAcceptableAge - правило, полностью реализующее calcPayment permission
      const agePermission = calcAcceptableAge(
        acceptableAge,
        this.userRepo.getPersonInfoQuery().data?.birthday,
      );

      if (!agePermission.isAllowed) {
        return deny(agePermission.reason);
      }

      allow();
    });
}

Debug режим

PolicyManagerStore позволяет при включении debug режима показывать логи при отказе в доступе:

export class PermissionsStore {
  private readonly policyManager: PolicyManagerStore;

  public readonly administration: AdministrationPolicyStore;

  constructor(billingRepo: BillingRepository, userRepo: UserRepository) {
    makeAutoObservable(this, {}, { autoBind: true });
    
    this.policyManager = createPolicyManagerStore({ isDebug: true });

    this.administration = createAdministrationPolicyStore(
      this.policyManager,
      userRepo,
    );
  }
}

Пример логов:

[@astral/permissions]/Policy:administration: При вычислении доступа не был вызван ни allow, ни deny Error: При вычислении доступа не был вызван ни allow, ни deny