UNPKG

@httpc/kit

Version:

httpc toolbox for building function-based API with minimal code and end-to-end type safety

322 lines (321 loc) 11.3 kB
import Parser from "./Parser"; import * as Token from "./Token"; import Serializer from "./Serializer"; import assert from "assert"; export { Token as PermissionToken }; export { Parser as PermissionParser }; export * from "./Parser"; export { Serializer as PermissionSerializer }; export * from "./Serializer"; export * from "./model"; export class InvalidClaim extends Error { constructor(claim, message) { super(message || "Invalid claim"); this.claim = claim; } } export class AuthorizationBuilder { constructor(extend) { this._claims = []; if (extend) { this.add(extend); } } add(claim) { if (typeof claim === "string") { Parser.parseAuthorization(claim).forEach(x => this.add(x)); return this; } if (Array.isArray(claim)) { claim.forEach(x => this.add(x)); return this; } if (claim instanceof AuthorizationBuilder) { claim._claims.forEach(x => this.add(x)); return this; } if (claim instanceof Authorization) { claim.claims.forEach(x => this.add(x)); return this; } this._claims.push({ token: Array.isArray(claim.token) ? claim.token.slice() : claim.token, scope: Array.isArray(claim.scope) ? claim.scope.slice() : claim.scope, }); return this; } build() { return new Authorization(this._claims); } } export class Authorization { constructor(claims) { this.claims = claims; } *[Symbol.iterator]() { yield* this.claims; } merge(auth) { return new AuthorizationBuilder(this).add(auth).build(); } toString() { return Serializer.serialize(this); } static parse(claims) { return new AuthorizationBuilder(claims).build(); } } export class AssertionBuilder { constructor(extend) { this._claims = []; if (extend) { this.add(extend); } } add(claim) { if (typeof claim === "string") { Parser.parseAssertion(claim).forEach(x => this.add(x)); return this; } if (Array.isArray(claim)) { claim.forEach(x => this.add(x)); return this; } if (claim instanceof AssertionBuilder) { claim._claims.forEach(x => this.add(x)); return this; } if (claim instanceof Assertion) { claim.claims.forEach(x => this.add(x)); return this; } this._claims.push({ token: Array.isArray(claim.token) ? claim.token.slice() : claim.token, scope: Array.isArray(claim.scope) ? claim.scope.slice() : claim.scope, negative: claim.negative, }); return this; } build() { return new Assertion(this._claims); } } export class Assertion { constructor(claims) { this.claims = claims; } *[Symbol.iterator]() { yield* this.claims; } toString() { return Serializer.serialize(this); } test(auth) { if (this.claims.length === 0) return { success: true }; for (const assertion of this.claims) { let isPass = false; for (const claim of auth) { const isTokenPass = Token.match(claim.token, assertion.token); const isScopePass = assertion.scope ? claim.scope ? Token.match(claim.scope, assertion.scope) : false : true; if (isTokenPass && isScopePass) { isPass = true; break; } } isPass = isPass !== assertion.negative; if (!isPass) { return { success: false, failed: assertion }; } } return { success: true }; } static parse(claims) { return new AssertionBuilder(claims).build(); } } export class PermissionsChecker { constructor(options) { this._model = options?.model; this._cache = options?.cache === true ? new Map() : options?.cache || undefined; } can(authorization, assertion) { return this.test(authorization, assertion).success; } test(authorization, assertion) { [authorization] = this._getAuthorization(authorization); authorization = this.validate(authorization); [assertion] = this._getAssertion(assertion); assertion = this.validate(assertion); if (assertion.claims.length === 0) { return { success: true }; } const model = this._model; if (!model) { return assertion.test(authorization); } for (const assertClaim of assertion) { let isPass = false; for (const authClaim of authorization) { const isTokenPass = tokenMatch(authClaim.token, assertClaim.token); const isScopePass = assertClaim.scope ? authClaim.scope ? Token.match(authClaim.scope, assertClaim.scope) : false : true; if (isTokenPass && isScopePass) { isPass = true; break; } } isPass = isPass !== assertClaim.negative; if (!isPass) { return { success: false, failed: assertClaim }; } } return { success: true }; function tokenMatch(source, target) { assert(model, "model"); if (Token.match(source, target)) { return true; } const def = model.find(source, "exact"); // must be defined, because it already passed validation, if error -> this is a bug assert(def, "token definition not found"); if (def.includes && def.includes.length > 0) { for (const include of def.includes) { if (tokenMatch(Parser.parseToken(include), target)) { return true; } } } return false; } } supports(authorization) { const model = this._model; if (!model) return true; [authorization] = this._getAuthorization(authorization); for (const { token } of authorization) { if (!model.find(token, "exact")) { return false; } } return true; } parse(what, value) { const model = this._model; let [instance, validated] = what === "assertion" ? this._getAssertion(value) : this._getAuthorization(value); if (!model || validated) { return instance; } instance = this.validate(instance); if (this._cache) { const key = this._getCacheKey(what, value); this._cache.set(key, [instance, true]); } return instance; } validate(what) { const model = this._model; if (!model) return what; for (const claim of what) { if (!model.find(claim.token, "exact")) { throw new InvalidClaim(what instanceof Assertion ? Serializer.serializeAssertionClaim(claim) : Serializer.serializeAuthorizationClaim(claim)); } } return this.consolidate(what); } consolidate(what) { const model = this._model; // try to reduce the instance // - alias substitution (model only) // - eliminating duplicate if (what instanceof Authorization) { let isConsolidated = false; let claims = []; for (let claim of what) { // alias substitution const deAliasedToken = replaceAlias(claim); if (deAliasedToken) { isConsolidated = true; claim = { token: deAliasedToken, scope: claim.scope, }; } // look for already present const existing = claims.find(x => x.scope === claim.scope && Token.equals(x.token, claim.token)); if (existing) { isConsolidated = true; continue; } claims.push(claim); } return isConsolidated ? new Authorization(claims) : what; } else if (what instanceof Assertion) { let isConsolidated = false; let claims = []; for (let claim of what) { // alias substitution const deAliasedToken = replaceAlias(claim); if (deAliasedToken) { isConsolidated = true; claim = { token: deAliasedToken, scope: claim.scope, negative: claim.negative, }; } // look for already present const existing = claims.find(x => x.scope === claim.scope && Token.equals(x.token, claim.token) && !!x.negative === !!claim.negative); if (existing) { isConsolidated = true; continue; } claims.push(claim); } return isConsolidated ? new Assertion(claims) : what; } throw new Error("Invalid param: authorization or assertion required"); function replaceAlias(claim) { if (!model) return; // find works with alias const def = model.find(claim.token, "exact"); if (!def) { throw new InvalidClaim(Serializer.serializeTokenClaim(claim.token)); } // if token are different, it means it matched an alias if (!Token.equals(claim.token, def.fullToken)) { return Token.simplify(def.fullToken); } } } _getAuthorization(authorization) { return this._getCachedOrCreate(authorization, "authorization", authorization => Authorization.parse(authorization)); } _getAssertion(assertion) { return this._getCachedOrCreate(assertion, "assertion", assertion => Assertion.parse(assertion)); } _getCachedOrCreate(value, prefix, factory) { if (typeof value !== "string") { return [value, false]; } if (!this._cache) { return [factory(value), false]; } const key = this._getCacheKey(prefix, value); let item = this._cache.get(key); if (!item) { this._cache.set(key, item = [factory(value), false]); } return item; } _getCacheKey(prefix, value) { return `${prefix}/${value}`; } }