Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a2a7ec9
Remove unused grainId parameter from ExternalWebSession.
kentonv Feb 14, 2017
23e7fb5
Catch exceptions properly in ExternalWebSession.
kentonv Feb 16, 2017
b406da6
cleanup: Move builtin powerbox card templates to their own files.
kentonv Feb 16, 2017
3a6617f
Add some TODOs to ApiSession.PowerboxTag.
kentonv Feb 16, 2017
ad7c1e7
Add api-session-impl.capnp to define PersistentApiSession.
kentonv Feb 16, 2017
7c1993d
Define external HTTP frontendRef schema.
kentonv Feb 16, 2017
23603ef
Support simple outgoing HTTP powerbox options.
kentonv Feb 16, 2017
2c952c0
Implement OAuth-authenticated HTTP powerbox options.
kentonv Feb 16, 2017
bc7525f
Fix broken path handling in ExternalWebSession.
kentonv Feb 16, 2017
6992b5b
Add some TODOs to ExternalWebSession.
kentonv Feb 16, 2017
9df7488
cleanup: Fix weird use of undeclared dependency on ServiceConfigurati…
kentonv Feb 16, 2017
c80c7c3
jscs --fix
kentonv Feb 16, 2017
2a7458d
Address @dwrensha's review comments.
kentonv Feb 16, 2017
aa25051
Add comment about revoking OAuth tokens when deleting ApiToken records.
kentonv Feb 16, 2017
3b66bc8
Document how we encrypt credentials in ApiTokens.
kentonv Feb 17, 2017
0537541
Add helpers to read and write ApiTokens with encryption applied.
kentonv Feb 17, 2017
69f9f8c
Use new fetchApiToken/insertApiToken helpers to implement encrypted c…
kentonv Feb 17, 2017
33cdbb9
Pad short secrets to 32 bytes, per the documentation of http.auth.bas…
kentonv Feb 17, 2017
9b4df64
jscs --fix
kentonv Feb 17, 2017
68d83da
Fix bugs caught by tests.
kentonv Feb 17, 2017
7658292
typo
kentonv Feb 17, 2017
60d3b87
Typos spotted by @ocdtrekkie
kentonv Feb 27, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions shell/.meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ [email protected]
[email protected]

[email protected]
service-configuration
google
github
1 change: 0 additions & 1 deletion shell/client/accounts/login-buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import AccountsUi from "/imports/client/accounts/accounts-ui.js";

// for convenience
const loginButtonsSession = Accounts._loginButtonsSession;
const ServiceConfiguration = Package["service-configuration"].ServiceConfiguration;

const isDemoUserHelper = function () {
return this._db.isDemoUser();
Expand Down
4 changes: 2 additions & 2 deletions shell/client/admin/identity-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Template.googleLoginSetupInstructions.helpers({

// Google form.
Template.adminIdentityProviderConfigureGoogle.onCreated(function () {
const configurations = Package["service-configuration"].ServiceConfiguration.configurations;
const configurations = ServiceConfiguration.configurations;
const googleConfiguration = configurations.findOne({ service: "google" });
const clientId = (googleConfiguration && googleConfiguration.clientId) || "";
const clientSecret = (googleConfiguration && googleConfiguration.secret) || "";
Expand Down Expand Up @@ -283,7 +283,7 @@ Template.githubLoginSetupInstructions.helpers({

// GitHub form.
Template.adminIdentityProviderConfigureGitHub.onCreated(function () {
const configurations = Package["service-configuration"].ServiceConfiguration.configurations;
const configurations = ServiceConfiguration.configurations;
const githubConfiguration = configurations.findOne({ service: "github" });
const clientId = (githubConfiguration && githubConfiguration.clientId) || "";
const clientSecret = (githubConfiguration && githubConfiguration.secret) || "";
Expand Down
95 changes: 95 additions & 0 deletions shell/client/powerbox-builtins.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template name="grainPowerboxCard">
{{ grainInfo.title }}
</template>

<template name="uiViewPowerboxConfiguration">
{{#if choseHostedObject}}
<div class="powerbox-iframe-mount">{{ setupIframe }}</div>
{{else}}
<form>
<p>with permissions:</p>
{{#each role in viewInfo.roles}}
<label><input type="radio" name="role" value="{{role.index}}">{{role.title.defaultText}} - {{role.verbPhrase.defaultText}}</label>
{{else}}
<label><input type="radio" checked="true" name="role" value="all">Full access</label>
{{/each}}
<div class="choose-buttons">
<button class="connect-button">Connect</button>
{{#if option.hostedObject}}
<button class="choose-hosted-object">Browse grain contents &raquo;</button>
{{/if}}
</div>
</form>
{{/if}}
</template>

<template name="ipNetworkPowerboxCard">
Admin: grant all outgoing network access
{{#with encryption}}
(encrypted with {{.}})
{{/with}}
</template>

<template name="ipInterfacePowerboxCard">
Admin: grant all incoming network access
</template>

<template name="emailVerifierPowerboxCard">
{{#if option.frontendRef.emailVerifier.services}}
Verify e-mail addresses using {{ serviceTitle }}
{{else}}
Verify e-mail addresses using any login service
{{/if}}
</template>

<template name="verifiedEmailPowerboxCard">
{{ option.frontendRef.verifiedEmail.address }}
</template>

<template name="identityPowerboxCard">
{{ option.profile.name }}
</template>

<template name="identityPowerboxConfiguration">
<form>
<p>with permissions:</p>
{{#each role in sufficientRoles}}
<label><input type="radio" name="role" value="{{role.index}}">{{role.title.defaultText}} - {{role.verbPhrase.defaultText}}</label>
{{else}}
<label><input type="radio" checked="true" name="role" value="all">Full access</label>
{{/each}}
<div class="choose-buttons">
<button class="connect-button">Connect</button>
</div>
</form>
</template>

<template name="addNewVerifiedEmailPowerboxCard">
Add a new address
</template>

<template name="httpUrlPowerboxCard">
Use {{option.frontendRef.http.url}}
</template>

<template name="httpArbitraryPowerboxCard">
Specify URL...
</template>

<template name="httpArbitraryPowerboxConfiguration">
<form>
Enter URL:
<input class="url" type="text" placeholder="https://example.com">
<div class="choose-buttons">
<button class="connect-button">Connect</button>
</div>
</form>
</template>

<template name="httpOAuthPowerboxCard">
Use {{option.oauthServiceInfo.service}}
</template>

<template name="httpOAuthPowerboxConfiguration">
Requesting permissions from {{option.oauthServiceInfo.service}} (see popup).
</template>
262 changes: 262 additions & 0 deletions shell/client/powerbox-builtins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Sandstorm - Personal Cloud Sandbox
// Copyright (c) 2017 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.

const prepareViewInfoForDisplay = function (viewInfo) {
const result = _.clone(viewInfo || {});
if (result.permissions) indexElements(result.permissions);
// It's essential that we index the roles *before* hiding obsolete roles,
// or else we'll produce the incorrect roleAssignment for roles that are
// described after obsolete roles in the pkgdef.
if (result.roles) {
indexElements(result.roles);
result.roles = removeObsolete(result.roles);
}

return result;
};

const indexElements = function (arr) {
// Helper function to annotate an array of objects with their indices
for (let i = 0; i < arr.length; i++) {
arr[i].index = i;
}
};

const removeObsolete = function (arr) {
// remove entries from the list that are flagged as obsolete
return _.filter(arr, function (el) {
return !el.obsolete;
});
};

Template.ipNetworkPowerboxCard.helpers({
encryption: function () {
const encryption = this.option.frontendRef.ipNetwork.encryption || {};
if ("tls" in encryption) {
return "TLS";
} else {
return null;
}
},
});

Template.grainPowerboxCard.powerboxIconSrc = card => {
return card.grainInfo.iconSrc;
};

Template.uiViewPowerboxConfiguration.onCreated(function () {
// this.data is a card; see filteredCardData()

this._choseHostedObject = new ReactiveVar(false);

this._viewInfo = new ReactiveVar({});

// Fetch the view info for the grain.
if (this.data.grainInfo.cachedViewInfo) {
this._viewInfo.set(prepareViewInfoForDisplay(this.data.grainInfo.cachedViewInfo));
} else if (this.data.grainInfo.apiTokenId) {
Meteor.call("getViewInfoForApiToken", this.data.grainInfo.apiTokenId, (err, result) => {
if (err) {
console.log(err);
this.data.powerboxRequest.failRequest(err);
} else {
this._viewInfo.set(prepareViewInfoForDisplay(result));
}
});
}
});

Template.uiViewPowerboxConfiguration.helpers({
choseHostedObject: function () {
return !this.option.uiView || Template.instance()._choseHostedObject.get();
},

viewInfo: function () {
return Template.instance()._viewInfo.get();
},

setupIframe: function () {
// HACK: A GrainView iframe has to be managed outside of the usual Blaze template flow and
// reactive contexts. We manually attach the iframe as a child of the "powerbox-iframe-mount"
// div and hope that that div doesn't get re-rendered unexpectedly.
// TODO(cleanup): This is terrible but what else can we do?
const tmpl = Template.instance();
Meteor.defer(() => {
if (!tmpl._grainView) {
const mount = tmpl.find(".powerbox-iframe-mount");
const powerboxRequest = {
descriptors: this.powerboxRequest.getQuery(),
requestingSession: this.powerboxRequest.getSessionId(),
};
tmpl._grainView = new this.powerboxRequest.GrainView(
null, this.db, this.option.grainId, "", null, mount, { powerboxRequest });
tmpl._grainView.setActive(true);
tmpl._grainView.openSession();

this.powerboxRequest.onFinalize(() => {
tmpl._grainView.destroy();
});

tmpl.autorun(() => {
const fulfilledInfo = tmpl._grainView.fulfilledInfo();
if (fulfilledInfo) {
this.powerboxRequest.completeRequest(fulfilledInfo.token, fulfilledInfo.descriptor);
}
});
}
});
},
});

Template.uiViewPowerboxConfiguration.events({
"click .connect-button": function (event) {
event.preventDefault();
const selectedInput = Template.instance().find('form input[name="role"]:checked');
if (selectedInput) {
let roleAssignment;
if (selectedInput.value === "all") {
roleAssignment = { allAccess: null };
} else {
const role = parseInt(selectedInput.value, 10);
roleAssignment = { roleId: role };
}

this.powerboxRequest.completeUiView(this.option.grainId, roleAssignment);
}
},

"click .choose-hosted-object": function (event, tmpl) {
event.preventDefault();
tmpl._choseHostedObject.set(true);
},
});

const isSubsetOf = function (p1, p2) {
for (let idx = 0; idx < p1.length; ++idx) {
if (p1[idx] && !p2[idx]) {
return false;
}
}

return true;
};

Template.identityPowerboxConfiguration.helpers({
sufficientRoles: function () {
const requestedPermissions = this.option.requestedPermissions;

const session = this.db.collections.sessions.findOne(
{ _id: this.powerboxRequest._requestInfo.sessionId, });
const roles = prepareViewInfoForDisplay(session.viewInfo).roles;

return roles && roles.filter(r => isSubsetOf(requestedPermissions, r.permissions));
},
});

Template.identityPowerboxConfiguration.events({
"click .connect-button": function (event, instance) {
event.preventDefault();
const selectedInput = instance.find('form input[name="role"]:checked');
if (selectedInput) {
let roleAssignment;
if (selectedInput.value === "all") {
roleAssignment = { allAccess: null };
} else {
const role = parseInt(selectedInput.value, 10);
roleAssignment = { roleId: role };
}

this.powerboxRequest.completeNewFrontendRef({
identity: {
id: instance.data.option.frontendRef.identity,
roleAssignment,
},
});
}
},
});

Template.identityPowerboxCard.powerboxIconSrc = card => {
return card.option.profile.pictureUrl;
};

Template.emailVerifierPowerboxCard.helpers({
serviceTitle: function () {
const services = this.option.frontendRef.emailVerifier.services;
const name = services[0];
const service = Accounts.identityServices[name];
if (service.loginTemplate.name === "oauthLoginButton") {
return service.loginTemplate.data.displayName;
} else if (name === "email") {
return "passwordless e-mail login";
} else if (name === "ldap") {
return "LDAP";
} else {
return name;
}
},
});

Template.emailVerifierPowerboxCard.powerboxIconSrc = () => "/email-m.svg";
Template.verifiedEmailPowerboxCard.powerboxIconSrc = () => "/email-m.svg";
Template.addNewVerifiedEmailPowerboxCard.powerboxIconSrc = () => "/add-email-m.svg";

Template.httpUrlPowerboxCard.powerboxIconSrc = () => "/web-m.svg";

Template.httpArbitraryPowerboxCard.powerboxIconSrc = () => "/web-m.svg";
Template.httpArbitraryPowerboxConfiguration.events({
"click .connect-button": function (event, instance) {
event.preventDefault();
const input = instance.find("form>input.url");

this.powerboxRequest.completeNewFrontendRef({
http: {
url: input.value,
auth: { none: null },
},
});
},
});

Template.httpOAuthPowerboxCard.powerboxIconSrc = () => "/web-m.svg";
Template.httpOAuthPowerboxConfiguration.onCreated(function () {
const option = this.data.option;

const serviceMap = { google: Google, github: Github };

const serviceHandler = serviceMap[option.oauthServiceInfo.service];

if (!serviceHandler) {
throw new Error("unknown service: " + option.oauthServiceInfo.service);
}

serviceHandler.requestCredential({
loginStyle: "popup",
requestPermissions: option.oauthScopes.map(scope => scope.name),

// Google-specific options... others should ignore.
forceApprovalPrompt: true,
requestOfflineToken: true,
}, credentialToken => {
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken);
this.data.powerboxRequest.completeNewFrontendRef({
http: {
url: option.httpUrl,
auth: { oauth: { credentialToken, credentialSecret } },
},
});
});
});
Loading