jinaga
Version:
Data management for web and mobile applications.
303 lines (257 loc) • 9.97 kB
text/typescript
import { FactRepository, LabelOf, User, buildModel } from "../../src";
import { AuthorizationRules } from '../../src/authorization/authorizationRules';
import { dehydrateFact } from '../../src/fact/hydrate';
import { MemoryStore } from '../../src/memory/memory-store';
import { FactRecord, FactReference, factReferenceEquals } from '../../src/storage';
import { Trace } from '../../src/util/trace';
function givenUserFact(identity = 'authorized-user') {
return dehydrateFact({
type: 'Jinaga.User',
publicKey: identity
})[0];
}
function givenGroupMember() {
return dehydrateFact({
type: 'Member',
group: {
type: 'Group',
identity: 'known-group'
},
user: {
type: 'Jinaga.User',
publicKey: 'authorized-user'
}
});
}
function givenMessage(sender = 'authorized-user') {
return dehydrateFact({
type: 'Message',
author: {
type: 'Jinaga.User',
publicKey: sender
}
})[1];
}
function givenAnonymousMessage() {
return dehydrateFact({
type: 'Message',
author: null
})[0];
}
function givenMessageFromMultipleAuthors() {
return dehydrateFact({
type: 'Message',
author: [
{
type: 'Jinaga.User',
publicKey: 'authorized-user'
},{
type: 'Jinaga.User',
publicKey: 'unauthorized-user'
}
]
})[2];
}
function givenUnauthorizedMessageFromPotentiallyMultipleAuthors() {
return dehydrateFact({
type: 'Message',
author: [
{
type: 'Jinaga.User',
publicKey: 'unauthorized-user'
}
]
})[1];
}
function givenMessageInGroup() {
return dehydrateFact({
type: 'Message',
group: {
type: 'Group',
identity: 'known-group'
}
})[1];
}
function givenAuthorizationRules(builder: (a: AuthorizationRules) => AuthorizationRules =
a => a) {
return builder(new AuthorizationRules(model));
}
async function whenAuthorize(authorizationRules: AuthorizationRules, userFact: FactRecord | null, fact: FactRecord) {
const store = new MemoryStore();
const facts = [ ...givenGroupMember(), givenUserFact('unauthorized-user') ];
await store.save(facts.map(f => ({ fact: f, signatures: [] })));
const userPublicKey = userFact && userFact.fields.hasOwnProperty("publicKey")
? userFact.fields.publicKey : null;
const candidateKeys = userPublicKey
? [ userPublicKey ] : [];
const allFacts = [ ...facts, fact ];
const allReferences = ancestors(fact, [...facts, fact]);
const transitiveClosure = allReferences
.map(reference => allFacts.find(factReferenceEquals(reference))!);
const authorized = await authorizationRules.getAuthorizedPopulation(candidateKeys, fact, transitiveClosure, store);
return authorized.quantifier === "everyone" ||
(authorized.quantifier === "some" && userPublicKey && authorized.authorizedKeys.indexOf(userPublicKey) >= 0);
}
function ancestors(reference: FactReference, facts: FactRecord[]): FactReference[] {
const fact = facts.find(factReferenceEquals(reference));
if (!fact) {
throw new Error(`Fact ${reference.type}:${reference.hash} not found.`);
}
const allPredecessors: FactReference[] = [];
for (const predecessor of Object.values(fact.predecessors)) {
if (Array.isArray(predecessor)) {
allPredecessors.push(...predecessor);
}
else if (predecessor) {
allPredecessors.push(predecessor);
}
}
return [ reference, ...allPredecessors
.flatMap(p => ancestors(p, facts))];
}
class Group {
static Type = "Group" as const;
type = Group.Type;
constructor(
public identity: string
) {}
}
class Member {
static Type = "Member" as const;
type = Member.Type;
constructor(
public group: Group,
public user: User
) {}
}
class Message {
static Type = "Message" as const;
type = Message.Type;
constructor(
public author: User,
public group: Group
) {}
}
class Approval {
static Type = "Approval" as const;
type = Approval.Type;
constructor(
public message: Message,
public approver: User
) {}
}
const model = buildModel(b => b
.type(User)
.type(Group)
.type(Member, m => m
.predecessor("group", Group)
.predecessor("user", User)
)
.type(Message, m => m
.predecessor("author", User)
.predecessor("group", Group)
)
.type(Approval, m => m
.predecessor("message", Message)
.predecessor("approver", User)
)
);
const membersOfGroup = (message: LabelOf<Message>, facts: FactRepository) =>
facts.ofType(Member)
.join(member => member.group, message.group)
.selectMany(member => facts.ofType(User)
.join(user => user, member.user)
);
describe('Authorization rules', () => {
Trace.off();
it('should reject all facts by default', async () => {
const authorizationRules = givenAuthorizationRules();
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, null, fact);
expect(authorized).toBeFalsy();
});
it('should accept known facts', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.any(Message.Type));
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, null, fact);
expect(authorized).toBeTruthy();
});
it('should reject unknown facts', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.any(Message.Type));
const fact = givenUserFact();
const authorized = await whenAuthorize(authorizationRules, null, fact);
expect(authorized).toBeFalsy();
});
it('should reject known fact when not logged in', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, null, fact);
expect(authorized).toBeFalsy();
});
it('should accept permissive fact when not logged in', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.any(Message.Type));
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, null, fact);
expect(authorized).toBeTruthy();
});
it('should reject known fact from no user', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const userFact = givenUserFact();
const fact = givenAnonymousMessage();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeFalsy();
});
it('should reject known fact from unauthorized user', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const userFact = givenUserFact('unauthorized-user');
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeFalsy();
});
it('should accept known fact from authorized user', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const userFact = givenUserFact();
const fact = givenMessage();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeTruthy();
});
it('should accept known fact from multiple users', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const userFact = givenUserFact();
const fact = givenMessageFromMultipleAuthors();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeTruthy();
});
it('should reject fact from multiple users when authorized is not in list', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, m => m.author));
const userFact = givenUserFact();
const fact = givenUnauthorizedMessageFromPotentiallyMultipleAuthors();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeFalsy();
});
it('should accept fact from a member of a group', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, membersOfGroup));
const userFact = givenUserFact();
const fact = givenMessageInGroup();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeTruthy();
});
it('should reject fact from a non-member of a group', async () => {
const authorizationRules = givenAuthorizationRules(a => a
.type(Message, membersOfGroup));
const userFact = givenUserFact('unauthorized-user');
const fact = givenMessageInGroup();
const authorized = await whenAuthorize(authorizationRules, userFact, fact);
expect(authorized).toBeFalsy();
});
});