oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package calls
2
3import (
4 "context"
5 "fmt"
6 "strings"
7)
8
9func normalizeAddress(address string) string {
10 return strings.ToLower(strings.TrimSpace(address))
11}
12
13// allowlist is the team in lone shape (spec §7.1): the addresses that
14// are members. A nil map admits everyone, which is allowed only in
15// development.
16type allowlist struct {
17 addrs map[string]struct{}
18}
19
20func (a allowlist) allows(address string) bool {
21 if a.addrs == nil {
22 return true
23 }
24 _, ok := a.addrs[normalizeAddress(address)]
25 return ok
26}
27
28func resolveAllowlist(cfg Config) (allowlist, error) {
29 addrs := make(map[string]struct{})
30 for _, addr := range cfg.AllowedAddresses {
31 addr = normalizeAddress(addr)
32 if addr == "" {
33 continue
34 }
35 addrs[addr] = struct{}{}
36 }
37 if len(addrs) > 0 {
38 return allowlist{addrs: addrs}, nil
39 }
40 if cfg.development() {
41 return allowlist{}, nil
42 }
43 return allowlist{}, fmt.Errorf("calls: $CALLS_ALLOWED_ADDRESSES is empty and Env is %q, not %q; refusing to start with no team", cfg.Env, EnvDevelopment)
44}
45
46// Standing is a person's entry authority toward a meeting (spec §2.3).
47// Computed, never stored.
48type Standing int
49
50const (
51 StandingNone Standing = iota
52 StandingGuest
53 StandingMember
54)
55
56func (s Standing) String() string {
57 switch s {
58 case StandingMember:
59 return "member"
60 case StandingGuest:
61 return "guest"
62 }
63 return "none"
64}
65
66// allowNow is the team as it stands right now. It is read through an
67// atomic rather than straight off a.cfg because TestHandle.SetAllowlist
68// swaps it while it is being read — by request handlers on their own
69// goroutines, which is what the race detector actually catches here, and
70// by the hub's tick when a test drives one. A member dropped from the
71// team mid-call is what spec §2.4's tick has to notice, and there is no
72// other way to make that happen in a test without rewriting an
73// environment variable and restarting the process.
74// Production never calls the setter, so this is one atomic load on a
75// path that already does a database read.
76func (a *app) allowNow() allowlist {
77 if l := a.allow.Load(); l != nil {
78 return *l
79 }
80 return a.cfg.allow
81}
82
83// isMember is the lone-shape roster read: the allowlist.
84func (a *app) isMember(p Person) bool {
85 l := a.allowNow()
86 return !p.Anonymous && l.addrs != nil && l.allows(p.Email)
87}
88
89func (a *app) standingFor(ctx context.Context, p Person, m Meeting) (Standing, error) {
90 if p.Anonymous && m.RequireSignin {
91 return StandingNone, nil
92 }
93 if a.isMember(p) {
94 return StandingMember, nil
95 }
96 g, ok, err := a.store.grantFor(ctx, m.ID, p.ID)
97 if err != nil {
98 return StandingNone, err
99 }
100 if ok && g.ExpiresAt.After(a.store.now()) {
101 return StandingGuest, nil
102 }
103 return StandingNone, nil
104}
105