anbaric-cloud-hosting 1.60.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import {MembershipService} from "../../../auth/MembershipService";
2
+ import {EntitlementOffer, EntitlementStore} from "../../../data-store/EntitlementStore";
2
3
  import {canInvite, isTenantRole} from "../../../auth/TenantRole";
3
4
  import {Request} from "../../Request";
4
5
  import {RequestHandler} from "../../RequestHandler";
@@ -6,10 +7,17 @@ import {RequestHandler} from "../../RequestHandler";
6
7
  const EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7
8
 
8
9
  /* The console's side of inviting people into this tenant: list what is
9
- pending, invite by email (attributed to the signed-in user), revoke. */
10
+ pending, invite by email (attributed to the signed-in user), revoke.
11
+
12
+ An invitation may also carry the app it should open on and the entitlements
13
+ the inviter picked out for whoever accepts it. The two are kept apart on
14
+ purpose: the landing app travels to the control plane, which owns the
15
+ invitation and the addresses, while the entitlements stay here, because
16
+ grants are the platform's and are held against the email address until
17
+ that person first signs in. */
10
18
  class InvitationsHandler implements RequestHandler {
11
19
 
12
- constructor(private memberships : MembershipService) {}
20
+ constructor(private memberships : MembershipService, private entitlements? : EntitlementStore) {}
13
21
 
14
22
  async handle(request : Request) : Promise<void> {
15
23
  // Only a tenant's owner or admin administers its membership. Central
@@ -34,7 +42,8 @@ class InvitationsHandler implements RequestHandler {
34
42
  }
35
43
 
36
44
  private async invite(request : Request) : Promise<void> {
37
- const body = await request.body() as { email? : string, role? : string } | undefined;
45
+ const body = await request.body() as
46
+ { email? : string, role? : string, landingApp? : string, entitlements? : Array<unknown> } | undefined;
38
47
  const email = String(body?.email ?? "").trim();
39
48
  if (! EMAIL_SHAPE.test(email)) return request.reply(400, { error: "A valid email address is required" });
40
49
 
@@ -43,7 +52,38 @@ class InvitationsHandler implements RequestHandler {
43
52
  return request.reply(400, { error: "Invite someone as an ADMIN, BUILDER or USER" });
44
53
  }
45
54
 
46
- request.reply(201, await this.memberships.invite(email, role, this.asking(request)));
55
+ const landingApp = typeof body?.landingApp === "string" && body.landingApp.trim()
56
+ ? body.landingApp.trim()
57
+ : undefined;
58
+
59
+ const invitation = await this.memberships.invite(email, role, this.asking(request), landingApp);
60
+
61
+ /* Put aside after the invitation exists, so nothing is held for an
62
+ email that was never actually invited. */
63
+ const chosen = this.chosenEntitlements(email, body?.entitlements);
64
+ if (chosen.length > 0 && this.entitlements) {
65
+ await this.entitlements.offer(chosen, request.user?.id ?? "system");
66
+ }
67
+
68
+ request.reply(201, { ...invitation, entitlements: chosen.length });
69
+ }
70
+
71
+ // Each is an app and an entitlement, with a null app meaning every app -
72
+ // the same scoping a grant already has.
73
+ private chosenEntitlements(email : string, chosen : Array<unknown> | undefined) : Array<EntitlementOffer> {
74
+ if (! Array.isArray(chosen)) return [];
75
+
76
+ return chosen.flatMap(each => {
77
+ const offer = each as { appId? : unknown, entitlementId? : unknown };
78
+ if (typeof offer?.entitlementId !== "string" || ! offer.entitlementId) return [];
79
+
80
+ return [{
81
+ email,
82
+ appId: typeof offer.appId === "string" && offer.appId ? offer.appId : null,
83
+ entitlementId: offer.entitlementId,
84
+ notes: "",
85
+ }];
86
+ });
47
87
  }
48
88
 
49
89
  }
@@ -23,6 +23,14 @@ class AuthenticationMiddleware implements Middleware {
23
23
 
24
24
  private recorded = new Map<string, number>();
25
25
 
26
+ /* Anything owed to this person that could only be arranged against their
27
+ email address, because until now they had no user id - entitlements an
28
+ inviter chose for them before they ever arrived. Injected rather than
29
+ named here: the middleware's business is who someone is, not what they
30
+ are owed. Called where the directory first records them, which is that
31
+ moment, and safe to call again because claiming twice claims nothing. */
32
+ claimWhatIsOwed : (user : User) => Promise<void> = async () => {};
33
+
26
34
  constructor(private authenticator? : Authenticator,
27
35
  private tokenAuthenticator? : TokenAuthenticator,
28
36
  private isOpen : OpenRequestPredicate = () => false,
@@ -96,18 +104,25 @@ class AuthenticationMiddleware implements Middleware {
96
104
  }
97
105
 
98
106
  private async remember(user : User) : Promise<void> {
99
- if (! this.userDirectory) return;
100
-
101
107
  const now = Date.now();
102
108
  const last = this.recorded.get(user.id) ?? 0;
103
109
  if (now - last < DIRECTORY_REFRESH_MS) return;
104
110
 
105
111
  this.recorded.set(user.id, now);
112
+
113
+ if (this.userDirectory) {
114
+ try {
115
+ await this.userDirectory.record(user);
116
+ } catch (error) {
117
+ this.recorded.delete(user.id);
118
+ console.error(`Could not record user "${user.id}" in the directory:`, error);
119
+ }
120
+ }
121
+
106
122
  try {
107
- await this.userDirectory.record(user);
123
+ await this.claimWhatIsOwed(user);
108
124
  } catch (error) {
109
- this.recorded.delete(user.id);
110
- console.error(`Could not record user "${user.id}" in the directory:`, error);
125
+ console.error(`Could not claim what is owed to "${user.id}":`, error);
111
126
  }
112
127
  }
113
128