Convert the permissions tests over to mocha & chai.

Fixes #3188

Nothing of substance has really changed in the bodies of the tests
themselves; they are the same except for some find & replace changes
like:

    s/test.isTrue/chai.assert.isOk/g

...but they pass now.

This also adds explicit imports & exports for the `SandstormPermissions`
global, and adds a side-effect only import of another module to the
tests. This was done now because:

* Unless we pass `--full-app` to `meteor test`, meteor does not
  implicitly load all of our modules.
* Passing `--full-app` runs into problems trying to start the smtp
  server on startup, because our code is written to assume it was passed
  FDs by the gateway, but `meteor test` is running it in a more
  conventional fashion.
* It was easier to just fix the imports (which is what we want to do
  long term anyway) than to fight with `--full-app`.

This also turns the tests back on in CI.
This commit is contained in:
Ian Denhardt committed 2020-04-27 16:12:28 -04:00
1 parent d366557c18
commit 010dcce62d
16 files changed
+884 -811

No files matched your search

-2
View File
@@ -4,8 +4,6 @@ on: [push, pull_request]
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache"
# TODO: remove this. See https://github.com/sandstorm-io/sandstorm/issues/3188
SKIP_UNITTESTS: 1
jobs:
build_and_test:
runs-on: ubuntu-latest
+1
View File
@@ -43,3 +43,4 @@ google-oauth@1.3.0
dynamic-import@0.5.1
tap:i18n
typescript@3.7.5
meteortesting:mocha
+3
View File
@@ -64,6 +64,9 @@ mdg:validation-error@0.5.1
meteor@1.9.3
meteor-base@1.4.0
meteorspark:util@0.2.0
meteortesting:browser-tests@1.3.3
meteortesting:mocha@2.0.0
meteortesting:mocha-core@7.0.1
minifier-css@1.5.0
minifier-js@2.6.0
minimongo@1.5.0
@@ -1,796 +0,0 @@
// Sandstorm - Personal Cloud Sandbox
// Copyright (c) 2015 Sandstorm Development Group, Inc. and contributors
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO(cleanup): These tests are no longer run in any automated fashion.
// Figure out a new strategy for making sure we run tests.
import Crypto from "crypto";
import { Meteor } from "meteor/meteor";
import { Match, check } from "meteor/check";
const globalDb = new SandstormDb();
// TODO(cleanup): Use a lightweight fake (minimongo-based?) database here and construct a clean
// instance at the start of each test case.
const localizedTextPattern = {
defaultText: String,
localizations: Match.Optional([{ locale: String, text: String }]),
};
const roleDefPattern = {
title: Match.Optional(localizedTextPattern),
verbPhrase: Match.Optional(localizedTextPattern),
description: Match.Optional(localizedTextPattern),
permissions: [Boolean],
obsolete: Match.Optional(Boolean),
default: Match.Optional(Boolean),
};
const permissionDefPattern = {
name: String,
title: Match.Optional(localizedTextPattern),
description: Match.Optional(localizedTextPattern),
obsolete: Match.Optional(Boolean),
};
const viewInfoPattern = {
permissions: Match.Optional([permissionDefPattern]),
roles: Match.Optional([roleDefPattern]),
deniedPermissions: Match.Optional([Boolean]),
matchRequests: Match.Optional(Object), // TODO
matchOffers: Match.Optional(Object), // TODO
};
class Grain {
constructor(db, account, viewInfo, isPublic) {
check(db, SandstormDb);
check(account, Account);
check(viewInfo, viewInfoPattern);
this.viewInfo = viewInfo;
this.id = Crypto.randomBytes(10).toString("hex");
db.collections.grains.insert({
_id: this.id,
packageId: "mock-package-id",
appId: "mock-app-id",
appVersion: 0,
userId: account.id,
identityId: Crypto.randomBytes(10).toString("hex"),
title: "mock-grain-title",
cachedViewInfo: this.viewInfo,
private: !isPublic,
});
}
}
class Account {
constructor(db, isAdmin) {
check(db, SandstormDb);
check(isAdmin, Boolean);
const name = Crypto.randomBytes(10).toString("hex");
this.db = db;
this.id = Accounts.insertUserDoc(
{ profile: { name: name }, },
{ type: "account", loginCredentials: [], nonloginCredentials: [], isAdmin: isAdmin, });
}
mayOpenGrain(grain) {
check(grain, Grain);
return SandstormPermissions.mayOpenGrain(globalDb, { grain: { _id: grain.id,
accountId: this.id, }, });
}
grainPermissions(grain) {
check(grain, Grain);
return SandstormPermissions.grainPermissions(globalDb,
{ grain: { _id: grain.id,
accountId: this.id, }, },
grain.viewInfo).permissions;
}
_shareTo(grainId, owner, roleAssignment, membraneRequirements) {
return createNewTokenHelper(this.db, grainId, { accountId: this.id },
owner, roleAssignment, membraneRequirements);
}
shareToAccount(grain, recipient, roleAssignment, membraneRequirements) {
check(grain, Grain);
check(recipient, Account);
return this._shareTo(grain.id, { user: { accountId: recipient.id, title: "share" } },
roleAssignment, membraneRequirements);
}
shareToWebkey(grain, roleAssignment, membraneRequirements) {
check(grain, Grain);
const result = this._shareTo(grain.id, { webkey: { forSharing: true } },
roleAssignment, membraneRequirements);
return new Webkey(this.db, result.token, result.id, grain);
}
}
function createNewTokenHelper(db, grainId, provider, owner, roleAssignment, membraneRequirements) {
check(db, SandstormDb);
check(grainId, String);
check(roleAssignment, db.roleAssignmentPattern);
const result = SandstormPermissions.createNewApiToken(db, provider, grainId, "<petname>",
roleAssignment, owner);
if (membraneRequirements) {
membraneRequirements.forEach((requirement) => {
db.collections.apiTokens.update(
{ _id: result.id },
{ $push: { requirements: requirement } });
});
}
return result;
}
class Webkey {
constructor(db, rawToken, hashedToken, grain) {
check(db, SandstormDb);
check(rawToken, String);
check(hashedToken, String);
check(grain, Grain);
this.db = db;
this.rawToken = rawToken;
this.hashedToken = hashedToken;
this.grain = grain;
}
mayOpenGrain() {
return SandstormPermissions.mayOpenGrain(globalDb, { token: { _id: this.hashedToken,
grainId: this.grain.id, }, });
}
grainPermissions() {
return SandstormPermissions.grainPermissions(globalDb,
{ token: { _id: this.hashedToken,
grainId: this.grain.id, }, },
this.grain.viewInfo).permissions;
}
_shareTo(owner, roleAssignment, membraneRequirements) {
return createNewTokenHelper(this.db, this.grain.id, { rawParentToken: this.rawToken }, owner,
roleAssignment, membraneRequirements);
}
shareToAccount(recipient, roleAssignment, membraneRequirements) {
check(recipient, Account);
return this._shareTo({ user: { accountId: recipient.id, title: "share" } },
roleAssignment, membraneRequirements);
}
shareToWebkey(roleAssignment, membraneRequirements) {
const result = this._shareTo({ webkey: { forSharing: true } },
roleAssignment, membraneRequirements);
return new Webkey(this.db, result.token, result.id, this.grain);
}
}
const commonViewInfo = {
permissions: [{ name: "one" }, { name: "two" }, { name: "three" }],
roles: [{ permissions: [true, true, true] }, // 0
{ permissions: [true, false, false], default: true }, // 1
{ permissions: [false, false, true] }, // 2
{ permissions: [false, false, false] }, // 3
{ permissions: [true, true, false] }, // 4
{ permissions: [true, false, true] }, // 5
{ permissions: [false, true, true] }, // 6
],
};
Tinytest.add("permissions: legacy public grain", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo, true);
test.isTrue(alice.mayOpenGrain(grain));
test.isTrue(bob.mayOpenGrain(grain));
// anonymous
test.isTrue(
SandstormPermissions.mayOpenGrain(globalDb, {
grain: {
_id: grain.id,
accountId: null,
},
})
);
test.equal(alice.grainPermissions(grain), [true, true, true]);
test.equal(bob.grainPermissions(grain), [true, false, false]);
// anonymous
test.equal(SandstormPermissions.grainPermissions(globalDb,
{ grain: { _id: grain.id,
accountId: null, }, },
commonViewInfo).permissions,
[true, false, false]);
});
Tinytest.add("permissions: only owner may open private non-shared grain", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, {});
test.isTrue(alice.mayOpenGrain(grain));
test.isFalse(bob.mayOpenGrain(grain));
test.isFalse(carol.mayOpenGrain(grain));
});
Tinytest.add("permissions: owner gets all permissions", function (test) {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
test.equal(alice.grainPermissions(grain), [true, true, true]);
});
Tinytest.add("permissions: default role", function (test) {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const webkey = alice.shareToWebkey(grain, { none: null }, []);
test.isTrue(webkey.mayOpenGrain());
test.equal(webkey.grainPermissions(), [true, false, false]);
});
Tinytest.add("permissions: parentToken", function (test) {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const parent = alice.shareToWebkey(grain, { allAccess: null });
test.isTrue(parent.mayOpenGrain());
test.equal(parent.grainPermissions(), [true, true, true]);
const child = parent.shareToWebkey({ roleId: 2 });
test.isTrue(child.mayOpenGrain());
test.equal(child.grainPermissions(), [false, false, true]);
globalDb.collections.apiTokens.update(parent.hashedToken, { $set: { revoked: true } });
test.isFalse(parent.mayOpenGrain());
test.isFalse(child.mayOpenGrain());
});
Tinytest.add("permissions: merge user permissions", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const parent1 = alice.shareToWebkey(grain, { allAccess: null });
parent1.shareToAccount(bob, { roleId: 1 });
const parent2 = alice.shareToWebkey(grain, { allAccess: null });
parent2.shareToAccount(bob, { roleId: 2 });
test.isTrue(bob.mayOpenGrain(grain));
test.equal(bob.grainPermissions(grain), [true, false, true]);
});
Tinytest.add("permissions: membrane requirements", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, false],
},
};
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
test.isFalse(webkey.mayOpenGrain());
const result = bob.shareToAccount(bobGrain, carol, { roleId: 1 });
test.isTrue(carol.mayOpenGrain(bobGrain));
test.isTrue(webkey.mayOpenGrain());
globalDb.collections.apiTokens.update(result.id, { $set: { revoked: true } });
test.isFalse(webkey.mayOpenGrain());
const requirement1 = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: alice.id,
permissions: [true, false, false],
},
};
bob.shareToAccount(bobGrain, carol, { roleId: 1 }, [requirement1]);
test.isFalse(webkey.mayOpenGrain());
bob.shareToAccount(bobGrain, alice, { roleId: 1 });
test.isTrue(webkey.mayOpenGrain());
});
Tinytest.add("permissions: membrane requirements sequence", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const parentRequirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, false],
},
};
const parent = alice.shareToWebkey(aliceGrain, { allAccess: null }, [parentRequirement]);
const childRequirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, true],
},
};
const child = parent.shareToWebkey({ allAccess: null }, [childRequirement]);
test.isFalse(child.mayOpenGrain());
bob.shareToAccount(bobGrain, carol, { roleId: 1 });
test.isFalse(child.mayOpenGrain());
bob.shareToAccount(bobGrain, carol, { roleId: 2 });
test.isTrue(child.mayOpenGrain());
});
Tinytest.add("permissions: membrane requirements loop", function (test) {
// Create two tokens with membrane requirements that depend on each other.
// A naive permissions computation could get into a loop here.
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement1 = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: alice.id,
permissions: [],
},
};
alice.shareToAccount(aliceGrain, bob, { allAccess: null }, [requirement1]);
const requirement2 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [],
},
};
bob.shareToAccount(bobGrain, alice, { allAccess: null }, [requirement2]);
test.isTrue(alice.mayOpenGrain(aliceGrain));
test.isFalse(bob.mayOpenGrain(aliceGrain));
test.isTrue(bob.mayOpenGrain(bobGrain));
test.isFalse(alice.mayOpenGrain(bobGrain));
test.equal(alice.grainPermissions(bobGrain), null);
test.equal(bob.grainPermissions(aliceGrain), null);
});
Tinytest.add("permissions: membrane requirements nontrivial normalization", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement1 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: carol.id,
permissions: [true, true, true],
},
};
const webkey = alice.shareToWebkey(aliceGrain, { roleId: 2 }, [requirement1]);
test.isFalse(webkey.mayOpenGrain());
const requirement2 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, true, false],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 1 }, [requirement2]);
test.isFalse(webkey.mayOpenGrain());
const requirement3 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, false, true],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 2 }, [requirement3]);
test.isFalse(webkey.mayOpenGrain());
const requirement4 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, true, true],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 4 }, [requirement4]);
test.isFalse(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { roleId: 1 });
test.isFalse(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { roleId: 4 });
test.isFalse(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { allAccess: null });
test.isTrue(webkey.mayOpenGrain());
});
Tinytest.add("permissions: many membrane requirements", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const otherGrains = [];
const NUM_OTHER_GRAINS = 30;
for (let idx = 0; idx < NUM_OTHER_GRAINS; ++idx) {
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: otherGrain.id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grain, bob, { allAccess: null }, [requirement]);
otherGrains.push(otherGrain);
}
test.isFalse(bob.mayOpenGrain(grain));
alice.shareToAccount(otherGrains[0], bob, { allAccess: null });
test.isTrue(bob.mayOpenGrain(grain));
});
Tinytest.add("permissions: membrane requirements long chain", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grains = [];
const NUM_GRAINS = 50;
for (let idx = 0; idx < NUM_GRAINS; ++idx) {
grains.push(new Grain(globalDb, alice, commonViewInfo));
}
// Bob's access to grain[i] is dependent on his access to grain[i+1];
for (let idx = 0; idx < NUM_GRAINS - 1; ++idx) {
const requirement = {
permissionsHeld: {
grainId: grains[idx + 1].id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grains[idx], bob, { allAccess: null }, [requirement]);
}
test.isFalse(bob.mayOpenGrain(grains[0]));
alice.shareToAccount(grains[grains.length - 1], bob, { allAccess: null });
test.isTrue(bob.mayOpenGrain(grains[0]));
test.equal(bob.grainPermissions(grains[0]), [true, true, true]);
});
function createViewInfo(numPermissions) {
const permissionDefs = [];
const roleDefs = [];
for (let ii = 0; ii < numPermissions; ++ii) {
permissionDefs.push({ name: ii.toString() });
const roleDefPermissions = [];
for (let jj = 0; jj < numPermissions; ++jj) {
roleDefPermissions.push(ii !== jj);
}
const roleDef = { permissions: roleDefPermissions };
if (ii == 0) {
roleDef.default = true;
}
roleDefs.push(roleDef);
}
return { permissions: permissionDefs, roles: roleDefs };
}
Tinytest.add("permissions: membrane requirements many permissions", function (test) {
const NUM_PERMISSIONS = 25;
const viewInfo = createViewInfo(NUM_PERMISSIONS);
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain0 = new Grain(globalDb, alice, viewInfo);
const grain1 = new Grain(globalDb, alice, viewInfo);
const requirementPermissions = [];
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
requirementPermissions.push(idx % 2 == 0);
}
const requirement = {
permissionsHeld: {
grainId: grain1.id,
accountId: bob.id,
permissions: requirementPermissions,
},
};
alice.shareToAccount(grain0, bob, { allAccess: null }, [requirement]);
test.isFalse(bob.mayOpenGrain(grain0));
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
alice.shareToAccount(grain1, bob, { roleId: idx });
}
test.isTrue(bob.mayOpenGrain(grain0));
});
Tinytest.add("permissions: blow up disjunctive normal form", function (test) {
// In a previous version of our permissions computation, the time this test took to complete
// was at least exponential in `NUM_PERMISSIONS`, and effectively took forever if
// `NUM_PERMISSIONS` was greater than 10.
const NUM_PERMISSIONS = 30;
const viewInfo = createViewInfo(NUM_PERMISSIONS);
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain1 = new Grain(globalDb, alice, viewInfo);
const grain2 = new Grain(globalDb, alice, commonViewInfo);
const allPermissions = new Array(NUM_PERMISSIONS);
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
allPermissions[idx] = true;
}
const requirement = {
permissionsHeld: {
grainId: grain1.id,
accountId: bob.id,
permissions: allPermissions,
},
};
alice.shareToAccount(grain2, bob, { allAccess: null }, [requirement]);
test.isFalse(bob.mayOpenGrain(grain1));
test.isFalse(bob.mayOpenGrain(grain2));
const otherGrains = [];
const NUM_OTHER_GRAINS = NUM_PERMISSIONS; // Also equals number of roles.
for (let idx = 0; idx < NUM_OTHER_GRAINS; ++idx) {
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: otherGrain.id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grain1, bob, { roleId: idx }, [requirement]);
otherGrains.push(otherGrain);
}
test.isFalse(bob.mayOpenGrain(grain1));
test.isFalse(bob.mayOpenGrain(grain2));
alice.shareToAccount(otherGrains[0], bob, { allAccess: null });
test.isTrue(bob.mayOpenGrain(grain1));
test.isFalse(bob.mayOpenGrain(grain2));
alice.shareToAccount(otherGrains[otherGrains.length - 1], bob, { allAccess: null });
test.isTrue(bob.mayOpenGrain(grain1));
test.isTrue(bob.mayOpenGrain(grain2));
});
Tinytest.add("permissions: userIsAdmin requirements", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement = { userIsAdmin: alice.id };
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
test.isFalse(webkey.mayOpenGrain());
test.isFalse(!!webkey.grainPermissions());
Meteor.users.update({ _id: alice.id }, { $set: { isAdmin: true } });
test.isTrue(webkey.mayOpenGrain());
test.equal(webkey.grainPermissions(), [true, true, true]);
const childWebkey = webkey.shareToWebkey({ allAccess: null }, [{ userIsAdmin: bob.id }]);
test.isFalse(childWebkey.mayOpenGrain());
test.isFalse(!!childWebkey.grainPermissions());
Meteor.users.update({ _id: bob.id }, { $set: { isAdmin: true } });
test.isTrue(childWebkey.mayOpenGrain());
test.equal(childWebkey.grainPermissions(), [true, true, true]);
});
Tinytest.add("permissions: tokenValid requirements", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const tokenId = Crypto.randomBytes(20).toString("base64");
const requirement = { tokenValid: tokenId };
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
test.isFalse(webkey.mayOpenGrain());
test.isFalse(!!webkey.grainPermissions());
globalDb.collections.apiTokens.insert({ _id: tokenId });
test.isTrue(webkey.mayOpenGrain());
test.equal(webkey.grainPermissions(), [true, true, true]);
const childTokenId = Crypto.randomBytes(20).toString("base64");
globalDb.collections.apiTokens.insert({
_id: childTokenId,
parentToken: tokenId,
requirements: [{
permissionsHeld: {
accountId: bob.id,
grainId: aliceGrain.id,
permissions: [],
},
},
],
});
const webkey2 = alice.shareToWebkey(aliceGrain, { allAccess: null },
[{ tokenValid: childTokenId }]);
test.isFalse(webkey2.mayOpenGrain());
test.isFalse(!!webkey2.grainPermissions());
alice.shareToAccount(aliceGrain, bob, { allAccess: null });
test.isTrue(webkey2.mayOpenGrain());
test.equal(webkey2.grainPermissions(), [true, true, true]);
globalDb.collections.apiTokens.remove({ _id: tokenId });
test.isFalse(webkey2.mayOpenGrain());
test.isFalse(!!webkey2.grainPermissions());
});
Tinytest.add("permissions: collections app basic requirements", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const collectionGrain = new Grain(globalDb, alice, commonViewInfo);
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
alice.shareToAccount(collectionGrain, bob, { allAccess: null });
test.isTrue(bob.mayOpenGrain(collectionGrain));
test.isFalse(bob.mayOpenGrain(otherGrain));
const webkey = alice.shareToWebkey(otherGrain, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
accountId: alice.id,
grainId: collectionGrain.id,
},
},
]
);
test.isTrue(bob.mayOpenGrain(collectionGrain));
test.isFalse(bob.mayOpenGrain(otherGrain));
webkey.shareToAccount(bob, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
accountId: bob.id,
grainId: collectionGrain.id,
},
},
]
);
test.isTrue(bob.mayOpenGrain(collectionGrain));
test.isTrue(bob.mayOpenGrain(otherGrain));
});
Tinytest.add("permissions: permissionsHeld with tokenId", function (test) {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const webkey = alice.shareToWebkey(grain, { allAccess: null });
test.isTrue(webkey.mayOpenGrain(grain));
alice.shareToAccount(grain, bob, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
tokenId: webkey.hashedToken,
grainId: grain.id,
},
},
]
);
test.isTrue(bob.mayOpenGrain(grain));
globalDb.collections.apiTokens.update(webkey.hashedToken, { $set: { revoked: true } });
test.isFalse(bob.mayOpenGrain(grain));
});
@@ -22,7 +22,7 @@ import { _ } from "meteor/underscore";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
SandstormPermissions = {};
export const SandstormPermissions = {};
class PermissionSet {
// A wrapper around an array of booleans representing a set of permissions like "read" or
@@ -0,0 +1,812 @@
// Sandstorm - Personal Cloud Sandbox
// Copyright (c) 2015 Sandstorm Development Group, Inc. and contributors
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-env mocha */
import Crypto from "crypto";
import { Meteor } from "meteor/meteor";
import { Match, check } from "meteor/check";
import { Accounts } from "meteor/accounts-base";
import chai from "chai";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
// We import profile.js for the side-effect of defining more methods on SandstormDb.
// TODO(cleanup): Avoid adding methods to an object from another module like this; ew.
import {} from "/imports/sandstorm-db/profile.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
const globalDb = new SandstormDb();
// TODO(cleanup): Use a lightweight fake (minimongo-based?) database here and construct a clean
// instance at the start of each test case.
const localizedTextPattern = {
defaultText: String,
localizations: Match.Optional([{ locale: String, text: String }]),
};
const roleDefPattern = {
title: Match.Optional(localizedTextPattern),
verbPhrase: Match.Optional(localizedTextPattern),
description: Match.Optional(localizedTextPattern),
permissions: [Boolean],
obsolete: Match.Optional(Boolean),
default: Match.Optional(Boolean),
};
const permissionDefPattern = {
name: String,
title: Match.Optional(localizedTextPattern),
description: Match.Optional(localizedTextPattern),
obsolete: Match.Optional(Boolean),
};
const viewInfoPattern = {
permissions: Match.Optional([permissionDefPattern]),
roles: Match.Optional([roleDefPattern]),
deniedPermissions: Match.Optional([Boolean]),
matchRequests: Match.Optional(Object), // TODO
matchOffers: Match.Optional(Object), // TODO
};
class Grain {
constructor(db, account, viewInfo, isPublic) {
check(db, SandstormDb);
check(account, Account);
check(viewInfo, viewInfoPattern);
this.viewInfo = viewInfo;
this.id = Crypto.randomBytes(10).toString("hex");
db.collections.grains.insert({
_id: this.id,
packageId: "mock-package-id",
appId: "mock-app-id",
appVersion: 0,
userId: account.id,
identityId: Crypto.randomBytes(10).toString("hex"),
title: "mock-grain-title",
cachedViewInfo: this.viewInfo,
private: !isPublic,
});
}
}
class Account {
constructor(db, isAdmin) {
check(db, SandstormDb);
check(isAdmin, Boolean);
const name = Crypto.randomBytes(10).toString("hex");
this.db = db;
this.id = Accounts.insertUserDoc(
{ profile: { name: name }, },
{ type: "account", loginCredentials: [], nonloginCredentials: [], isAdmin: isAdmin, });
}
mayOpenGrain(grain) {
check(grain, Grain);
return SandstormPermissions.mayOpenGrain(globalDb, { grain: { _id: grain.id,
accountId: this.id, }, });
}
grainPermissions(grain) {
check(grain, Grain);
return SandstormPermissions.grainPermissions(globalDb,
{ grain: { _id: grain.id,
accountId: this.id, }, },
grain.viewInfo).permissions;
}
_shareTo(grainId, owner, roleAssignment, membraneRequirements) {
return createNewTokenHelper(this.db, grainId, { accountId: this.id },
owner, roleAssignment, membraneRequirements);
}
shareToAccount(grain, recipient, roleAssignment, membraneRequirements) {
check(grain, Grain);
check(recipient, Account);
return this._shareTo(grain.id, { user: { accountId: recipient.id, title: "share" } },
roleAssignment, membraneRequirements);
}
shareToWebkey(grain, roleAssignment, membraneRequirements) {
check(grain, Grain);
const result = this._shareTo(grain.id, { webkey: { forSharing: true } },
roleAssignment, membraneRequirements);
return new Webkey(this.db, result.token, result.id, grain);
}
}
function createNewTokenHelper(db, grainId, provider, owner, roleAssignment, membraneRequirements) {
check(db, SandstormDb);
check(grainId, String);
check(roleAssignment, db.roleAssignmentPattern);
const result = SandstormPermissions.createNewApiToken(db, provider, grainId, "<petname>",
roleAssignment, owner);
if (membraneRequirements) {
membraneRequirements.forEach((requirement) => {
db.collections.apiTokens.update(
{ _id: result.id },
{ $push: { requirements: requirement } });
});
}
return result;
}
class Webkey {
constructor(db, rawToken, hashedToken, grain) {
check(db, SandstormDb);
check(rawToken, String);
check(hashedToken, String);
check(grain, Grain);
this.db = db;
this.rawToken = rawToken;
this.hashedToken = hashedToken;
this.grain = grain;
}
mayOpenGrain() {
return SandstormPermissions.mayOpenGrain(globalDb, { token: { _id: this.hashedToken,
grainId: this.grain.id, }, });
}
grainPermissions() {
return SandstormPermissions.grainPermissions(globalDb,
{ token: { _id: this.hashedToken,
grainId: this.grain.id, }, },
this.grain.viewInfo).permissions;
}
_shareTo(owner, roleAssignment, membraneRequirements) {
return createNewTokenHelper(this.db, this.grain.id, { rawParentToken: this.rawToken }, owner,
roleAssignment, membraneRequirements);
}
shareToAccount(recipient, roleAssignment, membraneRequirements) {
check(recipient, Account);
return this._shareTo({ user: { accountId: recipient.id, title: "share" } },
roleAssignment, membraneRequirements);
}
shareToWebkey(roleAssignment, membraneRequirements) {
const result = this._shareTo({ webkey: { forSharing: true } },
roleAssignment, membraneRequirements);
return new Webkey(this.db, result.token, result.id, this.grain);
}
}
const commonViewInfo = {
permissions: [{ name: "one" }, { name: "two" }, { name: "three" }],
roles: [{ permissions: [true, true, true] }, // 0
{ permissions: [true, false, false], default: true }, // 1
{ permissions: [false, false, true] }, // 2
{ permissions: [false, false, false] }, // 3
{ permissions: [true, true, false] }, // 4
{ permissions: [true, false, true] }, // 5
{ permissions: [false, true, true] }, // 6
],
};
// TODO(cleanup): The names of the tests below scan a bit poorly,
// because mocha expects you to write things like it("Should ...", ...),
// (which is why the function is called 'it'), but the names below are
// a holdover from when we were using Tinytest. We should reword.
describe("permissions", function() {
it("legacy public grain", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo, true);
chai.assert.isOk(alice.mayOpenGrain(grain));
chai.assert.isOk(bob.mayOpenGrain(grain));
// anonymous
chai.assert.isOk(
SandstormPermissions.mayOpenGrain(globalDb, {
grain: {
_id: grain.id,
accountId: null,
},
})
);
chai.assert.deepEqual(alice.grainPermissions(grain), [true, true, true]);
chai.assert.deepEqual(bob.grainPermissions(grain), [true, false, false]);
// anonymous
chai.assert.deepEqual(
SandstormPermissions.grainPermissions(
globalDb,
{ grain: { _id: grain.id,
accountId: null, }, },
commonViewInfo
).permissions,
[true, false, false]);
});
it("only owner may open private non-shared grain", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, {});
chai.assert.isOk(alice.mayOpenGrain(grain));
chai.assert.isNotOk(bob.mayOpenGrain(grain));
chai.assert.isNotOk(carol.mayOpenGrain(grain));
});
it("owner gets all permissions", function () {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
chai.assert.deepEqual(alice.grainPermissions(grain), [true, true, true]);
});
it("default role", function () {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const webkey = alice.shareToWebkey(grain, { none: null }, []);
chai.assert.isOk(webkey.mayOpenGrain());
chai.assert.deepEqual(webkey.grainPermissions(), [true, false, false]);
});
it("parentToken", function () {
const alice = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const parent = alice.shareToWebkey(grain, { allAccess: null });
chai.assert.isOk(parent.mayOpenGrain());
chai.assert.deepEqual(parent.grainPermissions(), [true, true, true]);
const child = parent.shareToWebkey({ roleId: 2 });
chai.assert.isOk(child.mayOpenGrain());
chai.assert.deepEqual(child.grainPermissions(), [false, false, true]);
globalDb.collections.apiTokens.update(parent.hashedToken, { $set: { revoked: true } });
chai.assert.isNotOk(parent.mayOpenGrain());
chai.assert.isNotOk(child.mayOpenGrain());
});
it("merge user permissions", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const parent1 = alice.shareToWebkey(grain, { allAccess: null });
parent1.shareToAccount(bob, { roleId: 1 });
const parent2 = alice.shareToWebkey(grain, { allAccess: null });
parent2.shareToAccount(bob, { roleId: 2 });
chai.assert.isOk(bob.mayOpenGrain(grain));
chai.assert.deepEqual(bob.grainPermissions(grain), [true, false, true]);
});
it("membrane requirements", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, false],
},
};
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
chai.assert.isNotOk(webkey.mayOpenGrain());
const result = bob.shareToAccount(bobGrain, carol, { roleId: 1 });
chai.assert.isOk(carol.mayOpenGrain(bobGrain));
chai.assert.isOk(webkey.mayOpenGrain());
globalDb.collections.apiTokens.update(result.id, { $set: { revoked: true } });
chai.assert.isNotOk(webkey.mayOpenGrain());
const requirement1 = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: alice.id,
permissions: [true, false, false],
},
};
bob.shareToAccount(bobGrain, carol, { roleId: 1 }, [requirement1]);
chai.assert.isNotOk(webkey.mayOpenGrain());
bob.shareToAccount(bobGrain, alice, { roleId: 1 });
chai.assert.isOk(webkey.mayOpenGrain());
});
it("membrane requirements sequence", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const parentRequirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, false],
},
};
const parent = alice.shareToWebkey(aliceGrain, { allAccess: null }, [parentRequirement]);
const childRequirement = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: carol.id,
permissions: [true, false, true],
},
};
const child = parent.shareToWebkey({ allAccess: null }, [childRequirement]);
chai.assert.isNotOk(child.mayOpenGrain());
bob.shareToAccount(bobGrain, carol, { roleId: 1 });
chai.assert.isNotOk(child.mayOpenGrain());
bob.shareToAccount(bobGrain, carol, { roleId: 2 });
chai.assert.isOk(child.mayOpenGrain());
});
it("membrane requirements loop", function () {
// Create two tokens with membrane requirements that depend on each other.
// A naive permissions computation could get into a loop here.
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement1 = {
permissionsHeld: {
grainId: bobGrain.id,
accountId: alice.id,
permissions: [],
},
};
alice.shareToAccount(aliceGrain, bob, { allAccess: null }, [requirement1]);
const requirement2 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [],
},
};
bob.shareToAccount(bobGrain, alice, { allAccess: null }, [requirement2]);
chai.assert.isOk(alice.mayOpenGrain(aliceGrain));
chai.assert.isNotOk(bob.mayOpenGrain(aliceGrain));
chai.assert.isOk(bob.mayOpenGrain(bobGrain));
chai.assert.isNotOk(alice.mayOpenGrain(bobGrain));
chai.assert.deepEqual(alice.grainPermissions(bobGrain), null);
chai.assert.deepEqual(bob.grainPermissions(aliceGrain), null);
});
it("membrane requirements nontrivial normalization", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement1 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: carol.id,
permissions: [true, true, true],
},
};
const webkey = alice.shareToWebkey(aliceGrain, { roleId: 2 }, [requirement1]);
chai.assert.isNotOk(webkey.mayOpenGrain());
const requirement2 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, true, false],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 1 }, [requirement2]);
chai.assert.isNotOk(webkey.mayOpenGrain());
const requirement3 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, false, true],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 2 }, [requirement3]);
chai.assert.isNotOk(webkey.mayOpenGrain());
const requirement4 = {
permissionsHeld: {
grainId: aliceGrain.id,
accountId: bob.id,
permissions: [true, true, true],
},
};
alice.shareToAccount(aliceGrain, carol, { roleId: 4 }, [requirement4]);
chai.assert.isNotOk(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { roleId: 1 });
chai.assert.isNotOk(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { roleId: 4 });
chai.assert.isNotOk(webkey.mayOpenGrain());
alice.shareToAccount(aliceGrain, bob, { allAccess: null });
chai.assert.isOk(webkey.mayOpenGrain());
});
it("many membrane requirements", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const otherGrains = [];
const NUM_OTHER_GRAINS = 30;
for (let idx = 0; idx < NUM_OTHER_GRAINS; ++idx) {
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: otherGrain.id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grain, bob, { allAccess: null }, [requirement]);
otherGrains.push(otherGrain);
}
chai.assert.isNotOk(bob.mayOpenGrain(grain));
alice.shareToAccount(otherGrains[0], bob, { allAccess: null });
chai.assert.isOk(bob.mayOpenGrain(grain));
});
it("membrane requirements long chain", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grains = [];
const NUM_GRAINS = 50;
for (let idx = 0; idx < NUM_GRAINS; ++idx) {
grains.push(new Grain(globalDb, alice, commonViewInfo));
}
// Bob's access to grain[i] is dependent on his access to grain[i+1];
for (let idx = 0; idx < NUM_GRAINS - 1; ++idx) {
const requirement = {
permissionsHeld: {
grainId: grains[idx + 1].id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grains[idx], bob, { allAccess: null }, [requirement]);
}
chai.assert.isNotOk(bob.mayOpenGrain(grains[0]));
alice.shareToAccount(grains[grains.length - 1], bob, { allAccess: null });
chai.assert.isOk(bob.mayOpenGrain(grains[0]));
chai.assert.deepEqual(bob.grainPermissions(grains[0]), [true, true, true]);
});
function createViewInfo(numPermissions) {
const permissionDefs = [];
const roleDefs = [];
for (let ii = 0; ii < numPermissions; ++ii) {
permissionDefs.push({ name: ii.toString() });
const roleDefPermissions = [];
for (let jj = 0; jj < numPermissions; ++jj) {
roleDefPermissions.push(ii !== jj);
}
const roleDef = { permissions: roleDefPermissions };
if (ii == 0) {
roleDef.default = true;
}
roleDefs.push(roleDef);
}
return { permissions: permissionDefs, roles: roleDefs };
}
it("membrane requirements many permissions", function () {
const NUM_PERMISSIONS = 25;
const viewInfo = createViewInfo(NUM_PERMISSIONS);
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain0 = new Grain(globalDb, alice, viewInfo);
const grain1 = new Grain(globalDb, alice, viewInfo);
const requirementPermissions = [];
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
requirementPermissions.push(idx % 2 == 0);
}
const requirement = {
permissionsHeld: {
grainId: grain1.id,
accountId: bob.id,
permissions: requirementPermissions,
},
};
alice.shareToAccount(grain0, bob, { allAccess: null }, [requirement]);
chai.assert.isNotOk(bob.mayOpenGrain(grain0));
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
alice.shareToAccount(grain1, bob, { roleId: idx });
}
chai.assert.isOk(bob.mayOpenGrain(grain0));
});
it("blow up disjunctive normal form", function () {
// In a previous version of our permissions computation, the time this test took to complete
// was at least exponential in `NUM_PERMISSIONS`, and effectively took forever if
// `NUM_PERMISSIONS` was greater than 10.
const NUM_PERMISSIONS = 30;
const viewInfo = createViewInfo(NUM_PERMISSIONS);
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain1 = new Grain(globalDb, alice, viewInfo);
const grain2 = new Grain(globalDb, alice, commonViewInfo);
const allPermissions = new Array(NUM_PERMISSIONS);
for (let idx = 0; idx < NUM_PERMISSIONS; ++idx) {
allPermissions[idx] = true;
}
const requirement = {
permissionsHeld: {
grainId: grain1.id,
accountId: bob.id,
permissions: allPermissions,
},
};
alice.shareToAccount(grain2, bob, { allAccess: null }, [requirement]);
chai.assert.isNotOk(bob.mayOpenGrain(grain1));
chai.assert.isNotOk(bob.mayOpenGrain(grain2));
const otherGrains = [];
const NUM_OTHER_GRAINS = NUM_PERMISSIONS; // Also equals number of roles.
for (let idx = 0; idx < NUM_OTHER_GRAINS; ++idx) {
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
const requirement = {
permissionsHeld: {
grainId: otherGrain.id,
accountId: bob.id,
permissions: [],
},
};
alice.shareToAccount(grain1, bob, { roleId: idx }, [requirement]);
otherGrains.push(otherGrain);
}
chai.assert.isNotOk(bob.mayOpenGrain(grain1));
chai.assert.isNotOk(bob.mayOpenGrain(grain2));
alice.shareToAccount(otherGrains[0], bob, { allAccess: null });
chai.assert.isOk(bob.mayOpenGrain(grain1));
chai.assert.isNotOk(bob.mayOpenGrain(grain2));
alice.shareToAccount(otherGrains[otherGrains.length - 1], bob, { allAccess: null });
chai.assert.isOk(bob.mayOpenGrain(grain1));
chai.assert.isOk(bob.mayOpenGrain(grain2));
});
it("userIsAdmin requirements", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const carol = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const requirement = { userIsAdmin: alice.id };
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
chai.assert.isNotOk(webkey.mayOpenGrain());
chai.assert.isNotOk(!!webkey.grainPermissions());
Meteor.users.update({ _id: alice.id }, { $set: { isAdmin: true } });
chai.assert.isOk(webkey.mayOpenGrain());
chai.assert.deepEqual(webkey.grainPermissions(), [true, true, true]);
const childWebkey = webkey.shareToWebkey({ allAccess: null }, [{ userIsAdmin: bob.id }]);
chai.assert.isNotOk(childWebkey.mayOpenGrain());
chai.assert.isNotOk(!!childWebkey.grainPermissions());
Meteor.users.update({ _id: bob.id }, { $set: { isAdmin: true } });
chai.assert.isOk(childWebkey.mayOpenGrain());
chai.assert.deepEqual(childWebkey.grainPermissions(), [true, true, true]);
});
it("tokenValid requirements", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const aliceGrain = new Grain(globalDb, alice, commonViewInfo);
const bobGrain = new Grain(globalDb, bob, commonViewInfo);
const tokenId = Crypto.randomBytes(20).toString("base64");
const requirement = { tokenValid: tokenId };
const webkey = alice.shareToWebkey(aliceGrain, { allAccess: null }, [requirement]);
chai.assert.isNotOk(webkey.mayOpenGrain());
chai.assert.isNotOk(!!webkey.grainPermissions());
globalDb.collections.apiTokens.insert({ _id: tokenId });
chai.assert.isOk(webkey.mayOpenGrain());
chai.assert.deepEqual(webkey.grainPermissions(), [true, true, true]);
const childTokenId = Crypto.randomBytes(20).toString("base64");
globalDb.collections.apiTokens.insert({
_id: childTokenId,
parentToken: tokenId,
requirements: [{
permissionsHeld: {
accountId: bob.id,
grainId: aliceGrain.id,
permissions: [],
},
},
],
});
const webkey2 = alice.shareToWebkey(aliceGrain, { allAccess: null },
[{ tokenValid: childTokenId }]);
chai.assert.isNotOk(webkey2.mayOpenGrain());
chai.assert.isNotOk(!!webkey2.grainPermissions());
alice.shareToAccount(aliceGrain, bob, { allAccess: null });
chai.assert.isOk(webkey2.mayOpenGrain());
chai.assert.deepEqual(webkey2.grainPermissions(), [true, true, true]);
globalDb.collections.apiTokens.remove({ _id: tokenId });
chai.assert.isNotOk(webkey2.mayOpenGrain());
chai.assert.isNotOk(!!webkey2.grainPermissions());
});
it("collections app basic requirements", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const collectionGrain = new Grain(globalDb, alice, commonViewInfo);
const otherGrain = new Grain(globalDb, alice, commonViewInfo);
alice.shareToAccount(collectionGrain, bob, { allAccess: null });
chai.assert.isOk(bob.mayOpenGrain(collectionGrain));
chai.assert.isNotOk(bob.mayOpenGrain(otherGrain));
const webkey = alice.shareToWebkey(otherGrain, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
accountId: alice.id,
grainId: collectionGrain.id,
},
},
]
);
chai.assert.isOk(bob.mayOpenGrain(collectionGrain));
chai.assert.isNotOk(bob.mayOpenGrain(otherGrain));
webkey.shareToAccount(bob, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
accountId: bob.id,
grainId: collectionGrain.id,
},
},
]
);
chai.assert.isOk(bob.mayOpenGrain(collectionGrain));
chai.assert.isOk(bob.mayOpenGrain(otherGrain));
});
it("permissionsHeld with tokenId", function () {
const alice = new Account(globalDb, false);
const bob = new Account(globalDb, false);
const grain = new Grain(globalDb, alice, commonViewInfo);
const webkey = alice.shareToWebkey(grain, { allAccess: null });
chai.assert.isOk(webkey.mayOpenGrain(grain));
alice.shareToAccount(grain, bob, { allAccess: null },
[
{
permissionsHeld: {
permissions: [],
tokenId: webkey.hashedToken,
grainId: grain.id,
},
},
]
);
chai.assert.isOk(bob.mayOpenGrain(grain));
globalDb.collections.apiTokens.update(webkey.hashedToken, { $set: { revoked: true } });
chai.assert.isNotOk(bob.mayOpenGrain(grain));
});
});
@@ -21,6 +21,7 @@ import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
import { _ } from "meteor/underscore";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
import Capnp from "/imports/server/capnp.js";
import { waitPromise } from '../server/async-helpers.js';
+1
View File
@@ -19,6 +19,7 @@ import { HTTP } from "meteor/http";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
import { globalDb } from "/imports/db-deprecated.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
import { FrontendRefRegistry } from "/imports/server/frontend-ref.js";
import { PersistentImpl } from "/imports/server/persistent.js";
import { migrateToLatest } from "/imports/server/migrations.js";
+1
View File
@@ -26,6 +26,7 @@ import { Random } from "meteor/random";
import Capnp from "/imports/server/capnp.js";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
import { globalDb } from "/imports/db-deprecated.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
import { makeHackSessionContext } from "/imports/server/hack-session.js";
+1
View File
@@ -25,6 +25,7 @@ import { send as sendEmail } from "/imports/server/email.js";
import { inMeteor, waitPromise } from "/imports/server/async-helpers.js";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
import { globalDb } from "/imports/db-deprecated.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
const ROOT_URL = process.env.ROOT_URL;
+1
View File
@@ -20,6 +20,7 @@ import { _ } from "meteor/underscore";
import { SandstormDb } from "/imports/sandstorm-db/db.js";
import { globalDb } from "/imports/db-deprecated.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
import { PersistentImpl } from "/imports/server/persistent.js";
import { StaticAssetImpl, IdenticonStaticAssetImpl } from "/imports/server/static-asset.js";
import Capnp from "/imports/server/capnp.js";
+1
View File
@@ -22,6 +22,7 @@ import { Random } from "meteor/random";
import Crypto from "crypto";
import { inMeteor } from "/imports/server/async-helpers.js";
import Capnp from "/imports/server/capnp.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
const privateDb = Symbol("PersistentImpl.db");
const privateTemplate = Symbol("PersistentImpl.template");
+1
View File
@@ -18,6 +18,7 @@
import { Meteor } from "meteor/meteor";
import { globalDb } from "/imports/db-deprecated.js";
import { SandstormPermissions } from "/imports/sandstorm-permissions/permissions.js";
const ROOT_URL = process.env.ROOT_URL;
+53
View File
@@ -287,6 +287,12 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -416,6 +422,20 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"requires": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -433,6 +453,12 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -525,6 +551,15 @@
"ms": "2.0.0"
}
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -850,6 +885,12 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
@@ -1479,6 +1520,12 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
@@ -1848,6 +1895,12 @@
"prelude-ls": "~1.1.2"
}
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+1
View File
@@ -37,6 +37,7 @@
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"chai": "^4.2.0",
"eslint": "^6.8.0"
}
}
+6 -12
View File
@@ -1,14 +1,9 @@
#!/bin/bash
# Runs `meteor test-packages`. The tests run on a fresh isolated Mongo instance.
# Runs `meteor test`. The tests run on a fresh isolated Mongo instance.
set -euo pipefail
if ! $(which "spacejam" >/dev/null 2>/dev/null) ; then
echo "Couldn't find executable 'spacejam' - try installing the package from npm with 'sudo -H npm install -g spacejam'?"
exit 1
fi
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$SCRIPT_DIR"
@@ -56,12 +51,11 @@ __EOF__
# over its own bundled version, and the system gyp doesn't work.
export PYTHONPATH=$("$SCRIPT_DIR/../find-meteor-dev-bundle.sh")/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib
# To test interactively through a browser:
#meteor test-packages --settings $SETTINGS ./packages/sandstorm-permissions
# Terrible hack: Make capnp.node available to tests running under spacejam.
# Terrible hack: Make capnp.node available to tests.
rm -f /tmp/node_modules
ln -s "$(dirname "$PWD")/node_modules" /tmp/node_modules
# To test on the command line:
exec spacejam test-packages --settings $SETTINGS ./packages/sandstorm-permissions
meteor test \
--once \
--driver-package=meteortesting:mocha \
--settings "$SETTINGS"