UNPKG

@atproto/oauth-scopes

Version:

A library for manipulating and validating ATproto OAuth scopes in TypeScript.

153 lines • 5.98 kB
import { isNsid } from '../lib/nsid.js'; import { Parser } from '../lib/parser.js'; import { LexPermissionSyntax } from '../lib/syntax-lexicon.js'; import { ScopeStringSyntax } from '../lib/syntax-string.js'; import { isScopeStringFor, isScopeSyntaxFor, } from '../lib/syntax.js'; import { RepoPermission } from './repo-permission.js'; import { RpcPermission, isAtprotoDidRefAbsolute, } from './rpc-permission.js'; export { isNsid }; /** * This is used to handle "include:" oauth scope values, used to include * permissions from a lexicon defined permission set. Not being a resource * permission, it does not implement `Matchable`. */ export class IncludeScope { constructor(nsid, aud = undefined) { this.nsid = nsid; this.aud = aud; } toString() { return IncludeScope.parser.format(this); } toPermissions(permissionSet) { return Array.from(this.buildPermissions(permissionSet)); } toScopes(permissionSet) { return Array.from(this.buildPermissions(permissionSet), (p) => p.toString()); } /** * Converts an "include:" to the list of permissions it includes, based on the * lexicon defined permission set. */ *buildPermissions(permissionSet) { for (const lexPermission of permissionSet.permissions) { const syntax = this.parseLexPermission(lexPermission); if (!syntax) continue; const resourcePermission = toResourcePermission(syntax); if (!resourcePermission) continue; if (this.isAllowedPermission(resourcePermission)) { yield resourcePermission; } } } parseLexPermission(permission) { // This function converts permissions listed in the permission set into // their respective ScopeSyntax representations, handling special cases as // needed. if (isLexPermissionForResource(permission, 'repo')) { return new LexPermissionSyntax(permission); } if (isLexPermissionForResource(permission, 'rpc')) { // "rpc" permissions with a defined audience are not allowed in permission // sets if (permission.aud !== undefined && permission.aud !== '*') { return null; } // "rpc" permissions can "inherit" their audience from "aud" param defined // in the "include:<nsid>?aud=<audience>" scope the permission set was // loaded from. if (permission.inheritAud === true && permission.aud === undefined && this.aud !== undefined) { const { inheritAud, ...rest } = permission; return new LexPermissionSyntax({ aud: this.aud, ...rest }); } return new LexPermissionSyntax(permission); } return null; } /** * Verifies that a permission included through a lexicon permission set is * allowed in the context of the `include:` scope. This basically checks that * the permission is "under" the namespace authority of the `include:` scope, * and that it only contains "repo:", "rpc:", or "blob:" permissions. */ isAllowedPermission(permission) { if (permission instanceof RpcPermission) { return permission.lxm.every(this.isParentAuthorityOf, this); } if (permission instanceof RepoPermission) { return permission.collection.every(this.isParentAuthorityOf, this); } throw new TypeError(`Unexpected permission ${permission}`); } /** * Verifies that a permission item's nsid is under the same authority as the * nsid of the lexicon itself (which is the same as the nsid of the `include:` * scope). */ isParentAuthorityOf(otherNsid) { if (otherNsid === '*') { return false; } const lexiconNsid = this.nsid; const groupPrefixEnd = lexiconNsid.lastIndexOf('.'); // There should always be a dot, but since this is a security feature, let's // be strict about it. if (groupPrefixEnd === -1) { throw new TypeError('Dot character (".") missing from lexicon NSID'); } // Make sure that otherNsid is at least as long as the "group prefix" if (groupPrefixEnd >= otherNsid.length - 1) { return false; } // Make sure that the "otherNsid" starts with the group of the lexiconNsid, // up to the dot itself. We check in reverse order as nsids tend to have // long common prefixes. for (let i = groupPrefixEnd; i >= 0; i--) { if (lexiconNsid.charCodeAt(i) !== otherNsid.charCodeAt(i)) { return false; } } return true; } static { this.parser = new Parser('include', { nsid: { multiple: false, required: true, validate: isNsid, }, aud: { multiple: false, required: false, validate: isAtprotoDidRefAbsolute, }, }, 'nsid'); } static fromString(scope) { if (!isScopeStringFor(scope, 'include')) return null; const syntax = ScopeStringSyntax.fromString(scope); return IncludeScope.fromSyntax(syntax); } static fromSyntax(syntax) { const result = IncludeScope.parser.parse(syntax); if (!result) return null; return new IncludeScope(result.nsid, result.aud); } } function toResourcePermission(syntax) { if (isScopeSyntaxFor(syntax, 'repo')) { return RepoPermission.fromSyntax(syntax); } if (isScopeSyntaxFor(syntax, 'rpc')) { return RpcPermission.fromSyntax(syntax); } return null; } function isLexPermissionForResource(permission, type) { return permission.resource === type; } //# sourceMappingURL=include-scope.js.map