diff --git a/.meteor/packages b/.meteor/packages index 601cd7b32..96b23a76f 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -26,7 +26,6 @@ mongo@1.15.0-rc272.0 mquandalle:collection-mutations # Account system -kenton:accounts-sandstorm #wekan-ldap #wekan-accounts-cas #wekan-accounts-oidc @@ -142,3 +141,4 @@ useraccounts:unstyled service-configuration@1.3.0 communitypackages:picker simple:rest-accounts-password +wekan-accounts-sandstorm diff --git a/.meteor/versions b/.meteor/versions index 5896492cd..d091ebf74 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -76,7 +76,6 @@ jquery@1.11.11 kadira:blaze-layout@2.3.0 kadira:dochead@1.5.0 kadira:flow-router@2.12.1 -kenton:accounts-sandstorm@0.1.0 konecty:mongo-counter@0.0.5_3 launch-screen@1.3.0 livedata@1.0.18 @@ -225,5 +224,6 @@ useraccounts:flow-routing@1.15.0 useraccounts:unstyled@1.14.2 webapp@1.13.1 webapp-hashing@1.1.0 +wekan-accounts-sandstorm@0.7.0 wekan-markdown@1.0.9 zimme:active-route@2.3.2 diff --git a/packages/wekan-accounts-sandstorm/.gitignore b/packages/wekan-accounts-sandstorm/.gitignore new file mode 100644 index 000000000..b39d80246 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.gitignore @@ -0,0 +1,3 @@ +.build* +test-app/.meteor/local +test-app/.meteor-spk diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/.finished-upgraders b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.finished-upgraders new file mode 100644 index 000000000..910574ce2 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.finished-upgraders @@ -0,0 +1,17 @@ +# This file contains information which helps Meteor properly upgrade your +# app when you run 'meteor update'. You should check it into version control +# with your project. + +notices-for-0.9.0 +notices-for-0.9.1 +0.9.4-platform-file +notices-for-facebook-graph-api-2 +1.2.0-standard-minifiers-package +1.2.0-meteor-platform-split +1.2.0-cordova-changes +1.2.0-breaking-changes +1.3.0-split-minifiers-package +1.4.0-remove-old-dev-bundle-link +1.4.1-add-shell-server-package +1.4.3-split-account-service-packages +1.5-add-dynamic-import-package diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/.gitignore b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.gitignore new file mode 100644 index 000000000..408303742 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/.id b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.id new file mode 100644 index 000000000..2b0fce4e8 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +1w4v0yxh077n01wrnl8j diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/packages b/packages/wekan-accounts-sandstorm/.test-app/.meteor/packages new file mode 100644 index 000000000..68bc73b73 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/packages @@ -0,0 +1,31 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +# List accounts-sandstorm first so that any missing dependencies it has +# are discovered. +kenton:accounts-sandstorm + +# Optional dependency. Should still work commented-out. +accounts-base@1.3.1 + +meteor-base@1.1.0 # Packages every Meteor app needs to have +mobile-experience@1.0.4 # Packages for a great mobile UX +mongo@1.1.19 # The database Meteor supports right now +blaze-html-templates # Compile .html files into Meteor Blaze views +session@1.1.7 # Client-side reactive dictionary for your app +jquery@1.11.10 # Helpful client-side library +tracker@1.1.3 # Meteor's client-side reactive programming library + +es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. +ecmascript@0.8.1 # Enable ECMAScript2015+ syntax in app code + +autopublish@1.0.7 # Publish all data to the clients (for prototyping) +insecure@1.0.7 # Allow all DB writes from clients (for prototyping) + +standard-minifier-css +standard-minifier-js +shell-server +dynamic-import diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/platforms b/packages/wekan-accounts-sandstorm/.test-app/.meteor/platforms new file mode 100644 index 000000000..efeba1b50 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/release b/packages/wekan-accounts-sandstorm/.test-app/.meteor/release new file mode 100644 index 000000000..1e7fc5b56 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/release @@ -0,0 +1 @@ +METEOR@1.5.1 diff --git a/packages/wekan-accounts-sandstorm/.test-app/.meteor/versions b/packages/wekan-accounts-sandstorm/.test-app/.meteor/versions new file mode 100644 index 000000000..c72576ee5 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/.meteor/versions @@ -0,0 +1,83 @@ +accounts-base@1.3.1 +allow-deny@1.0.6 +autopublish@1.0.7 +autoupdate@1.3.12 +babel-compiler@6.19.4 +babel-runtime@1.0.1 +base64@1.0.10 +binary-heap@1.0.10 +blaze@2.3.2 +blaze-html-templates@1.1.2 +blaze-tools@1.0.10 +boilerplate-generator@1.1.1 +caching-compiler@1.1.9 +caching-html-compiler@1.1.2 +callback-hook@1.0.10 +check@1.2.5 +ddp@1.3.0 +ddp-client@2.0.0 +ddp-common@1.2.9 +ddp-rate-limiter@1.0.7 +ddp-server@2.0.0 +deps@1.0.12 +diff-sequence@1.0.7 +dynamic-import@0.1.1 +ecmascript@0.8.2 +ecmascript-runtime@0.4.1 +ecmascript-runtime-client@0.4.3 +ecmascript-runtime-server@0.4.1 +ejson@1.0.13 +es5-shim@4.6.15 +fastclick@1.0.13 +geojson-utils@1.0.10 +hot-code-push@1.0.4 +html-tools@1.0.11 +htmljs@1.0.11 +http@1.2.12 +id-map@1.0.9 +insecure@1.0.7 +jquery@1.11.10 +kenton:accounts-sandstorm@0.7.0 +launch-screen@1.1.1 +livedata@1.0.18 +localstorage@1.1.1 +logging@1.1.17 +meteor@1.7.1 +meteor-base@1.1.0 +minifier-css@1.2.16 +minifier-js@2.1.1 +minimongo@1.2.1 +mobile-experience@1.0.4 +mobile-status-bar@1.0.14 +modules@0.9.4 +modules-runtime@0.8.0 +mongo@1.1.22 +mongo-id@1.0.6 +npm-mongo@2.2.30 +observe-sequence@1.0.16 +ordered-dict@1.0.9 +promise@0.8.9 +random@1.0.10 +rate-limit@1.0.8 +reactive-dict@1.1.9 +reactive-var@1.0.11 +reload@1.1.11 +retry@1.0.9 +routepolicy@1.0.12 +service-configuration@1.0.11 +session@1.1.7 +shell-server@0.2.4 +spacebars@1.0.15 +spacebars-compiler@1.1.3 +standard-minifier-css@1.3.4 +standard-minifier-js@2.1.1 +templating@1.3.2 +templating-compiler@1.3.2 +templating-runtime@1.3.2 +templating-tools@1.1.2 +tracker@1.1.3 +ui@1.0.13 +underscore@1.0.10 +url@1.1.0 +webapp@1.3.17 +webapp-hashing@1.0.9 diff --git a/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.css b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.css new file mode 100644 index 000000000..b6b4052b4 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.css @@ -0,0 +1 @@ +/* CSS declarations go here */ diff --git a/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.html b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.html new file mode 100644 index 000000000..c7ce7638b --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.html @@ -0,0 +1,19 @@ + + accounts-meteor-test + + + +

Welcome to Meteor!

+ + {{> hello}} + + + diff --git a/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.js b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.js new file mode 100644 index 000000000..9e5539e45 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/accounts-meteor-test.js @@ -0,0 +1,48 @@ +if (Meteor.isClient) { + var Info = new Mongo.Collection("info"); + var Counter = new Mongo.Collection("counter"); + + Template.hello.onCreated(function () { + Meteor.subscribe("info"); + Meteor.subscribe("counter"); + }); + + Template.hello.helpers({ + counter: function () { + if (!Template.instance().subscriptionsReady()) return "not ready"; + return Counter.findOne("counter").counter; + }, + + serverInfo: function () { + var obj = Info.findOne("info"); + console.log("server", Meteor.loggingIn && Meteor.loggingIn(), obj); + return JSON.stringify(obj, null, 2); + }, + + clientInfo: function () { + var obj = Meteor.sandstormUser(); + console.log("client", Meteor.loggingIn && Meteor.loggingIn(), obj); + return JSON.stringify(obj, null, 2); + }, + }); +} + +if (Meteor.isServer) { + Meteor.startup(function () { + // code to run on server at startup + }); + + Meteor.publish("info", function () { + var user = Meteor.users && this.userId && Meteor.users.findOne(this.userId); + this.added("info", "info", {userId: this.userId, user: user, sandstormUser: this.connection.sandstormUser(), + sessionId: this.connection.sandstormSessionId(), + tabId: this.connection.sandstormTabId()}); + this.ready(); + }); + + var counter = 0; + Meteor.publish("counter", function () { + this.added("counter", "counter", {counter: counter++}); + this.ready(); + }); +} diff --git a/packages/wekan-accounts-sandstorm/.test-app/packages/kenton:accounts-sandstorm b/packages/wekan-accounts-sandstorm/.test-app/packages/kenton:accounts-sandstorm new file mode 120000 index 000000000..c25bddb6d --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/packages/kenton:accounts-sandstorm @@ -0,0 +1 @@ +../.. \ No newline at end of file diff --git a/packages/wekan-accounts-sandstorm/.test-app/sandstorm-pkgdef.capnp b/packages/wekan-accounts-sandstorm/.test-app/sandstorm-pkgdef.capnp new file mode 100644 index 000000000..a2df64a0c --- /dev/null +++ b/packages/wekan-accounts-sandstorm/.test-app/sandstorm-pkgdef.capnp @@ -0,0 +1,74 @@ +@0xb412d6a17c04e5cc; + +using Spk = import "/sandstorm/package.capnp"; + +const pkgdef :Spk.PackageDefinition = ( + id = "y49n7yrxk6p3ud1hkgeup1mah6f7a488nancvay7v6y1wxq78cn0", + + manifest = ( + appTitle = (defaultText = "Meteor Accounts Test App"), + appVersion = 0, + appMarketingVersion = (defaultText = "0.0.0"), + actions = [ + ( title = (defaultText = "New Test"), + command = .myCommand + ) + ], + + continueCommand = .myCommand, + ), + + sourceMap = ( + searchPath = [ + ( sourcePath = ".meteor-spk/deps" ), + ( sourcePath = ".meteor-spk/bundle" ) + ] + ), + + alwaysInclude = [ "." ], + + bridgeConfig = ( + viewInfo = ( + permissions = [ + ( + name = "editor", + title = (defaultText = "editor"), + description = (defaultText = "grants ability to modify data"), + ), + ( + name = "commenter", + title = (defaultText = "commenter"), + description = (defaultText = "grants ability to modify data"), + ), + ], + roles = [ + ( + title = (defaultText = "editor"), + permissions = [true, true], + verbPhrase = (defaultText = "can edit"), + description = (defaultText = "editors may view all site data and change settings."), + ), + ( + title = (defaultText = "commenter"), + permissions = [false, true], + verbPhrase = (defaultText = "can comment"), + description = (defaultText = "viewers may view what other users have written."), + ), + ( + title = (defaultText = "viewer"), + permissions = [false, false], + verbPhrase = (defaultText = "can view"), + description = (defaultText = "viewers may view what other users have written."), + ), + ], + ), + ), +); + +const myCommand :Spk.Manifest.Command = ( + argv = ["/sandstorm-http-bridge", "4000", "--", "node", "start.js"], + environ = [ + (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"), + (key = "SANDSTORM", value = "1"), + ] +); diff --git a/packages/wekan-accounts-sandstorm/LICENSE b/packages/wekan-accounts-sandstorm/LICENSE new file mode 100644 index 000000000..039de6863 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors +Licensed under the MIT License: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/packages/wekan-accounts-sandstorm/README.md b/packages/wekan-accounts-sandstorm/README.md new file mode 100644 index 000000000..17ad0d5e8 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/README.md @@ -0,0 +1,137 @@ +# Sandstorm.io login integration for Meteor.js + +[Sandstorm](https://sandstorm.io) is a platform for personal clouds that makes +installing apps to your personal server as easy as installing apps to your +phone. + +[Meteor](https://meteor.com) is a revolutionary web app framework. Sandstorm's +own UI is built using Meteor, and Meteor is also a great way to build Sandstorm +apps. + +This package is meant to be used by Meteor apps built to run on Sandstorm. +It integrates with Sandstorm's built-in login system to log the user in +automatically when they open the app. The user's `profile.name` will be +populated from Sandstorm. When using this package, you should not use +`accounts-ui` at all; just let login happen automatically. + +## Including in your app + +To use this package in your Meteor project, simply install it from the Meteor +package repository: + + meteor add kenton:accounts-sandstorm + +To package a Meteor app for Sandstorm, see +[the Meteor app packaging guide](https://docs.sandstorm.io/en/latest/vagrant-spk/packaging-tutorial-meteor/). + +Note that this package does nothing if the `SANDSTORM` environment variable is +not set. Therefore, it is safe to include the package even in non-Sandstorm +builds of your app. Note that `sandstorm-pkgdef.capnp` files generated by +`spk init` automatically have a line like `(key = "SANDSTORM", value = "1"),` +which sets the environment variable, so you shouldn't have to do anything +special to enable it. + +Conversely, when `SANDSTORM` is set, this package will enter Highlander Mode +in which it will *disable* all other accounts packages. This makes it safe +to include those other accounts packages in the Sandstorm build, which is +often convenient, although they will add bloat to your spk. + +## Usage + +* On the client, call `Meteor.sandstormUser()`. (This is a reactive data source.) +* In a method or publish (on the server), call `this.connection.sandstormUser()`. + +Either of these will return an object containing the following fields: + +* `id`: From `X-Sandstorm-User-Id`; globally unique and stable + identifier for this user. `null` if the user is not logged in. +* `name`: From "X-Sandstorm-Username`, the user's display name (e.g. + `"Kenton Varda"`). +* `picture`: From `X-Sandstorm-User-Picture`, URL of the user's preferred + avatar, or `null` if they don't have one. +* `permissions`: From `X-Sandstorm-Permissions` (but parsed to a list), + the list of permissions the user has as determined by the Sandstorm + sharing model. Apps can define their own permissions. +* `preferredHandle`: From `X-Sandstorm-Preferred-Handle`, the user's + preferred handle ("username", in the unix sense). This is NOT + guaranteed to be unique; it's just a different form of display name. +* `pronouns`: From `X-Sandstorm-User-Pronouns`, indicates the pronouns + by which the user prefers to be referred. + +See [the Sandstorm docs](https://docs.sandstorm.io/en/latest/developing/auth/#headers-that-an-app-receives) for more information about these fields. + +Note that `sandstormUser()` may return `null` on the client side if the login +handshake has not completed yet (`Meteor.loggingIn()` returns `true` during +this time). It never returns `null` on the server, but it may throw an +exception if the client skipped the authentication handshake (which indicates +the client is not running accounts-sandstorm, which is rather suspicious!). + +## Synchronization with Meteor Accounts + +`accounts-sandstorm` does NOT require `accounts-base`. However, if you do +include `accounts-base` in your dependencies, then `accounts-sandstorm` will +integrate with it in order to store information about users seen previously. +In particular: + +* A Meteor account will be automatically created for each logged-in Sandstorm user, + the first time they visit the grain. +* In the `Meteor.users` table, `services.sandstorm` will contain the same data + returned by `Meteor.sandstormUser()`. +* `Meteor.loggingIn()` will return `true` during the initial handshake (when + `sandstormUser()` would return `null`). + +Please note, however, that you should prefer `sandstormUser()` over +`Meteor.user().services.sandstorm` whenever possible, **especially** for enforcing +permissions, for a few reasons: + +* Anonymous users do NOT get a table entry, therefore `Meteor.user()` will be + `null` for them. However, anonymous users of a sharing link may have permissions! +* Moreover, in the future, anonymous users may additionally be able to assign + themselves names, handles, avatars, etc. The only thing that makes them "anonymous" + is that they have not provided the app with a unique identifier that could be used + to recognize the same user when they visit again later. +* `services.sandstorm` is only updated when the user is online; it may be stale + when they are not present. This implies that when a user's access is revoked, + their user table entry will never be updated again, and will continue to + indicate that they have permissions when they in fact no longer do. + +## Development aids + +`accounts-sandstorm` normally works its magic when running inside Sandstorm. However, +it's often a lot more convenient to develop Meteor apps using Meteor's normal dev tools +which currently cannot run inside Sandstorm. + +Therefore, when *not* running inside Sansdtorm, you may use the following console +function to fake your user information: + + SandstormAccounts.setTestUserInfo({ + id: "12345", + name: "Alice", + // ... other parameters, as listed above ... + }); + +This will cause `accounts-sandstorm` to spoof the `X-Sandstorm-*` headers with the +parameters you provided when it attempts to log in. When actually running inside +Sandstorm, such spoofing is blocked by Sandstorm, but when running outside it will +work and now you can test your app. + +Note that this functionality, like all of the package, is only enabled if you set the +`SANDSTORM` environment variable. So, run `meteor` like so: + + SANDSTORM=1 meteor + +## Migrating from 0.1 + +In version 0.1.x of this puackage, there was no `sandstormUser()` function; the +only mode of operation was through Meteor accounts. This had problems with +permissions and anonymous users as described adove. Introducing `sandstormUser()` +is a huge update. + +For almost all users, 0.2 should be a drop-in replacement for 0.1, only adding +new features. Please note, though, two possible issues: + +* If you did not explicitly depend on `accounts-base` before, you must add this + dependency, since it is no longer implied by `accounts-sansdtorm`. +* The `/.sandstorm-credentials` endpoint no longer exists. If you were directly + fetching this undocumented endpoint before, you will need to switch your code + to use `Meteor.sandstormUser()`. diff --git a/packages/wekan-accounts-sandstorm/client.js b/packages/wekan-accounts-sandstorm/client.js new file mode 100644 index 000000000..61c176fcb --- /dev/null +++ b/packages/wekan-accounts-sandstorm/client.js @@ -0,0 +1,186 @@ +// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +function loginWithSandstorm(connection, apiHost, apiToken) { + // Log in the connection using Sandstorm authentication. + // + // After calling this, connection.sandstormUser() will reactively return an object containing + // Sansdstorm user info, including permissions as authenticated by the server. Even if the user + // is anonymous, this information is returned. `sandstormUser()` returns `null` up until the + // point where login succeeds. + + // How this works: + // 1. We create a cryptographically random token, which we're going to send to the server twice. + // 2. We make a method call to log in with this token. The server initially has no idea who + // is calling and will block waiting for info. (The method is marked "wait" on the client side + // so that further method calls are blocked until login completes.) + // 3. We also send an XHR with the same token. When the server receives the XHR, it harvests the + // Sandstorm headers, looks for the corresponding login method call, marks its connection as + // logged in, and then lets it return. + // + // We don't actually use Accounts.callLoginMethod() because we don't need or want the + // "resume token" logic. On a disconnect, we need to re-authenticate, because the user's + // permissions may have changed (indeed, this may be the reason for the disconnect). + + // If the connection doesn't already have a sandstormUser() method, add it now. + if (!connection._sandstormUser) { + connection._sandstormUser = new ReactiveVar(null); + connection.sandstormUser = connection._sandstormUser.get.bind(connection._sandstormUser); + } + + // Generate a random token which we'll send both over an XHR and over DDP at the same time. + var token = Random.secret(); + + var waiting = true; // We'll keep retrying XHRs until the method returns. + var reconnected = false; + + var onResultReceived = function (error, result) { + waiting = false; + + if (error) { + // ignore for now; loggedInAndDataReadyCallback() will get the error too + } else { + connection.onReconnect = function () { + reconnected = true; + loginWithSandstorm(connection, apiHost, apiToken); + }; + } + }; + + var loggedInAndDataReadyCallback = function (error, result) { + if (reconnected) { + // Oh, we're already on a future connection attempt. Don't mess with anything. + return; + } + + if (error) { + console.error("loginWithSandstorm failed:", error); + } else { + connection._sandstormUser.set(result.sandstorm); + connection.setUserId(result.userId); + } + }; + + Meteor.apply("loginWithSandstorm", [token], + {wait: true, onResultReceived: onResultReceived}, + loggedInAndDataReadyCallback); + + var sendXhr = function () { + if (!waiting) return; // Method call finished. + + headers = {"Content-Type": "application/x-sandstorm-login-token"}; + + var testInfo = localStorage.sandstormTestUserInfo; + if (testInfo) { + testInfo = JSON.parse(testInfo); + if (testInfo.id) { + headers["X-Sandstorm-User-Id"] = testInfo.id; + } + if (testInfo.name) { + headers["X-Sandstorm-Username"] = encodeURI(testInfo.name); + } + if (testInfo.picture) { + headers["X-Sandstorm-User-Picture"] = testInfo.picture; + } + if (testInfo.permissions) { + headers["X-Sandstorm-Permissions"] = testInfo.permissions.join(","); + } + if (testInfo.preferredHandle) { + headers["X-Sandstorm-Preferred-Handle"] = testInfo.preferredHandle; + } + if (testInfo.pronouns) { + headers["X-Sandstorm-User-Pronouns"] = testInfo.pronouns; + } + } + + var postUrl = "/.sandstorm-login"; + // Sandstorm mobile apps need to point at a different host and use an Authorization token. + if (apiHost) { + postUrl = apiHost + postUrl; + headers.Authorization = "Bearer " + apiToken; + } + + // Send the token in an HTTP POST request which on the server side will allow us to receive the + // Sandstorm headers. + HTTP.post(postUrl, + {content: token, headers: headers}, + function (error, result) { + if (error) { + console.error("couldn't get /.sandstorm-login:", error); + + if (waiting) { + // Try again in a second. + Meteor.setTimeout(sendXhr, 1000); + } + } + }); + }; + + // Wait until the connection is up before we start trying to send XHRs. + var stopImmediately = false; // Unfortunately, Tracker.autorun() runs the first time inline. + var handle = Tracker.autorun(function () { + if (!waiting) { + if (handle) { + handle.stop(); + } else { + stopImmediately = true; + } + return; + } else if (connection.status().connected) { + if (handle) { + handle.stop(); + } else { + stopImmediately = true; + } + + // Wait 10ms before our first attempt to send the rendezvous XHR because if it arrives + // before the method call it will be rejected. + Meteor.setTimeout(sendXhr, 10); + } + }); + if (stopImmediately) handle.stop(); +} + +if (__meteor_runtime_config__.SANDSTORM) { + // Auto-login the main Meteor connection. + loginWithSandstorm(Meteor.connection, __meteor_runtime_config__.SANDSTORM_API_HOST, + __meteor_runtime_config__.SANDSTORM_API_TOKEN); + + if (Package["accounts-base"]) { + // Make Meteor.loggingIn() work by calling a private method of accounts-base. If this breaks then + // maybe we should just overwrite Meteor.loggingIn() instead. + Tracker.autorun(function () { + Package["accounts-base"].Accounts._setLoggingIn(!Meteor.connection.sandstormUser()); + }); + } + + Meteor.sandstormUser = function () { + return Meteor.connection.sandstormUser(); + }; + + SandstormAccounts = { + setTestUserInfo: function (info) { + localStorage.sandstormTestUserInfo = JSON.stringify(info); + loginWithSandstorm(Meteor.connection, __meteor_runtime_config__.SANDSTORM_API_HOST, + __meteor_runtime_config__.SANDSTORM_API_TOKEN); + } + }; +} diff --git a/packages/wekan-accounts-sandstorm/package.js b/packages/wekan-accounts-sandstorm/package.js new file mode 100644 index 000000000..1606cf263 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/package.js @@ -0,0 +1,45 @@ +// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +Package.describe({ + summary: "Login service for Sandstorm.io applications", + version: "0.7.0", + name: "wekan-accounts-sandstorm", + git: "https://github.com/sandstorm-io/meteor-accounts-sandstorm.git" +}); + +Package.onUse(function(api) { + api.versionsFrom('1.8.2'); + + api.use('random', ['client', 'server']); + api.use('accounts-base@~2.2.3-rc272.0', ['client', 'server'], {weak: true}); + api.use('webapp', 'server'); + api.use('http', 'client'); + api.use('tracker', 'client'); + api.use('reactive-var', 'client'); + api.use('check', 'server'); + api.use('ddp-server', 'server'); + + api.addFiles("client.js", "client"); + api.addFiles("server.js", "server"); + + api.export("SandstormAccounts", "client"); +}); diff --git a/packages/wekan-accounts-sandstorm/server.js b/packages/wekan-accounts-sandstorm/server.js new file mode 100644 index 000000000..032549692 --- /dev/null +++ b/packages/wekan-accounts-sandstorm/server.js @@ -0,0 +1,210 @@ +// Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +if (process.env.SANDSTORM) { + __meteor_runtime_config__.SANDSTORM = true; +} + +if (__meteor_runtime_config__.SANDSTORM) { + if (Package["accounts-base"]) { + // Highlander Mode: Disable all non-Sandstorm login mechanisms. + Package["accounts-base"].Accounts.validateLoginAttempt(function (attempt) { + if (!attempt.allowed) { + return false; + } + if (attempt.type !== "sandstorm") { + throw new Meteor.Error(403, "Non-Sandstorm login mechanisms disabled on Sandstorm."); + } + return true; + }); + Package["accounts-base"].Accounts.validateNewUser(function (user) { + if (!user.services.sandstorm) { + throw new Meteor.Error(403, "Non-Sandstorm login mechanisms disabled on Sandstorm."); + } + return true; + }); + } + + var Future = Npm.require("fibers/future"); + + var inMeteor = Meteor.bindEnvironment(function (callback) { + callback(); + }); + + var logins = {}; + // Maps tokens to currently-waiting login method calls. + + if (Package["accounts-base"]) { + Meteor.users.createIndex("services.sandstorm.id", {unique: 1, sparse: 1}); + } + + Meteor.onConnection(function (connection) { + connection._sandstormUser = null; + connection._sandstormSessionId = null; + connection._sandstormTabId = null; + connection.sandstormUser = function () { + if (!connection._sandstormUser) { + throw new Meteor.Error(400, "Client did not complete authentication handshake."); + } + return this._sandstormUser; + }; + connection.sandstormSessionId = function () { + if (!connection._sandstormUser) { + throw new Meteor.Error(400, "Client did not complete authentication handshake."); + } + return this._sandstormSessionId; + } + connection.sandstormTabId = function () { + if (!connection._sandstormUser) { + throw new Meteor.Error(400, "Client did not complete authentication handshake."); + } + return this._sandstormTabId; + } + }); + + Meteor.methods({ + loginWithSandstorm: function (token) { + check(token, String); + + var future = new Future(); + + logins[token] = future; + + var timeout = setTimeout(function () { + future.throw(new Meteor.Error("timeout", "Gave up waiting for login rendezvous XHR.")); + }, 10000); + + var info; + try { + info = future.wait(); + } finally { + clearTimeout(timeout); + delete logins[token]; + } + + // Set connection info. The call to setUserId() resets all publishes. We update the + // connection's sandstorm info first so that when the publishes are re-run they'll see the + // new info. In theory we really want to update it exactly when this.userId is updated, but + // we'd have to dig into Meteor internals to pull that off. Probably updating it a little + // early is fine? + // + // Note that calling setUserId() with the same ID a second time still goes through the motions + // of restarting all subscriptions, which is important if the permissions changed. Hopefully + // Meteor won't decide to "optimize" this by returning early if the user ID hasn't changed. + this.connection._sandstormUser = info.sandstorm; + this.connection._sandstormSessionId = info.sessionId; + this.connection._sandstormTabId = info.tabId; + this.setUserId(info.userId); + + return info; + } + }); + + WebApp.rawConnectHandlers.use(function (req, res, next) { + if (req.url === "/.sandstorm-login") { + handlePostToken(req, res); + return; + } + return next(); + }); + + function readAll(stream) { + var future = new Future(); + + var chunks = []; + stream.on("data", function (chunk) { + chunks.push(chunk.toString()); + }); + stream.on("error", function (err) { + future.throw(err); + }); + stream.on("end", function () { + future.return(); + }); + + future.wait(); + + return chunks.join(""); + } + + var handlePostToken = Meteor.bindEnvironment(function (req, res) { + inMeteor(function () { + try { + // Note that cross-origin POSTs cannot set arbitrary Content-Types without explicit CORS + // permission, so this effectively prevents XSRF. + if (req.headers["content-type"].split(";")[0].trim() !== "application/x-sandstorm-login-token") { + throw new Error("wrong Content-Type for .sandstorm-login: " + req.headers["content-type"]); + } + + var token = readAll(req); + + var future = logins[token]; + if (!future) { + throw new Error("no current login request matching token"); + } + + var permissions = req.headers["x-sandstorm-permissions"]; + if (permissions && permissions !== "") { + permissions = permissions.split(","); + } else { + permissions = []; + } + + var sandstormInfo = { + id: req.headers["x-sandstorm-user-id"] || null, + name: decodeURIComponent(req.headers["x-sandstorm-username"]), + permissions: permissions, + picture: req.headers["x-sandstorm-user-picture"] || null, + preferredHandle: req.headers["x-sandstorm-preferred-handle"] || null, + pronouns: req.headers["x-sandstorm-user-pronouns"] || null, + }; + + var userInfo = {sandstorm: sandstormInfo}; + if (Package["accounts-base"]) { + if (sandstormInfo.id) { + // The user is logged into Sandstorm. Create a Meteor account for them, or find the + // existing one, and record the user ID. + var login = Package["accounts-base"].Accounts.updateOrCreateUserFromExternalService( + "sandstorm", sandstormInfo, {profile: {name: sandstormInfo.name}}); + userInfo.userId = login.userId; + } else { + userInfo.userId = null; + } + } else { + // Since the app isn't using regular Meteor accounts, we can define Meteor.userId() + // however we want. + userInfo.userId = sandstormInfo.id; + } + + userInfo.sessionId = req.headers["x-sandstorm-session-id"] || null; + userInfo.tabId = req.headers["x-sandstorm-tab-id"] || null; + future.return(userInfo); + res.writeHead(204, {}); + res.end(); + } catch (err) { + res.writeHead(500, { + "Content-Type": "text/plain" + }); + res.end(err.stack); + } + }); + }); +}