@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
JavaScript
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}`;
}
}