-
-
Notifications
You must be signed in to change notification settings - Fork 709
/
db.js
2865 lines (2483 loc) · 115 KB
/
db.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Sandstorm - Personal Cloud Sandbox
// Copyright (c) 2014 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.
// This file defines the database schema.
// Useful for debugging: Set the env variable LOG_MONGO_QUERIES to have the server write every
// query it makes, so you can see if it's doing queries too often, etc.
if (Meteor.isServer && process.env.LOG_MONGO_QUERIES) {
const oldFind = Mongo.Collection.prototype.find;
Mongo.Collection.prototype.find = function () {
console.log(this._prefix, arguments);
return oldFind.apply(this, arguments);
};
}
// Helper so that we don't have to if (Meteor.isServer) before declaring indexes.
if (Meteor.isServer) {
Mongo.Collection.prototype.ensureIndexOnServer = Mongo.Collection.prototype._ensureIndex;
} else {
Mongo.Collection.prototype.ensureIndexOnServer = function () {};
}
// TODO(soon): Systematically go through this file and add ensureIndexOnServer() as needed.
const collectionOptions = { defineMutationMethods: Meteor.isClient };
// Set to `true` on the client so that method simulation works. Set to `false` on the server
// so that we can be extra certain that all mutations must go through methods.
// Users = new Mongo.Collection("users");
// The users collection is special and can be accessed through `Meteor.users`.
// See https://docs.meteor.com/#/full/meteor_users.
//
// There are two distinct types of entries in the users collection: identities and accounts. An
// identity contains personal profile information and typically includes some intrinsic method for
// authenticating as the owner of that information.
//
// An account is an owner of app actions, grains, contacts, notifications, and payment info.
// Each account can have multiple identities linked to it. To log in as an account you must first
// authenticate as one of its linked identities.
//
// Every user contains the following fields:
// _id: Unique string ID. For accounts, this is random. For identities, this is the globally
// stable SHA-256 ID of this identity, hex-encoded.
// createdAt: Date when this entry was added to the collection.
// lastActive: Date of the user's most recent interaction with this Sandstorm server.
// services: Object containing login data used by Meteor authentication services.
// expires: Date when this user should be deleted. Only present for demo users.
// upgradedFromDemo: If present, the date when this user was upgraded from being a demo user.
// TODO(cleanup): Unlike other dates in our database, this is stored as a number
// rather than as a Date object. We should fix that.
// appDemoId: If present and non-null, then the user is a demo user who arrived via an /appdemo/
// link. This field contains the app ID of the app that the user started out demoing.
// Unlike the `expires` field, this field is not cleared when the user upgrades from
// being a demo user.
// suspended: If this exists, this account/identity is supsended. Both accounts and identities
// can be suspended. After some amount of time, the user will be completely deleted
// and removed from the DB.
// It is an object with fields:
// voluntary: Boolean. This is true if the user initiated it. They will have the
// chance to still login and reverse the suspension/deletion.
// admin: The userId of the admin who suspended the account.
// timestamp: Date object. When the suspension occurred.
// willDelete: Boolean. If true, this account will be deleted after some time.
//
// Identity users additionally contain the following fields:
// profile: Object containing the data that will be shared with users and grains that come into
// contact with this identity. Includes the following fields:
// service: String containing the name of this identity's authentication method.
// name: String containing the chosen display name of the identity.
// handle: String containing the identity's preferred handle.
// picture: _id into the StaticAssets table for the identity's picture. If not present,
// an identicon will be used.
// pronoun: One of "male", "female", "neutral", or "robot".
// unverifiedEmail: If present, a string containing an email address specified by the user.
// referredBy: ID of the Account that referred this Identity.
//
// Account users additionally contain the following fields:
// loginIdentities: Array of identity objects, each of which may include the following fields.
// id: The globally-stable SHA-256 ID of this identity, hex-encoded.
// nonloginIdentities: Array of identity objects, of the same form as `loginIdentities`. We use
// a separate array here so that we can use a Mongo index to enforce the
// invariant that an identity only be a login identity for a single account.
// primaryEmail: String containing this account's primary email address. Must be a verified adress
// of one of this account's linked identities. Call SandstormDb.getUserEmails()
// to do this checking automatically.
// isAdmin: Boolean indicating whether this account is allowed to access the Sandstorm admin panel.
// signupKey: If this is an invited user, then this field contains their signup key.
// signupNote: If the user was invited through a link, then this field contains the note that the
// inviter admin attached to the key.
// signupEmail: If the user was invited by email, then this field contains the email address that
// the invite was sent to.
// hasCompletedSignup: True if this account has confirmed its profile and agreed to this server's
// terms of service.
// plan: _id of an entry in the Plans table which determines the user's quota.
// planBonus: {storage, compute, grains} bonus amounts to add to the user's plan. The payments
// module writes data here; we merely read it. Missing fields should be treated as
// zeroes. Does not yet include referral bonus, which is calculated separately.
// TODO(cleanup): Use for referral bonus too.
// storageUsage: Number of bytes this user is currently storing.
// payments: Object defined by payments module, if loaded.
// dailySentMailCount: Number of emails sent by this user today; used to limit spam.
// accessRequests: Object containing the following fields; used to limit spam.
// count: Number of "request access" emails during sent during the current interval.
// resetOn: Date when the count should be reset.
// referredByComplete: ID of the Account that referred this Account. If this is set, we
// stop writing new referredBy values onto Identities for this account.
// referredCompleteDate: The Date at which the completed referral occurred.
// referredIdentityIds: List of Identity IDs that this Account has referred. This is used for
// reliably determining which Identity's names are safe to display.
// experiments: Object where each field is an experiment that the user is in, and each value
// is the parameters for that experiment. Typically, the value simply names which
// experiment group which the user is in, where "control" is one group. If an experiment
// is not listed, then the user should not be considered at all for the purpose of that
// experiment. Each experiment may define a point in time where users not already in the
// experiment may be added to it and assigned to a group (for example, at user creation
// time). Current experiments:
// firstTimeBillingPrompt: Value is "control" or "test". Users are assigned to groups at
// account creation on servers where billing is enabled (i.e. Oasis). Users in the
// test group will see a plan selection dialog and asked to make an explitic choice
// (possibly "free") before they can create grains (but not when opening someone
// else's shared grain). The goal of the experiment is to determine whether this
// prompt scares users away -- and also whether it increases paid signups.
// freeGrainLimit: Value is "control" or or a number indicating the grain limit that the
// user should receive when on the "free" plan, e.g. "Infinity".
// stashedOldUser: A complete copy of this user from before the accounts/identities migration.
// TODO(cleanup): Delete this field once we're sure it's safe to do so.
Meteor.users.ensureIndexOnServer("services.google.email", { sparse: 1 });
Meteor.users.ensureIndexOnServer("services.github.emails.email", { sparse: 1 });
Meteor.users.ensureIndexOnServer("services.email.email", { unique: 1, sparse: 1 });
Meteor.users.ensureIndexOnServer("loginIdentities.id", { unique: 1, sparse: 1 });
Meteor.users.ensureIndexOnServer("nonloginIdentities.id", { sparse: 1 });
Meteor.users.ensureIndexOnServer("services.google.id", { unique: 1, sparse: 1 });
Meteor.users.ensureIndexOnServer("services.github.id", { unique: 1, sparse: 1 });
Meteor.users.ensureIndexOnServer("suspended.willDelete", { sparse: 1 });
// TODO(cleanup): This index is obsolete; delete it.
Meteor.users.ensureIndexOnServer("identities.id", { unique: 1, sparse: 1 });
Packages = new Mongo.Collection("packages", collectionOptions);
// Packages which are installed or downloading.
//
// Each contains:
// _id: 128-bit prefix of SHA-256 hash of spk file, hex-encoded.
// status: String. One of "download", "verify", "unpack", "analyze", "ready", "failed", "delete"
// progress: Float. -1 = N/A, 0-1 = fractional progress (e.g. download percentage),
// >1 = download byte count.
// error: If status is "failed", error message string.
// manifest: If status is "ready", the package manifest. See "Manifest" in package.capnp.
// appId: If status is "ready", the application ID string. Packages representing different
// versions of the same app have the same appId. The spk tool defines the app ID format
// and can cryptographically verify that a package belongs to a particular app ID.
// shouldCleanup: If true, a reference to this package was recently dropped, and the package
// collector should at some point check whether there are any other references and, if not,
// delete the package.
// url: When status is "download", the URL from which the SPK can be obtained, if provided.
// isAutoUpdated: This package was downloaded as part of an auto-update. We shouldn't clean it up
// even if it has no users.
// authorPgpKeyFingerprint: Verified PGP key fingerprint (SHA-1, hex, all-caps) of the app
// packager.
DevPackages = new Mongo.Collection("devpackages", collectionOptions);
// List of packages currently made available via the dev tools running on the local machine.
// This is normally empty; the only time it is non-empty is when a developer is using the spk tool
// on the local machine to publish an under-development app to this server. That should only ever
// happen on developers' desktop machines.
//
// While a dev package is published, it automatically appears as installed by every user of the
// server, and it overrides all packages with the same application ID. If any instances of those
// packages are currently open, they are killed and reset on publish.
//
// When the dev tool disconnects, the package is automatically unpublished, and any open instances
// are again killed and refreshed.
//
// Each contains:
// _id: The package ID string (as with Packages._id).
// appId: The app ID this package is intended to override (as with Packages.appId).
// timestamp: Time when the package was last updated. If this changes while the package is
// published, all running instances are reset. This is used e.g. to reset the app each time
// changes are made to the source code.
// manifest: The app's manifest, as with Packages.manifest.
// mountProc: True if the supervisor should mount /proc.
UserActions = new Mongo.Collection("userActions", collectionOptions);
// List of actions that each user has installed which create new grains. Each app may install
// some number of actions (usually, one).
//
// Each contains:
// _id: random
// userId: Account ID of the user who has installed this action.
// packageId: Package used to run this action.
// appId: Same as Packages.findOne(packageId).appId; denormalized for searchability.
// appTitle: Same as Packages.findOne(packageId).manifest.appTitle; denormalized so
// that clients can access it without subscribing to the Packages collection.
// appVersion: Same as Packages.findOne(packageId).manifest.appVersion; denormalized for
// searchability.
// appMarketingVersion: Human-readable presentation of the app version, e.g. "2.9.17"
// title: JSON-encoded LocalizedText title for this action, e.g.
// `{defaultText: "New Spreadsheet"}`.
// nounPhrase: JSON-encoded LocalizedText describing what is created when this action is run.
// command: Manifest.Command to run this action (see package.capnp).
Grains = new Mongo.Collection("grains", collectionOptions);
// Grains belonging to users.
//
// Each contains:
// _id: random
// packageId: _id of the package of which this grain is an instance.
// packageSalt: If present, a random string that will used in session ID generation. This field
// is usually updated when `packageId` is updated, triggering automatic refreshes for
// clients with active sessions.
// appId: Same as Packages.findOne(packageId).appId; denormalized for searchability.
// appVersion: Same as Packages.findOne(packageId).manifest.appVersion; denormalized for
// searchability.
// userId: The _id of the account that owns this grain.
// identityId: The identity with which the owning account prefers to open this grain.
// title: Human-readable string title, as chosen by the user.
// lastUsed: Date when the grain was last used by a user.
// private: If true, then knowledge of `_id` does not suffice to open this grain.
// cachedViewInfo: The JSON-encoded result of `UiView.getViewInfo()`, cached from the most recent
// time a session to this grain was opened.
// trashed: If present, the Date when this grain was moved to the trash bin. Thirty days after
// this date, the grain will be automatically deleted.
// suspended: If true, the owner of this grain has been suspended. They will soon be deleted,
// so treat this grain the same as "trashed". It is denormalized out of Users for ease
// of querying.
// ownerSeenAllActivity: True if the owner has viewed the grain since the last activity event
// occurred. See also ApiTokenOwner.user.seenAllActivity.
// size: On-disk size of the grain in bytes.
//
// The following fields *might* also exist. These are temporary hacks used to implement e-mail and
// web publishing functionality without powerbox support; they will be replaced once the powerbox
// is implemented.
// publicId: An id used to publicly identify this grain. Used e.g. to route incoming e-mail and
// web publishing. This field is initialized when first requested by the app.
Grains.ensureIndexOnServer("cachedViewInfo.matchRequests.tags.id", { sparse: 1 });
RoleAssignments = new Mongo.Collection("roleAssignments", collectionOptions);
// *OBSOLETE* Before `user` was a variant of ApiTokenOwner, this collection was used to store edges
// in the permissions sharing graph. This functionality has been subsumed by the ApiTokens
// collection.
Contacts = new Mongo.Collection("contacts", collectionOptions);
// Edges in the social graph.
//
// If Alice has Bob as a contact, then she is allowed to see Bob's profile information and Bob
// will show up in her user-picker UI for actions like share-by-identity.
//
// Contacts are not symmetric. Bob might be one of Alice's contacts even if Alice is not one of
// Bob's.
//
// Each contains:
// _id: random
// ownerId: The accountId of the user account who owns this contact.
// petname: Human-readable label chosen by and only visible to the owner. Uniquely identifies
// the contact to the owner.
// created: Date when this contact was created.
// identityId: The `_id` of the user whose contact info this contains.
Sessions = new Mongo.Collection("sessions", collectionOptions);
// UI sessions open to particular grains. A new session is created each time a user opens a grain.
//
// Each contains:
// _id: String generated as a SHA256 hash of the grain ID, the user ID, a salt generated by the
// client, and the grain's `packageSalt`.
// grainId: _id of the grain to which this session is connected.
// hostId: ID part of the hostname from which this grain is being served. I.e. this replaces the
// '*' in WILDCARD_HOST.
// tabId: Random value unique to the grain tab in which this session is displayed. Typically
// every session has a different `tabId`, but embedded sessions (including in the powerbox)
// have the same `tabId` as the outer session.
// timestamp: Time of last keep-alive message to this session. Sessions time out after some
// period.
// userId: Account ID of the user who owns this session.
// identityId: Identity ID of the user who owns this session.
// hashedToken: If the session is owned by an anonymous user, the _id of the entry in ApiTokens
// that was used to open it. Note that for old-style sharing (i.e. when !grain.private),
// anonymous users can get access without an API token and so neither userId nor hashedToken
// are present.
// powerboxView: Information about a server-initiated powerbox interaction taking place in this
// session. When the client sees a `powerboxView` appear on the session, it opens the
// powerbox popup according to the contents. This field is an object containing one of:
// offer: A capability is being offered to the user by the app. This is an object containing:
// token: For a non-UiView capability, the API token that can be used to restore this
// capability.
// uiView: A UiView capability. This object contains one of:
// tokenId: The _id of an ApiToken belonging to the current user.
// token: A full webkey token which can be opened by an anonymous user.
// fulfill: A capability is being offered which fulfills the active powerbox request. This
// is an object with members:
// token: The SturdyRef of the fulfilling capability. This token can only be used in a call
// to claimRequest() by the requesting
// grain.
// descriptor: Packed-base64 PowerboxDescriptor for the capability.
// powerboxRequest: If present, this session is a powerbox request session. Object containing:
// descriptors: Array of PowerboxDescriptors representing the request.
// requestingSession: Session ID of the session initiating the request.
// viewInfo: The UiView.ViewInfo corresponding to the underlying UiSession. This isn't populated
// until newSession is called on the UiView.
// permissions: The permissions for the current identity on this UiView. This isn't populated
// until newSession is called on the UiView.
// hasLoaded: Marked as true by the proxy when the underlying UiSession has responded to its first
// request
SignupKeys = new Mongo.Collection("signupKeys", collectionOptions);
// Invite keys which may be used by users to get access to Sandstorm.
//
// Each contains:
// _id: random
// used: Boolean indicating whether this key has already been consumed.
// note: Text note assigned when creating key, to keep track of e.g. whom the key was for.
// email: If this key was sent as an email invite, the email address to which it was sent.
ActivityStats = new Mongo.Collection("activityStats", collectionOptions);
// Contains usage statistics taken on a regular interval. Each entry is a data point.
//
// Each contains:
// timestamp: Date when measurements were taken.
// daily: Contains stats counts pertaining to the last day before the sample time.
// weekly: Contains stats counts pertaining to the last seven days before the sample time.
// monthly: Contains stats counts pertaining to the last thirty days before the timestamp.
//
// Each of daily, weekly, and monthly contains:
// activeUsers: The number of unique users who have used a grain on the server in the time
// interval. Only counts logged-in users.
// demoUsers: Demo users.
// appDemoUsers: Users that came in through "app demo".
// activeGrains: The number of unique grains that have been used in the time interval.
// apps: An object indexed by app ID recording, for each app:
// owners: Number of unique owners of this app (counting only grains that still exist).
// sharedUsers: Number of users who have accessed other people's grains of this app (counting
// only grains that still exist).
// grains: Number of active grains of this app (that still exist).
// deleted: Number of non-demo grains of this app that were deleted.
// demoed: Number of demo grains created and expired.
// appDemoUsers: Number of app demos initiated with this app.
DeleteStats = new Mongo.Collection("deleteStats", collectionOptions);
// Contains records of objects that were deleted, for stat-keeping purposes.
//
// Each contains:
// type: "grain" or "user" or "demoGrain" or "demoUser" or "appDemoUser"
// lastActive: Date of the user's or grain's last activity.
// appId: For type = "grain", the app ID of the grain. For type = "appDemoUser", the app ID they
// arrived to demo. For others, undefined.
// experiments: The experiments the user (or owner of the grain) was in. See user.experiments.
FileTokens = new Mongo.Collection("fileTokens", collectionOptions);
// Tokens corresponding to backup files that are currently stored on the server. A user receives
// a token when they create a backup file (either by uploading it, or by backing up one of their
// grains) and may use the token to read the file (either to download it, or to restore a new
// grain from it).
//
// Each contains:
// _id: The unguessable token string.
// name: Suggested filename.
// timestamp: File creation time. Used to figure out when the token and file should be wiped.
ApiTokens = new Mongo.Collection("apiTokens", collectionOptions);
// Access tokens for APIs exported by apps.
//
// Originally API tokens were only used by external users through the HTTP API endpoint. However,
// now they are also used to implement SturdyRefs, not just held by external users, but also when
// an app holds a SturdyRef to another app within the same server. See the various `save()`,
// `restore()`, and `drop()` methods in `grain.capnp` (on `SandstormApi`, `AppPersistent`, and
// `MainView`) -- the fields of type `Data` are API tokens.
//
// Each contains:
// _id: A SHA-256 hash of the token, base64-encoded.
// grainId: The grain servicing this API. (Not present if the API isn't serviced by a grain.)
// identityId: For UiView capabilities, this is the identity for which the view is attenuated.
// That is, the UiView's newSession() method will intersect the requested permissions
// with this identity's permissions before forwarding on to the underlying app. If
// `identityId` is not present, then no identity attenuation is applied, i.e. this is
// a raw UiView as implemented by the app. (The `roleAssignment` field, below, may
// still apply. For non-UiView capabilities, `identityId` is never present. Note that
// this is NOT the identity against which the `requiredPermissions` parameter of
// `SandstormApi.restore()` is checked; that would be `owner.grain.introducerIdentity`.
// accountId: For tokens where `identityId` is set, the `_id` (in the Users table) of the account
// that created the token.
// roleAssignment: If this API token represents a UiView, this field contains a JSON-encoded
// Grain.ViewSharingLink.RoleAssignment representing the permissions it carries. These
// permissions will be intersected with those held by `identityId` when the view is
// opened.
// forSharing: If true, requests sent to the HTTP API endpoint with this token will be treated as
// anonymous rather than as directly associated with `identityId`. This has no effect
// on the permissions granted.
// objectId: If present, this token represents an arbitrary Cap'n Proto capability exported by
// the app or its supervisor (whereas without this it strictly represents UiView).
// sturdyRef is the JSON-encoded SupervisorObjectId (defined in `supervisor.capnp`).
// Note that if the SupervisorObjectId contains an AppObjectId, that field is
// treated as type AnyPointer, and so encoded as a raw Cap'n Proto message.
// frontendRef: If present, this token actually refers to an object implemented by the front-end,
// not a particular grain. (`grainId` and `identityId` are not set.) This is an object
// containing exactly one of the following fields:
// notificationHandle: A `Handle` for an ongoing notification, as returned by
// `NotificationTarget.addOngoing`. The value is an `_id` from the
// `Notifications` collection.
// ipNetwork: An IpNetwork capability that is implemented by the frontend. Eventually, this
// will be moved out of the frontend and into the backend, but we'll migrate the
// database when that happens. This field contains the boolean true to signify that
// it has been set.
// ipInterface: Ditto IpNetwork, except it's an IpInterface.
// emailVerifier: An EmailVerifier capability that is implemented by the frontend. The
// value is an object containing the fields `id` and `services`. `id` is the
// value returned by `EmailVerifier.getId()` and is used as part of a
// powerbox query for matching verified emails. `services` is a
// list of names of identity providers that are trusted to verify addresses.
// If `services` is omitted or falsy, all configured identity providers are
// trusted. Note that a malicious user could specify invalid names in the
// list; they should be ignored.
// verifiedEmail: An VerifiedEmail capability that is implemented by the frontend.
// An object containing `verifierId`, `tabId`, and `address`.
// identity: An Identity capability. The field is the identity ID.
// parentToken: If present, then this token represents exactly the capability represented by
// the ApiToken with _id = parentToken, except possibly (if it is a UiView) attenuated
// by `roleAssignment` (if present). To facilitate permissions computations, if the
// capability is a UiView, then `grainId` is set to the backing grain, `identityId`
// is set to the identity that shared the view, and `accountId` is set to the account
// that shared the view. Neither `objectId` nor `frontendRef` is present when
// `parentToken` is present.
// petname: Human-readable label for this access token, useful for identifying tokens for
// revocation. This should be displayed when visualizing incoming capabilities to
// the grain identified by `grainId`.
// created: Date when this token was created.
// revoked: If true, then this sturdyref has been revoked and can no longer be restored. It may
// become un-revoked in the future.
// trashed: If present, the Date when this token was moved to the trash bin. Thirty days after
// this date, the token will be automatically deleted.
// suspended: If true, the owner of this token has been suspended. They will soon be deleted,
// so treat this token the same as "trashed". It is denormalized out of Users for
// ease of querying.
// expires: Optional expiration Date. If undefined, the token does not expire.
// lastUsed: Optional Date when this token was last used.
// owner: A `ApiTokenOwner` (defined in `supervisor.capnp`, stored as a JSON object)
// as passed to the `save()` call that created this token. If not present, treat
// as `webkey` (the default for `ApiTokenOwner`).
// expiresIfUnused:
// Optional Date after which the token, if it has not been used yet, expires.
// This field should be cleared on a token's first use.
// requirements: List of conditions which must hold for this token to be considered valid.
// Semantically, this list specifies the powers which were *used* to originally
// create the token. If any condition in the list becomes untrue, then the token must
// be considered revoked, and all live refs and sturdy refs obtained transitively
// through it must also become revoked. Each item is the JSON serialization of the
// `MembraneRequirement` structure defined in `supervisor.capnp`.
// hasApiHost: If true, there is an entry in ApiHosts for this token, which will need to be
// cleaned up when the token is.
//
// It is important to note that a token's owner and provider are independent from each other. To
// illustrate, here is an approximate definition of ApiToken in pseudo Cap'n Proto schema language:
//
// struct ApiToken {
// owner :ApiTokenOwner;
// provider :union {
// grain :group {
// grainId :Text;
// union {
// uiView :group {
// identityId :Text;
// roleAssignment :RoleAssignment;
// forSharing :Bool;
// }
// objectId :SupervisorObjectId;
// }
// }
// frontendRef :union {
// notificationHandle :Text;
// ipNetwork :Bool;
// ipInterface :Bool;
// emailVerifier :group {
// id :Text;
// services :List(String);
// }
// verifiedEmail :group {
// verifierId :Text;
// tabId :Text;
// address :Text;
// }
// identity :Text;
// }
// child :group {
// parentToken :Text;
// union {
// uiView :group {
// grainId :Text;
// identityId :Text;
// roleAssignment :RoleAssignment = (allAccess = ());
// }
// other :Void;
// }
// }
// }
// requirements: List(Supervisor.MembraneRequirement);
// ...
// }
ApiTokens.ensureIndexOnServer("grainId", { sparse: 1 });
ApiTokens.ensureIndexOnServer("owner.user.identityId", { sparse: 1 });
ApiTokens.ensureIndexOnServer("frontendRef.emailVerifier.id", { sparse: 1 });
ApiHosts = new Mongo.Collection("apiHosts", collectionOptions);
// Allows defining some limited static behavior for an API host when accessed unauthenticated. This
// mainly exists to allow backwards-compatibility with client applications that expect to be able
// to probe an API host without authentication to determine capabilities such as DAV protocols
// supported, before authenticating to perform real requests. An app can specify these properties
// when creating an offerTemplate.
//
// Each contains:
// _id: apiHostIdHashForToken() of the corresponding API token.
// hash2: hash(hash(token)), aka hash(ApiToken._id). Used to allow ApiHosts to be cleaned
// up when ApiTokens are deleted.
// options: Specifies how to respond to unauthenticated OPTIONS requests on this host.
// This is an object containing fields:
// dav: List of strings specifying DAV header `compliance-class`es, e.g. "1" or
// "calendar-access". https://tools.ietf.org/html/rfc4918#section-10.1
// resources: Object mapping URL paths (including initial '/') to static HTTP responses to
// give when those paths are accessed unauthenticated. Due to Mongo disliking '.'
// and '$' in keys, these characters must be escaped as '\uFF0E' and '\uFF04'
// (see SandstormDb.escapeMongoKey). Each value in this map is an object with
// fields:
// type: Content-Type.
// language: Content-Language.
// encoding: Content-Encoding.
// body: Entity-body as a string or buffer.
Notifications = new Mongo.Collection("notifications", collectionOptions);
// Notifications for a user.
//
// Each contains:
// _id: random
// grainId: The grain originating this notification, if any.
// userId: Account ID of the user receiving the notification.
// text: The JSON-ified LocalizedText to display in the notification.
// isUnread: Boolean indicating if this notification is unread.
// timestamp: Date when this notification was last updated
// eventType: If this notification is due to an activity event, this is the numeric index
// of the event type on the grain's ViewInfo.
// count: The number of times this exact event has repeated. Identical events are
// aggregated by incrementing the count.
// initiatingIdentity: Identity ID of the user who initiated this notification.
// initiatorAnonymous: True if the initiator is an anonymous user. If neither this nor
// initiatingIdentity is present, the notification is not from a user.
// path: Path inside the grain to which the user should be directed if they click on
// the notification.
// ongoing: If present, this is an ongoing notification, and this field contains an
// ApiToken referencing the `OngoingNotification` capability.
// admin: If present, this is a notification intended for an admin.
// action: If present, this is a (string) link that the notification should direct the
// admin to.
// type: The type of notification -- currently can only be "reportStats".
// appUpdates: If present, this is an app update notification. It is an object with the appIds
// as keys.
// $appId: The appId that has an outstanding update.
// packageId: The packageId that it will update to.
// name: The name of the app. (appTitle from package.manifest)
// version: The app's version number. (appVersion from package.manifest)
// marketingVersion: String marketing version of this app. (appMarketingVersion from package.manifest)
// referral: If this boolean field is true, then treat this notification as a referral
// notification. This causes text to be ignored, since we need custom logic.
// mailingListBonus: Like `referral`, but notify the user about the mailing list bonus. This is
// a one-time notification only to Oasis users who existed when the bonus program
// was implemented.
ActivitySubscriptions = new Mongo.Collection("activitySubscriptions", collectionOptions);
// Activity events to which a user is subscribed.
//
// Each contains:
// _id: random
// identityId: Who is subscribed.
// grainId: Grain to which subscription applies.
// threadPath: If present, the subscription is on a specific thread. Otherwise, it is on the
// whole grain.
// mute: If true, this is an anti-subscription -- matching events should NOT notify.
// This allows is useful to express:
// - A user wants to subscribe to a grain but mute a specific thread.
// - The owner of a grain does not want notifications (normally, they are
// implicitly subscribed).
// - A user no longer wishes to be implicitly subscribed to threads in a grain on
// which they comment, so they mute the grain.
ActivitySubscriptions.ensureIndexOnServer("identityId");
ActivitySubscriptions.ensureIndexOnServer({ "grainId": 1, "threadPath": 1 });
StatsTokens = new Mongo.Collection("statsTokens", collectionOptions);
// Access tokens for the Stats collection
//
// These tokens are used for accessing the ActivityStats collection remotely
// (ie. from a dashboard webapp)
//
// Each contains:
// _id: The token. At least 128 bits entropy (Random.id(22)).
Misc = new Mongo.Collection("misc", collectionOptions);
// Miscellaneous configuration and other settings
//
// This table is currently only used for persisting BASE_URL from one session to the next,
// but in general any miscellaneous settings should go in here
//
// Each contains:
// _id: The name of the setting. eg. "BASE_URL"
// value: The value of the setting.
Settings = new Mongo.Collection("settings", collectionOptions);
// Settings for this Sandstorm instance go here. They are configured through the adminSettings
// route. This collection differs from misc in that any admin user can update it through the admin
// interface.
//
// Each contains:
// _id: The name of the setting. eg. "smtpConfig"
// value: The value of the setting.
// automaticallyReset: Sometimes the server needs to automatically reset a setting. When it does
// so, it will also write an object to this field indicating why the reset was
// needed. That object can have the following variants:
// baseUrlChangedFrom: The reset was due to BASE_URL changing. This field contains a string
// with the old BASE_URL.
// preinstalledApps: A list of objects:
// appId: The Packages.appId of the app to install
// status: packageId
// packageId: The Packages._id of the app to install
//
// potentially other fields that are unique to the setting
Migrations = new Mongo.Collection("migrations", collectionOptions);
// This table tracks which migrations we have applied to this instance.
// It contains a single entry:
// _id: "migrations_applied"
// value: The number of migrations this instance has successfully completed.
StaticAssets = new Mongo.Collection("staticAssets", collectionOptions);
// Collection of static assets served up from the Sandstorm server's "static" host. We only
// support relatively small assets: under 1MB each.
//
// Each contains:
// _id: Random ID; will be used in the URL.
// hash: A base64-encoded SHA-256 hash of the data, used to de-dupe.
// mimeType: MIME type of the asset, suitable for Content-Type header.
// encoding: Either "gzip" or not present, suitable for Content-Encoding header.
// content: The asset content (byte buffer).
// refcount: Number of places where this asset's ID appears in the database. Since Mongo doesn't
// have transactions, this needs to bias towards over-counting; a backup GC could be used
// to catch leaked assets, although it's probably not a big deal in practice.
AssetUploadTokens = new Mongo.Collection("assetUploadTokens", collectionOptions);
// Collection of tokens representing a single-use permission to upload an asset, such as a new
// profile picture.
//
// Each contains:
// _id: Random ID.
// purpose: Contains one of the following, indicating how the asset is to be used:
// profilePicture: Indicates that the upload is a new profile picture. Contains fields:
// userId: Account ID of user whose picture shall be replaced.
// identityId: Which of the user's identities shall be updated.
// expires: Time when this token will go away if unused.
Plans = new Mongo.Collection("plans", collectionOptions);
// Subscription plans, which determine quota.
//
// Each contains:
// _id: Plan ID, usually a short string like "free", "standard", "large", "mega", ...
// storage: Number of bytes this user is allowed to store.
// compute: Number of kilobyte-RAM-seconds this user is allowed to consume.
// computeLabel: Label to display to the user describing this plan's compute units.
// grains: Total number of grains this user can create (often `Infinity`).
// price: Price per month in US cents.
// hidden: If true, a user cannot switch to this plan, but some users may be on it and are
// allowed to switch away.
// title: Title from display purposes. If missing, default to capitalizing _id.
AppIndex = new Mongo.Collection("appIndex", collectionOptions);
// A mirror of the data from the App Market index
//
// Each contains:
// _id: the appId of the app
// The rest of the fields are defined in src/sandstorm/app-index/app-index.capnp:AppIndexForMarket
KeybaseProfiles = new Mongo.Collection("keybaseProfiles", collectionOptions);
// Cache of Keybase profile information. The profile for a user is re-fetched every time a package
// by that user is installed, as well as if the keybase profile is requested and not already
// present for some reason.
//
// Each contains:
// _id: PGP key fingerprint (SHA-1, hex, all-caps)
// displayName: Display name from Keybase. (NOT VERIFIED AT ALL.)
// handle: Keybase handle.
// proofs: The "proofs_summary.all" array from the Keybase lookup. See the non-existent Keybase
// docs for details. We also add a boolean "status" field to each proof indicating whether
// we have directly verified the proof ourselves. Its values may be "unverified" (Keybase
// returned this but we haven't checked it directly), "verified" (we verified the proof and it
// is valid), "invalid" (we checked the proof and it was definitely bogus), or "checking" (the
// server is currently actively checking this proof). Note that if a check fails due to network
// errors, the status goes back to "unverified".
//
// WARNING: Currently verification is NOT IMPLEMENTED, so all proofs will be "unverified"
// for now and we just trust Keybase.
FeatureKey = new Mongo.Collection("featureKey", collectionOptions);
// OBSOLETE: This was used to implement the Sandstorm for Work paywall, which has been removed.
// Collection object still defined because it could have old data in it, for servers that used
// to have a feature key.
SetupSession = new Mongo.Collection("setupSession", collectionOptions);
// Responsible for storing information about setup sessions. Contains a single document with three
// keys:
//
// _id: "current-session"
// creationDate: Date object indicating when this session was created.
// hashedSessionId: the sha256 of the secret session id that was returned to the client
const DesktopNotifications = new Mongo.Collection("desktopNotifications", collectionOptions);
// Responsible for very short-lived queueing of desktop notification information.
// Entries are removed when they are ~30 seconds old. This collection is a bit
// odd in that it is intended primarily for edge-triggered communications, but
// Meteor's collections aren't really designed to support that organization.
// Fields for each :
//
// _id: String. Used as the tag to coordinate notification merging between browser tabs.
// creationDate: Date object. indicating when this notification was posted.
// userId: String. Account id to which this notification was published.
// notificationId: String. ID of the matching event in the Notifications table to dismiss if this
// notification is activated.
// appActivity: Object with fields:
// user: Optional Object. Not present if this notification wasn't generated by a user. If
// present, it will have one of the following shapes:
// { anonymous: true } if this notification was generated by an anonymous user. Otherwise:
// {
// identityId: String The user's identity ID.
// name: String The user's display name.
// avatarUrl: String The URL for the user's profile picture.
// },
// grainId: String, Which grain this action took place on
// path: String, The path of the notification.
// body: Util.LocalizedText, The main body of the activity event.
// actionText: Util.LocalizedText, What action the user took, e.g.
// { defaultText: "added a comment" }
const StandaloneDomains = new Mongo.Collection("standaloneDomains", collectionOptions);
// A standalone domain that points to a single share link. These domains act a little different
// than a normal shared Sandstorm grain. They completely drop any Sandstorm topbar/sidebar, and at
// first glance look completely like a non-Sandstorm hosted webserver. The apps instead act in
// concert with Sandstorm through the postMessage API, which allows it to do things like prompt for
// login.
// Fields for each :
//
// _id: String. The domain name to use.
// token: String. _id of a sharing token (it must be a webkey).
if (Meteor.isServer) {
Meteor.publish("credentials", function () {
// Data needed for isSignedUp() and isAdmin() to work.
if (this.userId) {
const db = this.connection.sandstormDb;
return [
Meteor.users.find({ _id: this.userId },
{ fields: { signupKey: 1, isAdmin: 1, expires: 1, storageUsage: 1,
plan: 1, planBonus: 1, hasCompletedSignup: 1, experiments: 1,
referredIdentityIds: 1, cachedStorageQuota: 1, suspended: 1, }, }),
db.collections.plans.find(),
];
} else {
return [];
}
});
}
const countReferrals = function (user) {
const referredIdentityIds = user.referredIdentityIds;
return (referredIdentityIds && referredIdentityIds.length || 0);
};
const calculateReferralBonus = function (user) {
// This function returns an object of the form:
//
// - {grains: 0, storage: 0}
//
// which are extra resources this account gets as part of participating in the referral
// program. (Storage is measured in bytes, as usual for plans.)
// TODO(cleanup): Consider moving referral bonus logic into Oasis payments module (since it's
// payments-specific) and aggregating into `planBonus`.
// Authorization note: Only call this if accountId is the current user!
const isPaid = (user.plan && user.plan !== "free");
successfulReferralsCount = countReferrals(user);
if (isPaid) {
const maxPaidStorageBonus = 30 * 1e9;
return { grains: 0,
storage: Math.min(
successfulReferralsCount * 2 * 1e9,
maxPaidStorageBonus), };
} else {
const maxFreeStorageBonus = 2 * 1e9;
const bonus = {
storage: Math.min(
successfulReferralsCount * 50 * 1e6,
maxFreeStorageBonus),
};
if (successfulReferralsCount > 0) {
bonus.grains = Infinity;
} else {
bonus.grains = 0;
}
return bonus;
}
};
isAdmin = function () {
// Returns true if the user is the administrator.
const user = Meteor.user();
if (user && user.isAdmin) {
return true;
} else {
return false;
}
};
isAdminById = function (id) {
// Returns true if the user's id is the administrator.
const user = Meteor.users.findOne({ _id: id }, { fields: { isAdmin: 1 } });
if (user && user.isAdmin) {
return true;
} else {
return false;
}
};
findAdminUserForToken = function (token) {
if (!token.requirements) {
return;
}
const requirements = token.requirements.filter(function (requirement) {
return "userIsAdmin" in requirement;
});
if (requirements.length > 1) {
return;
}
if (requirements.length === 0) {
return;
}
return requirements[0].userIsAdmin;
};
const wildcardHost = Meteor.settings.public.wildcardHost.toLowerCase().split("*");
if (wildcardHost.length != 2) {
throw new Error("Wildcard host must contain exactly one asterisk.");
}
matchWildcardHost = function (host) {
// See if the hostname is a member of our wildcard. If so, extract the ID.
// We remove everything after the first ":" character so that our
// comparison logic ignores port numbers.
const prefix = wildcardHost[0];
const suffix = wildcardHost[1].split(":")[0];
const hostSansPort = host.split(":")[0];
if (hostSansPort.lastIndexOf(prefix, 0) >= 0 &&
hostSansPort.indexOf(suffix, -suffix.length) >= 0 &&
hostSansPort.length >= prefix.length + suffix.length) {
const id = hostSansPort.slice(prefix.length, -suffix.length);
if (id.match(/^[-a-z0-9]*$/)) {
return id;
}
}
return null;
};
makeWildcardHost = function (id) {
return wildcardHost[0] + id + wildcardHost[1];
};
const isApiHostId = function (hostId) {
if (hostId) {
const split = hostId.split("-");
if (split[0] === "api") return split[1] || "*";
}
return false;
};
const isTokenSpecificHostId = function (hostId) {
return hostId.lastIndexOf("api-", 0) === 0;
};
let apiHostIdHashForToken;
if (Meteor.isServer) {
const Crypto = Npm.require("crypto");
apiHostIdHashForToken = function (token) {
// Given an API token, compute the host ID that must be used when requesting this token.
// We add a leading 'x' to the hash so that knowing the hostname alone is not sufficient to
// find the corresponding API token in the ApiTokens table (whose _id values are also hashes
// of tokens). This doesn't technically add any security, but helps prove that we don't have
// any bugs which would allow someone who knows only the hostname to access the app API.
return Crypto.createHash("sha256").update("x" + token).digest("hex").slice(0, 32);
};
} else {
apiHostIdHashForToken = function (token) {
// Given an API token, compute the host ID that must be used when requesting this token.
// We add a leading 'x' to the hash so that knowing the hostname alone is not sufficient to
// find the corresponding API token in the ApiTokens table (whose _id values are also hashes
// of tokens). This doesn't technically add any security, but helps prove that we don't have
// any bugs which would allow someone who knows only the hostname to access the app API.
return SHA256("x" + token).slice(0, 32);
};
}
const apiHostIdForToken = function (token) {
return "api-" + apiHostIdHashForToken(token);
};
const makeApiHost = function (token) {
return makeWildcardHost(apiHostIdForToken(token));
};
if (Meteor.isServer) {
const Url = Npm.require("url");
getWildcardOrigin = function () {
// The wildcard URL can be something like "foo-*-bar.example.com", but sometimes when we're
// trying to specify a pattern matching hostnames (say, a Content-Security-Policy directive),
// an astrisk is only allowed as the first character and must be followed by a period. So we need
// "*.example.com" instead -- which matches more than we actually want, but is the best we can
// really do. We also add the protocol to the front (again, that's what CSP wants).
// TODO(cleanup): `protocol` is computed in other files, like proxy.js. Put it somewhere common.
const protocol = Url.parse(process.env.ROOT_URL).protocol;
const dotPos = wildcardHost[1].indexOf(".");
if (dotPos < 0) {
return protocol + "//*";
} else {
return protocol + "//*" + wildcardHost[1].slice(dotPos);
}
};
}
SandstormDb = function (quotaManager) {
// quotaManager is an object with the following method:
// updateUserQuota: It is provided two arguments
// db: This SandstormDb object
// user: A collections.users account object
// and returns a quota object:
// storage: A number (can be Infinity)
// compute: A number (can be Infinity)
// grains: A number (can be Infinity)
this.quotaManager = quotaManager;
this.collections = {
// Direct access to underlying collections. DEPRECATED, but better than accessing the top-level
// collection globals directly.
//
// TODO(cleanup): Over time, we will provide methods covering each supported query and remove
// direct access to the collections.
users: Meteor.users,
packages: Packages,
devPackages: DevPackages,
userActions: UserActions,
grains: Grains,
roleAssignments: RoleAssignments, // Deprecated, only used by the migration that eliminated it.
contacts: Contacts,
sessions: Sessions,
signupKeys: SignupKeys,
activityStats: ActivityStats,
deleteStats: DeleteStats,
fileTokens: FileTokens,
apiTokens: ApiTokens,
apiHosts: ApiHosts,
notifications: Notifications,
activitySubscriptions: ActivitySubscriptions,
statsTokens: StatsTokens,
misc: Misc,
settings: Settings,
migrations: Migrations,