chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import {
bareSpaceTypes,
expandSpaceScopes,
parseScopesForDisplay,
parseSpaceScope,
ScopePermissions,
spaceScopeAllowsManage,
spaceScopeMatches,
spaceTypeCollections,
} from '@pdsjs/core/scope';
import { describe, expect, it } from 'vitest';
const USER = 'did:plc:user';
const AUTHORITY = 'did:plc:authority';
/**
* parseSpaceScope returns null for scopes it does not recognise; these cases
* all expect a grant, so assert that here rather than at every use.
* @param {string} scope
* @returns {any}
*/
function parse(scope) {
const grant = parseSpaceScope(scope);
if (!grant) throw new Error(`expected a space grant: ${scope}`);
return grant;
}
describe('parseSpaceScope', () => {
it('ignores scopes of other kinds', () => {
expect(parseSpaceScope('repo:com.example.post')).toBeNull();
expect(parseSpaceScope('atproto')).toBeNull();
});
it('defaults authority to self and skey to *', () => {
const g = parse('space:com.example.forum');
expect(g.spaceType).toBe('com.example.forum');
expect(g.authority).toBe('self');
expect(g.skey).toBe('*');
});
it('defaults actions to everything but read_self', () => {
const g = parse('space:com.example.forum');
expect(g.actions).toEqual(['read', 'create', 'update', 'delete']);
expect(g.actions).not.toContain('read_self');
});
it('leaves collections null when omitted', () => {
// Null means "the space type's declared collections" — only the space type
// can resolve that, so the grant cannot.
expect(parse('space:com.example.forum').collections).toBeNull();
});
it('parses every parameter', () => {
const g = parse(
`space:com.example.forum?authority=${AUTHORITY}&skey=general` +
'&collection=com.example.post&collection=com.example.reply' +
'&action=read&action=create&manage=delete',
);
expect(g).toEqual({
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
collections: ['com.example.post', 'com.example.reply'],
actions: ['read', 'create'],
manage: ['delete'],
});
});
it('accepts wildcards', () => {
const g = parse('space:*?authority=*&skey=*&collection=*');
expect(g.spaceType).toBe('*');
expect(g.authority).toBe('*');
expect(g.collections).toEqual(['*']);
});
it('rejects a space type that is not an NSID', () => {
expect(parseSpaceScope('space:nodots')).toBeNull();
});
it('rejects a malformed authority', () => {
expect(parseSpaceScope('space:com.e.f?authority=alice.test')).toBeNull();
});
it('rejects an over-long skey', () => {
const skey = 'x'.repeat(513);
expect(parseSpaceScope(`space:com.e.f?skey=${skey}`)).toBeNull();
});
it('rejects a scope whose actions are all unrecognised', () => {
expect(parseSpaceScope('space:com.e.f?action=frobnicate')).toBeNull();
});
it('drops unrecognised manage verbs', () => {
expect(parse('space:com.e.f?manage=frobnicate').manage).toEqual([]);
});
});
describe('spaceScopeMatches', () => {
const req = (over = {}) => ({
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
action: 'read',
userDid: USER,
...over,
});
it('matches on the full tuple', () => {
const g = parse(
`space:com.example.forum?authority=${AUTHORITY}&skey=general`,
);
expect(spaceScopeMatches(g, req())).toBe(true);
expect(spaceScopeMatches(g, req({ skey: 'other' }))).toBe(false);
expect(spaceScopeMatches(g, req({ spaceType: 'com.other.kind' }))).toBe(
false,
);
expect(spaceScopeMatches(g, req({ authority: 'did:plc:other' }))).toBe(
false,
);
});
it('resolves self to the granting user', () => {
const g = parse('space:com.example.forum');
expect(spaceScopeMatches(g, req({ authority: USER }))).toBe(true);
// The space belongs to someone else, so a `self` grant does not reach it.
expect(spaceScopeMatches(g, req({ authority: AUTHORITY }))).toBe(false);
});
it('honours wildcards', () => {
const g = parse('space:*?authority=*&skey=*');
expect(spaceScopeMatches(g, req({ spaceType: 'anything.at.all' }))).toBe(
true,
);
});
it('lets read imply read_self but not the reverse', () => {
const readOnly = parse(
`space:com.example.forum?authority=${AUTHORITY}&action=read`,
);
expect(spaceScopeMatches(readOnly, req({ action: 'read_self' }))).toBe(
true,
);
const selfOnly = parse(
`space:com.example.forum?authority=${AUTHORITY}&action=read_self`,
);
expect(spaceScopeMatches(selfOnly, req({ action: 'read' }))).toBe(false);
expect(spaceScopeMatches(selfOnly, req({ action: 'read_self' }))).toBe(
true,
);
});
it('constrains writes by collection', () => {
const g = parse(
`space:com.example.forum?authority=${AUTHORITY}&collection=com.example.post`,
);
expect(
spaceScopeMatches(
g,
req({ action: 'create', collection: 'com.example.post' }),
),
).toBe(true);
expect(
spaceScopeMatches(
g,
req({ action: 'create', collection: 'com.example.reply' }),
),
).toBe(false);
});
it('falls back to the space type declared collections', () => {
const g = parse(`space:com.example.forum?authority=${AUTHORITY}`);
expect(
spaceScopeMatches(
g,
req({
action: 'create',
collection: 'com.example.post',
declaredCollections: ['com.example.post'],
}),
),
).toBe(true);
// No declared collections and none granted means no write targets at all.
expect(
spaceScopeMatches(g, req({ action: 'create', collection: 'com.e.p' })),
).toBe(false);
});
it('does not let a record action imply management', () => {
const g = parse(`space:com.example.forum?authority=${AUTHORITY}`);
expect(
spaceScopeAllowsManage(g, {
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
op: 'delete',
userDid: USER,
}),
).toBe(false);
});
it('permits management when the verb is granted', () => {
const g = parse(
`space:com.example.forum?authority=${AUTHORITY}&manage=create&manage=delete`,
);
const base = {
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
userDid: USER,
};
expect(spaceScopeAllowsManage(g, { ...base, op: 'create' })).toBe(true);
expect(spaceScopeAllowsManage(g, { ...base, op: 'update' })).toBe(false);
});
});
describe('ScopePermissions', () => {
it('collects space grants alongside the others', () => {
const p = new ScopePermissions(
`repo:com.example.post space:com.example.forum?authority=${AUTHORITY}`,
);
expect(p.spacePermissions).toHaveLength(1);
expect(p.repoPermissions).toHaveLength(1);
});
it('allowsSpace honours the grant', () => {
const p = new ScopePermissions(
`space:com.example.forum?authority=${AUTHORITY}&action=read`,
);
const req = {
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
action: 'read',
userDid: USER,
};
expect(p.allowsSpace(req)).toBe(true);
expect(p.allowsSpace({ ...req, action: 'create' })).toBe(false);
});
/** @param {string} scope */
const reaches = (scope) =>
new ScopePermissions(scope).allowsSpace({
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'k',
action: 'delete',
collection: 'com.e.p',
});
it('the account password session covers spaces', () => {
expect(reaches('com.atproto.access')).toBe(true);
});
it('an OAuth session reaches no space it was not granted', () => {
// `atproto` is in every OAuth authorization request, so a session that
// reached spaces through it would give every application every space.
// `transition:generic` predates permissioned data entirely.
expect(reaches('atproto')).toBe(false);
expect(reaches('atproto transition:generic')).toBe(false);
expect(reaches('atproto repo:com.example.post blob:image/*')).toBe(false);
});
it('the same session reaches a space its scope names', () => {
expect(
reaches(
`atproto space:com.example.forum?authority=${AUTHORITY}&collection=com.e.p`,
),
).toBe(true);
});
it('grants nothing without a space scope', () => {
const p = new ScopePermissions('repo:com.example.post');
expect(
p.allowsSpace({
spaceType: 'com.example.forum',
authority: AUTHORITY,
skey: 'general',
action: 'read',
userDid: USER,
}),
).toBe(false);
});
});
describe('parseScopesForDisplay', () => {
it('surfaces space grants for the consent screen', () => {
const display = parseScopesForDisplay(
`space:com.example.forum?authority=${AUTHORITY}&action=read repo:com.example.post`,
);
expect(display.spacePermissions).toHaveLength(1);
expect(display.spacePermissions[0].spaceType).toBe('com.example.forum');
expect(display.spacePermissions[0].actions).toEqual(['read']);
});
});
describe('space types without collections', () => {
const FORUM = {
lexicon: 1,
id: 'com.example.forum',
defs: {
main: {
type: 'space',
collections: ['com.example.post', 'com.example.reply', '*', 'nodots'],
},
},
};
it('reads the collections a space type declares', () => {
expect(spaceTypeCollections(FORUM)).toEqual([
'com.example.post',
'com.example.reply',
]);
expect(spaceTypeCollections({ defs: { main: { type: 'space' } } })).toEqual(
[],
);
expect(
spaceTypeCollections({ defs: { main: { type: 'record' } } }),
).toBeNull();
expect(spaceTypeCollections(null)).toBeNull();
});
it('names the space types a scope grants without a collection', () => {
expect(
bareSpaceTypes(
'atproto space:com.example.forum?authority=* space:com.example.forum ' +
'space:com.example.chat?collection=com.example.msg space:* repo:com.example.post',
),
).toEqual(['com.example.forum']);
});
it('writes the declared collections into those scopes', () => {
const scope = expandSpaceScopes(
'atproto space:com.example.forum?authority=*&manage=create ' +
'space:com.example.forum space:com.example.chat?collection=com.example.msg ' +
'space:com.example.empty',
{
'com.example.forum': ['com.example.post', 'com.example.reply'],
'com.example.empty': [],
},
);
expect(scope.split(' ')).toEqual([
'atproto',
'space:com.example.forum?authority=*&manage=create&collection=com.example.post&collection=com.example.reply',
'space:com.example.forum?collection=com.example.post&collection=com.example.reply',
'space:com.example.chat?collection=com.example.msg',
'space:com.example.empty',
]);
const grant = parse(scope.split(' ')[1]);
expect(grant.collections).toEqual([
'com.example.post',
'com.example.reply',
]);
expect(grant.authority).toBe('*');
expect(grant.manage).toEqual(['create']);
});
});