{"id":13626536,"url":"https://github.com/tselect-npm/access-control","last_synced_at":"2025-04-16T14:33:56.804Z","repository":{"id":26896251,"uuid":"110463296","full_name":"tselect-npm/access-control","owner":"tselect-npm","description":"Simple, flexible and reliable access control for NodeJS and Typescript. Supports both RBAC and ABAC.","archived":false,"fork":false,"pushed_at":"2024-06-18T15:57:14.000Z","size":626,"stargazers_count":40,"open_issues_count":4,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-09-12T04:47:30.264Z","etag":null,"topics":["abac","accesscontrol","acl","bluejay","node","npm","permission","rbac","role","typescript"],"latest_commit_sha":null,"homepage":"","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/tselect-npm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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}},"created_at":"2017-11-12T19:44:48.000Z","updated_at":"2024-07-05T17:23:09.000Z","dependencies_parsed_at":"2024-09-12T04:57:39.565Z","dependency_job_id":null,"html_url":"https://github.com/tselect-npm/access-control","commit_stats":{"total_commits":122,"total_committers":6,"mean_commits":"20.333333333333332","dds":"0.20491803278688525","last_synced_commit":"b021f993350c6310c0305d1642c86e13b6fde217"},"previous_names":["bluebirds-blue-jay/access-control","tselect-npm/access-control","sylvainestevez/access-control"],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tselect-npm%2Faccess-control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tselect-npm%2Faccess-control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tselect-npm%2Faccess-control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tselect-npm%2Faccess-control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tselect-npm","download_url":"https://codeload.github.com/tselect-npm/access-control/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249250827,"owners_count":21237961,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["abac","accesscontrol","acl","bluejay","node","npm","permission","rbac","role","typescript"],"created_at":"2024-08-01T21:02:22.435Z","updated_at":"2025-04-16T14:33:56.531Z","avatar_url":"https://github.com/tselect-npm.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# AccessControl\n\n[![npm](https://img.shields.io/npm/v/@tselect/access-control.svg?style=flat-square)](https://www.npmjs.com/package/@tselect/access-control)\n [![npm](https://img.shields.io/npm/dm/@tselect/access-control.svg?style=flat-square)](https://www.npmjs.com/package/@tselect/access-control)\n[![npm](https://img.shields.io/npm/l/@tselect/access-control.svg?style=flat-square)](https://www.npmjs.com/package/@tselect/access-control)\n\nSimple, flexible and reliable [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) / [ABAC](https://en.wikipedia.org/wiki/Attribute-based_access_control) access control for NodeJS and Typescript.\n\n## Installation\n\n`npm i @tselect/access-control`\n\n## Guide\n\n### Simple role based access control\n\nMany applications define a list of roles which are assigned to users and thus define what those users can or cannot do. This is called RBAC (Role Based Access Control) and is one of the most used access control mechanism. Below we'll take a look at how this can be implemented with TSelect's AccessControl.\n\n\nFirst, let's define some application context:\n\n```typescript\n// Here we define 2 roles: one for regular customers, and a special admin role for internal users.\n// Note that TSelect's AccessControl doesn't require any specific role values nor the use of an enum. A role in TSelect's world is as simple as a string.\nexport enum Role {\n  CUSTOMER = 'customer',\n  ADMIN = 'admin'\n}\n\n// This interface describes the attributes of the \"post\" entity, which represents blog posts in our example application.\nexport interface IPost {\n  id: integer;\n  title: string;\n  content: string;\n  created_by: integer;\n  created_at: Date;\n  updated_at: Date;\n}\n```\n\nNext, we'll define a `Subject` class to hold our user's information and communicate with TSelect's AccessControl. A *subject* is just a fancy security term that defines a security view of an application user. Note that a subject does not have to represent a human being and could for example be a 3rd party system that connects to your application.\n\nFor the sake of simplicity, we'll assume that all users are stored in the same database table and are uniquely identified by an `id` integer. We'll implement the abstract `getPrincipal()` method in order to let TSelect know what attribute to use as an identifier. A `principal` is another security term that defines an identifying attribute for an application user.\n\n```typescript\nimport { Subject } from '@tselect/access-control';\n\nexport class UserSubject extends Subject\u003c{ id: number }\u003e {\n  public getPrincipal() {\n    return this.get('id');\n  }\n\n  // Note: you can perfectly add custom methods here\n}\n```\n\nWe'll then need to tell TSelect where to look for permissions. AccessControl comes package with a built-in `MemoryStore` that allows you to manage permissions in memory. We'll cover persistent stores [later in this documentation](#stores).\n\n```typescript\nimport { MemoryStore } from '@tselect/access-control';\n\nconst store = new MemoryStore();\n```\n\nNow let's create an instance of TSelect's AccessControl to be used across the application.\n\n```typescript\nimport { AccessControl } from '@tselect/access-control';\n\nconst accessControl = new AccessControl({ store });\n```\n\nWe're now ready to declare permissions:\n\n```typescript\nconst customer = new UserSubject({ id: 1 });\nconst admin = new UserSubject({ id: 2 });\n\nstore\n  .addPermissionToRole(Role.CUSTOMER, { // Limited power for customers\n    id: 'CustomerPostsPolicy',\n    effect: 'allow',\n    resource: 'posts',\n    action: ['create', 'read']\n  })\n  .addPermissionToRole(Role.ADMIN, { // Full power for admins\n    id: 'AdminPolicy',\n    effect: 'allow',\n    resource: '*',\n    action: '*'\n  })\n  .addRoleToSubject(customer, Role.CUSTOMER)\n  .addRoleToSubject(admin, Role.ADMIN);\n```\n\nWe can finally check our users permissions:\n\n```typescript\nawait accessControl.can(customer, 'posts', 'create'); // true\nawait accessControl.can(customer, 'posts', 'update'); // false\nawait accessControl.can(admin, 'posts', 'delete'); // true\n```\n\n### Simple attribute based access control\n\nABAC (Attribute Based Access Control) provides an fine-grained control over which attributes a particular role is able to access. The following examples assume that you have already read the RBAC examples.\n\nLet's create some more specific permission:\n\n```typescript\nstore\n  .addPermissionToRole(Role.CUSTOMER, {\n    id: 'CustomerCreatePostPolicy',\n    effect: 'allow',\n    resource: 'posts',\n    action: 'create',\n    condition: {\n      stringEquals: {\n        forAllValues: {\n          bodyAttributes: ['title', 'content']\n        }\n      }\n    }\n  })\n  .addPermissionToRole(Role.ADMIN, {\n    id: 'AdminPolicy',\n    effect: 'allow',\n    resource: '*',\n    action: '*'\n  });\n```\n\nThe `condition` part defines a set of rules used to evaluate whether or not the permission is applicable. Conditions are defined in a 3 levels object that can be described as follows:\n- operator (`stringEquals` in our case)\n    - modifier (`forAllValues` in our case)\n        - attributeName (`bodyAttributes` in our case)\n\nAn `operator` defines what type of data we're comparing and how to compare then. Example operators are `dateEquals`, `numberGreaterThan`, `bool`, `stringNotEquals`, ...\nA `modifier` defines the type of input data (single value vs. array) as well as how to interpret them. Example modifiers are `forAllValues`, `simpleValue`, `simpleValueIfExists`\nAn `attributeName` defines an attribute that *may be* be present in the `environment`. Our example defines `bodyAttributes` which is meant to contain the attributes of the POST request's body.\n\nFor more about condition modifiers, see the next section.\n\nWe are essentially saying that *for all values* in `bodyAttributes`, we expect to find an *equal string* in the provided condition values. Stated another way, we expect the body to only contain attributes that are listed in the condition values.\n\nWe'll be building a simple express POST endpoint that allows consumers to create blog posts. We'll assume that the request has been authenticated and the current user stored as `req.user`. We'll also assume that the request's body has already been validated and contains only valid values in regards to the data model.\n\nWe will be making use of TSelect's `Keys` utility, which helps performing various attribute related operations on objects and arrays, in a format that is understood by TSelect.\n\n```typescript\nimport { Keys } from '@tselect/access-control';\n\napp.post('/posts', authenticate(), validatePostBody(), async (req: Request, res: Response) =\u003e {\n  const body: Partial\u003cIPost\u003e = req.body;\n\n  const subject = new UserSubject(req.user);\n\n  // We're passing the body's attributes in the environment. Keys.list() will make sure that the resulting list of attributes\n  // is understandable by TSelect.\n  const isAllowed = await accessControl.can(subject, 'posts', 'create', { bodyAttributes: Keys.list(body) });\n\n  // For an admin user, since no attributes condition has been defined in the permission, any body will be authorized.\n  // For a customer user, the access will only be authorized if all attributes in the body are listed in the permission.\n\n  if (isAllowed) {\n    await postService.create(body);\n    res.status(201).end();\n  } else {\n    res.status(403).end();\n  }\n});\n```\n\n### Condition operators\n\nCondition operators define how environment values are compared to condition values.\n\nCondition values are defined as strings no matter their type and further casted at runtime. This allows consumers to store permissions in a schema based storage system such a RDMS without worrying about casting values themselves or storing condition values in separate tables because of the type differences.\n\n#### String operators\n\n##### `stringEquals`\n\n```\nconst condition = {\n  stringEquals: {\n    simpleValue: {\n      foo: 'bar'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'bar' // This will pass because foo === 'bar'\n};\n\nconst nok = {\n  foo: 'baz' // This won't pass because foo !== 'bar'\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo !== 'bar'\n}\n```\n\n##### `stringNotEquals`\n\n```\nconst condition = {\n  stringNotEquals: {\n    simpleValue: {\n      foo: 'bar'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'baz' // This will pass because foo !== 'bar'\n};\n\nconst nok = {\n  foo: 'bar' // This won't pass because foo === 'bar'\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a string\n}\n```\n\n##### `stringImplies`\n\n```\nconst condition = {\n  stringImplies: {\n    simpleValue: {\n      foo: 'bar*'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'bar' // This will pass because foo matches 'bar'\n};\n\nconst ok2 = {\n  foo: 'barack' // This will pass because foo matches 'bar'\n};\n\nconst nok = {\n  foo: 'baz' // This won't pass because foo does not match 'bar'\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a string\n}\n```\n\n##### `stringNotImplies`\n\n```\nconst condition = {\n  stringNotImplies: {\n    simpleValue: {\n      foo: 'bar*'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'baz' // This will pass because foo does not match 'bar'\n};\n\nconst nok = {\n  foo: 'bar' // This won't pass because foo matches 'bar'\n};\n\nconst nok2 = {\n  foo: 'barack' // This won't pass because foo matches 'bar'\n};\n\nconst nok3 = {\n  foo: undefined // This won't pass because foo is not a string\n}\n```\n\n#### Number operators\n\n##### `numberEquals`\n\n```\nconst condition = {\n  numberEquals: {\n    simpleValue: {\n      foo: '1'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 1 // This will pass because foo === 1\n};\n\nconst nok = {\n  foo: 2 // This won't pass because foo !== 1\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a number\n}\n```\n\n##### `numberNotEquals`\n\n```\nconst condition = {\n  numberNotEquals: {\n    simpleValue: {\n      foo: '0'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 1 // This will pass because foo !== 1\n};\n\nconst nok = {\n  foo: 0 // This won't pass because foo === 0\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a number\n}\n```\n\n##### `numberGreaterThan` / `numberGreaterThanEquals`\n\n```\nconst condition = {\n  numberGreaterThan: {\n    simpleValue: {\n      foo: '0'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 1 // This will pass because foo \u003e 1\n};\n\nconst nok = {\n  foo: 0 // This won't pass because foo === 0\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a number\n}\n```\n\n##### `numberLowerThan` / `numberLowerThanEquals`\n\n```\nconst condition = {\n  numberLowerThan: {\n    simpleValue: {\n      foo: '100'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 1 // This will pass because foo \u003c 100\n};\n\nconst nok = {\n  foo: 101 // This won't pass because foo \u003e 0\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a number\n}\n```\n\n#### `bool`\n\n```\nconst condition = {\n  bool: {\n    simpleValue: {\n      foo: 'true'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: true // This will pass because foo is true\n};\n\nconst nok = {\n  foo: false // This won't pass because foo is not true\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a boolean\n}\n```\n\n\n#### `null`\n\n```\nconst condition = {\n  null: {\n    simpleValue: {\n      foo: 'true'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: null // This will pass because foo is null\n};\n\nconst nok = {\n  foo: true // This won't pass because foo is not null\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not null\n}\n```\n\n#### Date operators\n\n##### `dateEquals`\n\n```\nconst condition = {\n  dateEquals: {\n    simpleValue: {\n      foo: '2018-09-21T09:46:12.441Z'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: '2018-09-21T09:46:12.441Z' // This will pass because matches the contidion value\n};\n\nconst ok2 = {\n  foo: new Date('2018-09-21T09:46:12.441Z') // This will pass because matches the contidion value\n};\n\nconst ok2 = {\n  foo: 1537523172441 // This will pass because matches the contidion value\n};\n\nconst nok = {\n  foo: '2017-09-21T09:46:12.441Z' // This won't pass because foo does not match the condition value\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not castable to a date\n}\n```\n\n##### `dateNotEquals`\n\n```\nconst condition = {\n  dateNotEquals: {\n    simpleValue: {\n      foo: '2018-09-21T09:46:12.441Z'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: '2017-09-21T09:46:12.441Z' // This will pass because is different from the contidion value\n};\n\nconst ok2 = {\n  foo: new Date('2017-09-21T09:46:12.441Z') // This will pass because is different from the contidion value\n};\n\nconst ok2 = {\n  foo: 1437523172441 // This will pass because is different from the contidion value\n};\n\nconst nok = {\n  foo: '2017-09-21T09:46:12.441Z' // This won't pass because foo does matches the condition value\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not castable to a date\n}\n```\n\n##### `dateGreaterThan` / `dateGreaterThanEquals`\n\n```\nconst condition = {\n  dateGreaterThan: {\n    simpleValue: {\n      foo: '2018-09-21T09:46:12.441Z'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: '2019-09-21T09:46:12.441Z' // This will pass because foo \u003e condition value\n};\n\nconst nok = {\n  foo: '2017-09-21T09:46:12.441Z' // This won't pass because foo \u003c condition value\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a date\n}\n```\n\n##### `dateLowerThan` / `dateLowerThanEquals`\n\n```\nconst condition = {\n  dateLowerThan: {\n    simpleValue: {\n      foo: '2018-09-21T09:46:12.441Z'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: '2017-09-21T09:46:12.441Z' // This will pass because foo \u003c condition value\n};\n\nconst nok = {\n  foo: '2019-09-21T09:46:12.441Z' // This won't pass because foo \u003e condition value\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo is not a date\n}\n```\n\n### Condition modifiers\n\nCondition modifiers are largely inspired by AWS IAM policies, with some minor additions (see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_multi-value-conditions.html and https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html).\n\nWe support 6 modifiers:\n- `simpleValue`\n- `simpleValueIfExists`\n- `forAllValues`\n- `forAllValuesIfExists`\n- `forAnyValue`\n- `forAnyValueIfExists`\n\n#### Modifiers that check a single value\n\n##### `simpleValue`\n\n```\nconst condition = {\n  stringEquals: {\n    simpleValue: {\n      foo: 'bar'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'bar' // This will pass because foo === 'bar'\n};\n\nconst nok = {\n  foo: 'baz' // This won't pass because foo !== 'bar'\n};\n\nconst nok2 = {\n  foo: undefined // This won't pass because foo !== 'bar'\n}\n```\n\n##### `simpleValueIfExists`\n\n```\nconst condition = {\n  stringEquals: {\n    simpleValueIfExists: {\n      foo: 'bar'\n    }\n  }\n};\n\n\nconst ok = {\n  foo: 'bar' // This will pass because foo === 'bar'\n};\n\nconst ok2 = {\n  foo: undefined // This will pass because foo does not exist\n}\n\nconst nok = {\n  foo: 'baz' // This won't pass because foo !== 'bar'\n};\n\n```\n\n#### Modifiers that check multiple values\n\n##### `forAllValues`\n\nThis modifier enforces that all values - if any - present in the environment must match a value in the condition.\n\n```\nconst condition = {\n  stringEquals: {\n    forAllValues: {\n      foo: ['bar', 'baz', 'boo']\n    }\n  }\n};\n\n\nconst ok = {\n  foo: ['bar'] // This will pass because foo is \"one of\" the condition's value\n};\n\nconst ok2 = {\n  foo: [] // This will pass because no value in foo contradicts the condition's values\n};\n\nconst nok = {\n  foo: ['booz', 'bar'] // This won't pass because booz is not listed in the condition's values\n};\n\nconst nok2 = {\n  foo: [undefined] // This won't pass because undefined is not an allowed value\n}\n```\n\n##### `forAllValuesIfExists`\n\nThis modifier does exactly the same thing as `forAllValues` expect that it ignores undefined values.\n\n```\nconst condition = {\n  stringEquals: {\n    forAllValuesIfExists: {\n      foo: ['bar', 'baz', 'boo']\n    }\n  }\n};\n\n\nconst ok = {\n  foo: ['bar'] // This will pass because foo is \"one of\" the condition's value\n};\n\nconst ok2 = {\n  foo: [] // This will pass because no value in foo contradicts the condition's values\n};\n\nconst ok3 = {\n  foo: [undefined] // This will pass because only \"existing\" values are evaluated\n}\n\nconst nok = {\n  foo: ['booz', 'bar'] // This won't pass because booz is not listed in the condition's values\n};\n```\n\n##### `forAnyValue`\n\nThis modifier says that the condition is met if at least one value in the environment matches the accepted values in the condition.\n\n```\nconst condition = {\n  stringEquals: {\n    forAnyValue: {\n      foo: ['bar', 'baz', 'boo']\n    }\n  }\n};\n\n\nconst ok = {\n  foo: ['bar', 'booz'] // This will pass because bar is an accepted value\n};\n\nconst ok2 = {\n  foo: ['bar', 'baz'] // This will pass because all values are accepted\n};\n\nconst nok = {\n  foo: ['booz', 'biz'] // This won't pass because no value matches the condition values\n};\n\nconst nok2 = {\n  foo: [] // This won't pass because no value in foo matches a condition value\n};\n```\n\n##### `forAnyValueIfExists`\n\nYou will most likely never use this modifier unless you expect the environment values to be a mix of valid and undefined values.\n\n```\nconst condition = {\n  stringEquals: {\n    forAnyValueIfExists: {\n      foo: ['bar', 'baz', 'boo']\n    }\n  }\n};\n\n\nconst ok = {\n  foo: ['bar', 'booz', undefined] // This will pass because bar is an accepted value\n};\n\nconst nok = {\n  foo: ['booz', 'biz'] // This won't pass because no value matches the condition values\n};\n\nconst nok2 = {\n  foo: [] // This won't pass because no value in foo matches a condition value\n};\n\nconst nok3 = {\n  foo: [undefined] // This is equivalent to an empty array\n};\n```\n\n### Condition variables\n\nTSelect provides us with a powerful way of defining variables as condition values, making permissions dynamic.\n\nLet's say that we want to expose an endpoint and allows our users to modify their information. We need to make sure that a given user can only modify their own information, and no one else's. A naive way of defining permissions would look like the following.\n\n```typescript\nstore\n  .addPermissionToRole(Role.CUSTOMER, {\n    id: 'CustomerUpdateInformationPolicy',\n    effect: 'allow',\n    resource: 'users',\n    action: 'update',\n    condition: {\n      numberEquals: {\n        simpleValue: {\n          id: '?????' // Here we would need to create one permission per user!\n        }\n      }\n    }\n  })\n```\n\nThanks to TSelect's variables, we have a more dynamic way of doing this.\n\n```typescript\nstore\n  .addPermissionToRole(Role.CUSTOMER, {\n    id: 'CustomerUpdateInformationPolicy',\n    effect: 'allow',\n    resource: 'users',\n    action: 'update',\n    condition: {\n      numberEquals: {\n        simpleValue: {\n          'params.id': '{{{subject.id}}}' // This will be evaluated at runtime\n        }\n      }\n    }\n  })\n```\n\nNow let's look at the endpoint itself.\n\n```typescript\napp.patch('/users/:id', authenticate(), validatePatchBody(), async (req: Request, res: Response) =\u003e {\n  const body: Partial\u003cIPost\u003e = req.body;\n\n  const subject = new UserSubject(req.user);\n\n  // We're passing both the id and the subject to the environment\n  const isAllowed = await accessControl.can(subject, 'posts', 'create', { params: req.params, subject: subject.toJSON() });\n\n  // An admin user will be allowed to update any user. The id and subject in the environment will not even be used since no condition is defined.\n  // A customer, on the other side, will only be able to update their own user.\n\n  if (isAllowed) {\n    await userService.update({ id: req.params.id }, body);\n    res.status(204).end();\n  } else {\n    res.status(403).end();\n  }\n});\n```\n\n\n### Filtering returned attributes\n\nIt is important to understand that access control validates a request and has therefore no influence over the response that you send to your consumers.\n\nIf you need to control which fields are exposed in the responses to your different consumers, one solution is to have them explicitly request the set of attributes that they want to see returned. If this is how your application behaves, then you can validate the attributes that a particular user is requesting using a permission that would look like this:\n\n```typescript\nac.addPermissionToRole(Role.CUSTOMER, {\n  id: 'CustomerReadPostPolicy',\n  effect: 'allow',\n  resource: 'posts',\n  action: 'read',\n  condition: {\n    stringEquals: {\n      forAllValues: {\n        fields: ['id', 'title', 'content', 'created_by']\n      }\n    }\n  }\n});\n```\n\nIn this condition, we are essentially saying that *for all values* in `fields`, we expect to find an *equal string* in the permission's values.\n\n```typescript\n// We'll assume that the consumers make calls that look like \"GET /posts?fields=id,title,content\" where the `fields` query parameter defines the fields to be returned as a response.\n\napp.get('/posts', authenticate(), async (req: Request, res: Response) =\u003e {\n  const fields = (req.query.fields || '').split(',');\n  const subject = new UserSubject(req.user);\n\n  // We're passing the fields in the environment hash so that they can be evaluated\n  const access = await accessControl.authorize(subject, 'posts', 'read', { fields });\n\n  if (access.isAllowed()) {\n    // The application is responsible for only returning the fields that have been requested. The access control has made sure that only allowed attributes have been requested, so you can safely pass the fields to your service.\n    const data = await postService.list({ fields });\n    res.status(200).json(data);\n  } else {\n    res.status(403).end();\n  }\n});\n```\n\nHowever, in most applications, the consumer is not expected to provide the fields they want to see returned. TSelect provides you with a convenient way of dealing with this use case by introducing a special `returnedAttributes` property in the permission definition. With this knowledge, we can refactor the previous permission to this one:\n\n```typescript\nac.addPermissionToRole(Role.CUSTOMER, {\n  id: 'CustomerReadPostPolicy',\n  effect: 'allow',\n  resource: 'posts',\n  action: 'read',\n  returnedAttributes: ['id', 'title', 'content', 'created_by']\n});\n\n// We'll now assume that consumers make calls that look like \"GET /posts\".\n\napp.get('/posts', authenticate(), async (req: Request, res: Response) =\u003e {\n  const subject = new UserSubject(req.user);\n\n  // We're not passing any environment data here since the request does not contain any attribute information that could be useful to determine access. the `returnedAttributes` are simply ignored by TSelect.\n  const access = await accessControl.authorize(subject, 'posts', 'read');\n\n  if (access.isAllowed()) {\n    // This time, instead of using the request's `fields`, we're using the fields defined in the permission and accessible through `getReturnedAttributes()` on the access.\n    const data = await postService.list({ fields: access.getReturnedAttributes() || [] }); // Depending on who's calling, the returned attributes might be undefined\n    res.status(200).json(data);\n  } else {\n    res.status(403).end();\n  }\n});\n```\n\nIt is important to note that TSelect only acts as a middleman here and never uses `returnedAttributes` to determine access.\n\nAlso alternatively, if you are not able to have your services only return a specific set of attributes, you can use TSelect's `Keys` utility to filter the payload before responding:\n\n```typescript\nimport { Keys } from '@tselect/access-control';\n\napp.get('/posts', authenticate(), async (req: Request, res: Response) =\u003e {\n  const subject = new UserSubject(req.user);\n\n  // We're not passing any environment data here since the request does not contain any attribute information that could be useful to determine access. the `returnedAttributes` are simply ignored by the authorizer.\n  const access = await accessControl.authorize(subject, 'posts', 'read');\n\n  if (access.isAllowed()) {\n    const data = await postService.list();\n\n    // Keys.filter() accepts both objects and arrays\n    const payload = Keys.filter(data, access.getReturnedAttributes())\n\n    res.status(200).json(payload);\n  } else {\n    res.status(403).end();\n  }\n});\n```\n\n### Stores\n\n#### The default `MemoryStore`\n\nBy default, TSelect comes packaged with a `MemoryStore` which allows you to quickly get started by storing permissions in memory.\n\n- `createPermission()`: Create/store a new permission. Overrides any existing permission with the same ID. If no `id` property is set, one will be created\n- `deletePermission()`: Delete a single permission\n- `replacePermission()`: Replaces a permission by ID\n- `addPermissionToRole()`: Assigns a permission to a role. If the permission does not exist yet, it will be created\n- `removePermissionFromRole()`: Unassigns a permission from a role\n- `addRoleToSubject()`: Assigns a role to a subject\n- `removeRoleFromSubject()`: Unassigns a role from a subject\n- `getRolesForSubject()`: Lists roles assigned to a subject\n- `getPermissionsForRole()`: Lists permissions assigned to a role\n- `getPermissionsForSubject()`: Lists permissions assigned to a subject, that is, all permissions attached to the subject's roles\n- `createSubject()`: Creates/stores a subject\n- `deleteSubject()`: Deletes a subject\n- `getPermissions()`: Lists all permissions\n- `getPermissionById()`: Retrieve a permission by ID\n- `getSubjects()`: Lists all subjects\n- `getSubjectByPrincipal()`: Retrieve a subject by principal\n\n\n#### Creating your own store by implementing the `IStore` interface\n\nWhile the `MemoryStore` is a convenient way to get started, you will maybe feel the need of storing your permissions in a more persistent storage at some point. A `store`, as required by the `AccessControl` constructor, is nothing more than an implementation of the `IStore` interface, which looks like this:\n\n```typescript\nexport interface IStore {\n  getPermissionsForSubject(subject: ISubject\u003c{}\u003e): TPermission[] | Promise\u003cTPermission[]\u003e;\n}\n```\n\nThis interface defines a single method that will allow TSelect to retrieve the permissions assigned to a given subject. It can be either synchronous and return a simple array of permissions or asynchronous by returning a promise that resolves with the list.\n\nNo matter how permissions are stored, TSelect expects a permission to look like this:\n\n```typescript\ntype TPermission = {\n  id: string | number;              // A unique identifier for this permission\n  effect: 'allow' | 'deny';         // Whether this permission allows or denies access\n  resource: string | string[];      // Either a list or a single resource\n  action: string | string[]];       // Either a list or a single action\n  returnedAttributes?: string[];    // A list of attributes to return\n  condition?: TPermissionCondition; // A condition that defines whether or not the permission is applicable\n}\n```\n\n#### Example store implementations\n\nWhile TSelect does not provide other stores than the memory based one out of the box, we're going to take a little time in this documentation and provide some guidance.\n\n##### MongoDB\n\nBecause of its native JSON support, it is very simple to store permissions in MongoDB. We could create a collection `permissions` with a schema that directly maps to the `TPermission` type. In addition, and in order to store the association between roles and permissions, we'd add a `roles` property to permissions and end up with a schema that looks like this:\n\n```typescript\ntype TMongoPermission = {\n  _id: string;\n  effect: 'allow' | 'deny';\n  resource: string | string[];\n  action: string | string[]];\n  returnedAttributes?: string[];\n  condition?: TPermissionCondition;\n  roles: string[]; // Here we store the roles that get assigned the permission\n}\n```\n\nIn our application, we probably already have a `users` collection, which maybe looks like this:\n\n```typescript\ntype TUser = {\n  _id: string;\n  email: string;\n  first_name: string;\n  last_name: string;\n  roles: string[]; // Here we store the user's roles\n}\n```\n\nThen we can write a simple class to implement the `IStore` interface.\n\n```typescript\nimport { Db } from 'mongodb';\nimport { IStore } from './interfaces/store';\nimport { ISubject } from './interfaces/subject';\nimport { TPermission } from './types/permission';\n\n// This should be defined somewhere else in your project.\nexport interface IApplicationSubject extends ISubject\u003c{ _id: string; }\u003e {\n\n}\n\n// This is your custom permission schema.\nexport type TApplicationPermission = TPermission \u0026 {\n  _id: string;\n  roles: string[];\n};\n\n/**\n * Store implementation for TSelect access control.\n */\nexport class MongoStore implements IStore {\n  private db: Db;\n\n  public constructor(db: Db) {\n    this.db = db;\n  }\n\n  public async getPermissionsForSubject(subject: IApplicationSubject): Promise\u003cTApplicationPermission[]\u003e {\n    const userId = subject.get('_id');\n\n    // First we find the user in order to get their roles.\n    const user = await this.db.collection('users').findOne\u003c{ roles: string[] }\u003e({\n      _id: userId\n    }, {\n      projection: { roles: 1 }\n    });\n\n    if (!user) {\n      throw new Error(`Unknown user: ${userId}.`);\n    }\n\n    // Then we find the permissions that correspond to those roles.\n    const permissionsCursor = await this.db.collection('permissions').find\u003cTApplicationPermission\u003e({\n      role: { in: user.roles }\n    });\n\n    // That's it!\n    return permissionsCursor.toArray();\n  }\n\n}\n\n```\n\nThen to use the store:\n\n```typescript\nimport { db } from './db'; // We'll assume that you already have an instance exported.\n\nconst store = new MongoStore(db);\nconst accessControl = new AccessControl({ store });\n```\n\nThat's it! Now each time TSelect needs to know which permissions are associated with a user, it will fetch the data from your Mongo store.\n\n**Note**: We do not detail advanced methods to manage (create/update) your permissions as this is something you should control. If you're looking for inspiration though, have a look at the [MemoryStore](./src/classes/memory-store.ts).\n\n##### MySQL\n\nTODO\n\n\n### Returned attributes in depth: pattern matching and the `Keys` utility\n\nThe following examples apply to systems where the returned attributes are manually filtered using the `Keys.filter()` utility.\n\nTSelect uses a proprietary syntax for describing `returnedAttributes` in combination with `Keys`. The reason is that we want to offer a custom-fit experience when dealing with those attributes as well as make sure that we only perform necessary operations in order to obtain the best possible performance.\n\nThe examples below use a data structure that describes a blog post with various attributes.\n\n```typescript\ntype BlogPost = {\n  id: number:\n  title: string;\n  content: string;\n  author: {\n    id: number;\n    username: string;\n    email: string;\n    hobbies: string[];\n  },\n  comments: {\n    id: number;\n    content: string;\n    author: {\n      id: number;\n      username: string;\n      email: string;\n      hobbies: string[];\n    }\n  }[];\n}\n```\n\n#### Whitelist approach\n\nWhitelisting is the recommended way to describe your returned attributes, because it offers you the peace of mind and clarity of knowing what exactly is being returned to your user.\n\nLet's say we want the author of a blog post to receive all information about a blog post except the email addresses of the comments authors. We would write the returned attributes as such:\n\n```typescript\nconst permission: TPermission = {\n  id: 'User:BlogPost:GetItem',\n  resource: 'blog-posts',\n  action: 'get-item',\n  effect: PermissionEffect.ALLOW,\n  returnedAttributes: [\n    'id',\n    'title',\n    'content',\n    'author.id',\n    'author.username',\n    'author.email',\n    'author.hobbies',\n    'comments.[].id',\n    'comments.[].content',\n    'comments.[].author.id',\n    'comments.[].author.username',\n    'comments.[].author.hobbies',\n ]\n};\n```\n\nBecause we omitted `comments.[].author.email`, the user won't see the commenters emails, assuming that we filtered the payload using `Keys.filter()`.\n\n#### The `!` (bang) operator or the blacklist approach\n\nWhile the whitelist approach provides the most security, there are times where you will prefer to not describe all fields but rather exclude particular fields from the payload. Here comes the `!` operator. In order to obtain the exact same result as previously, we could rewrite the permission as such:\n\n```typescript\nconst permission: TPermission = {\n  id: 'User:BlogPost:GetItem',\n  resource: 'blog-posts',\n  action: 'get-item',\n  effect: PermissionEffect.ALLOW,\n  returnedAttributes: [\n    '!comments.[].author.email',\n  ]\n};\n```\n\nMagic!\n\n*Note:* It is not possible to combine the whitelist and blacklist approaches in a given `returnedAttributes` array\n\n\n#### The `*` (wild card) operator\n\nThe simplest and most permissive `returnedAttributes` can be written as such:\n\n```typescript\nconst permission: TPermission = {\n  id: 'User:BlogPost:GetItem',\n  resource: 'blog-posts',\n  action: 'get-item',\n  effect: PermissionEffect.ALLOW,\n  returnedAttributes: '*'\n};\n```\n\nThis basically means: return everything.\n\n\nWe could also want to simplify part of the returned attributes by combining the whitelist approach and the wild cards. We could for example rewrite the initial example as such:\n\n```typescript\nconst permission: TPermission = {\n  id: 'User:BlogPost:GetItem',\n  resource: 'blog-posts',\n  action: 'get-item',\n  effect: PermissionEffect.ALLOW,\n  returnedAttributes: [\n    'id',\n    'title',\n    'content',\n    'author.*',\n    'comments.[].id',\n    'comments.[].content',\n    'comments.[].author.id',\n    'comments.[].author.username',\n    'comments.[].author.hobbies',\n ]\n};\n```\n\nNotice the `author.*` that basically says: returned everything in the nested `author` object. Note that this doesn't have any influence over the nested `author` objects in the `comments`, but exclusively at the root of the object.\n\n*Info:* When you use the blacklist approach, a `returnedAttributes` with a value of `['!comments.[].author.email']` is essentially equivalent to `['*', '!comments.[].author.email']`\n\n#### Arrays and the `[]` (unwind) operator\n\nYou may have noticed the particular syntax for the `comments` array in the previous examples. This operator allows us to say \"for each element in the array, apply the following pattern\". It is particularly useful when dealing with lists of objects with no predefined length.\n\nWhen dealing with tuples though, each element in the array can be referenced by its index. Let's say, for example, that we only want users to receive the first comment from the `comments` array. We would write the permission as such:\n\n\n```typescript\nconst permission: TPermission = {\n  id: 'User:BlogPost:GetItem',\n  resource: 'blog-posts',\n  action: 'get-item',\n  effect: PermissionEffect.ALLOW,\n  returnedAttributes: [\n    'id',\n    'title',\n    'content',\n    'author.*',\n    'comments.0.id',\n    'comments.0.content',\n    'comments.0.author.id',\n    'comments.0.author.username',\n    'comments.0.author.hobbies',\n ]\n};\n```\n\nThis will ensure that only the first comment is returned.\n\n\n## Inspirations\n\nSpecial heads up to the following modules and their creators for the precious inspiration:\n- [Apache Shiro](https://shiro.apache.org/index.html)\n- [AWS IAM](http://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)\n- [AccessControl](https://onury.io/accesscontrol/?content=guide)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftselect-npm%2Faccess-control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftselect-npm%2Faccess-control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftselect-npm%2Faccess-control/lists"}