From 4c75ea8f5b0a3eeae2c24e6477cf4b1946a586f8 Mon Sep 17 00:00:00 2001 From: Buck Perley Date: Wed, 9 Feb 2022 18:50:27 -0600 Subject: [PATCH] adds services satisfier --- src/index.ts | 3 +- src/satisfiers.ts | 48 +++++++++++++++++++++++++++- src/service.ts | 3 ++ tests/satisfiers.spec.ts | 69 +++++++++++++++++++++++++++++++++++++++- tests/service.spec.ts | 2 +- 5 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 44b4bc0..b633d98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ export * from './identifier' export * from './caveat' export * from './lsat' export * from './types' -export { expirationSatisfier } from './satisfiers' +export * from './satisfiers' export * from './macaroon' +export * from './service' diff --git a/src/satisfiers.ts b/src/satisfiers.ts index c13afce..f6073d4 100644 --- a/src/satisfiers.ts +++ b/src/satisfiers.ts @@ -3,7 +3,13 @@ * ones that don't require the request object in a server as these can be used anywhere. */ -import { Satisfier, Caveat } from '.' +import { + Satisfier, + Caveat, + InvalidServicesError, + SERVICES_CAVEAT_CONDITION, + decodeServicesCaveat, +} from '.' /** * @description A satisfier for validating expiration caveats on macaroon. Used in the exported @@ -28,3 +34,43 @@ export const expirationSatisfier: Satisfier = { return true }, } + +export const createServicesSatisfier = (targetService: string): Satisfier => { + // validate targetService + if (typeof targetService !== 'string') throw new InvalidServicesError() + + return { + condition: SERVICES_CAVEAT_CONDITION, + satisfyPrevious: (prev: Caveat, curr: Caveat): boolean => { + const prevServices = decodeServicesCaveat(prev.value.toString()) + const currentServices = decodeServicesCaveat(curr.value.toString()) + let previouslyAllowed = new Map() + + // making typescript happy + if (!Array.isArray(prevServices) || !Array.isArray(currentServices)) + throw new InvalidServicesError() + + previouslyAllowed = prevServices.reduce( + (prev, current) => prev.set(current.name, current.tier), + previouslyAllowed + ) + for (const service of currentServices) { + if (!previouslyAllowed.has(service.name)) return false + const prevTier: number = previouslyAllowed.get(service.name) + if (prevTier > service.tier) return false + } + + return true + }, + satisfyFinal: (caveat: Caveat): boolean => { + const services = decodeServicesCaveat(caveat.value.toString()) + // making typescript happy + if (!Array.isArray(services)) throw new InvalidServicesError() + + for (const service of services) { + if (service.name === targetService) return true + } + return false + }, + } +} diff --git a/src/service.ts b/src/service.ts index bc7f1f7..dd721f8 100644 --- a/src/service.ts +++ b/src/service.ts @@ -70,6 +70,9 @@ export class Service extends bufio.Struct { } } +// the condition value in a caveat for services +export const SERVICES_CAVEAT_CONDITION = 'services' + /** * * @param {string} s - raw services string of format `name:tier,name:tier` diff --git a/tests/satisfiers.spec.ts b/tests/satisfiers.spec.ts index 5ec2d24..b993c41 100644 --- a/tests/satisfiers.spec.ts +++ b/tests/satisfiers.spec.ts @@ -1,5 +1,12 @@ import { expect } from 'chai' -import { Caveat, expirationSatisfier, verifyCaveats } from '../src' +import { + Caveat, + expirationSatisfier, + verifyCaveats, + SERVICES_CAVEAT_CONDITION, + InvalidServicesError, + createServicesSatisfier, +} from '../src' import { Satisfier } from '../src/types' describe('satisfiers', () => { @@ -52,4 +59,64 @@ describe('satisfiers', () => { ).to.be.false }) }) + + describe('services satisfier', () => { + let firstCaveat: Caveat, secondCaveat: Caveat + + beforeEach(() => { + firstCaveat = Caveat.decode(`${SERVICES_CAVEAT_CONDITION}=foo:0,bar:1`) + secondCaveat = Caveat.decode(`${SERVICES_CAVEAT_CONDITION}=foo:1,bar:1`) + }) + + const runTest = ( + caveats: Caveat[], + targetService: string + ): boolean | Error => { + const satisfier = createServicesSatisfier(targetService) + return verifyCaveats(caveats, satisfier) + } + + it('should fail to create satisfier on invalid target service', () => { + const invalidTargetServices = [12, { foo: 'bar' }, ['a', 'b', 'c']] + for (const target of invalidTargetServices) { + // @ts-expect-error + expect(() => createServicesSatisfier(target)).to.throw( + InvalidServicesError + ) + } + }) + + it('should throw InvalidServicesError if caveats are incorrect', () => { + const invalidCaveatValue = Caveat.decode( + `${SERVICES_CAVEAT_CONDITION}=noTier` + ) + + expect( + () => runTest([invalidCaveatValue, firstCaveat], 'foo'), + 'invalid caveat value' + ).to.throw(InvalidServicesError) + expect( + () => runTest([firstCaveat, invalidCaveatValue], 'foo'), + 'invalid caveat value' + ).to.throw(InvalidServicesError) + }) + + it('should not allow any services that were not previously allowed', () => { + const invalidCaveat = Caveat.decode(`${SERVICES_CAVEAT_CONDITION}=baz:0`) + const caveats = [firstCaveat, invalidCaveat] + expect(runTest(caveats, 'foo')).to.be.false + }) + + it('should validate only increasingly restrictive (higher) service tiers', () => { + // order matters + const caveats = [secondCaveat, firstCaveat] + expect(runTest(caveats, 'foo')).to.be.false + }) + + it('should validate for the specified target service', () => { + const caveats = [firstCaveat, secondCaveat] + expect(runTest(caveats, 'foo')).to.be.true + expect(runTest(caveats, 'baz')).to.be.false + }) + }) }) diff --git a/tests/service.spec.ts b/tests/service.spec.ts index 63504d6..0cfa4ad 100644 --- a/tests/service.spec.ts +++ b/tests/service.spec.ts @@ -9,7 +9,7 @@ import { SERVICE_CAPABILITIES_SUFFIX, } from '../src/service' -describe.only('services', () => { +describe('services', () => { it('can encode and decode service caveats', () => { const tests = [ {