package calls import ( "context" "fmt" "strings" ) func normalizeAddress(address string) string { return strings.ToLower(strings.TrimSpace(address)) } // allowlist is the team in lone shape (spec §7.1): the addresses that // are members. A nil map admits everyone, which is allowed only in // development. type allowlist struct { addrs map[string]struct{} } func (a allowlist) allows(address string) bool { if a.addrs == nil { return true } _, ok := a.addrs[normalizeAddress(address)] return ok } func resolveAllowlist(cfg Config) (allowlist, error) { addrs := make(map[string]struct{}) for _, addr := range cfg.AllowedAddresses { addr = normalizeAddress(addr) if addr == "" { continue } addrs[addr] = struct{}{} } if len(addrs) > 0 { return allowlist{addrs: addrs}, nil } if cfg.development() { return allowlist{}, nil } 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) } // Standing is a person's entry authority toward a meeting (spec §2.3). // Computed, never stored. type Standing int const ( StandingNone Standing = iota StandingGuest StandingMember ) func (s Standing) String() string { switch s { case StandingMember: return "member" case StandingGuest: return "guest" } return "none" } // allowNow is the team as it stands right now. It is read through an // atomic rather than straight off a.cfg because TestHandle.SetAllowlist // swaps it while it is being read — by request handlers on their own // goroutines, which is what the race detector actually catches here, and // by the hub's tick when a test drives one. A member dropped from the // team mid-call is what spec §2.4's tick has to notice, and there is no // other way to make that happen in a test without rewriting an // environment variable and restarting the process. // Production never calls the setter, so this is one atomic load on a // path that already does a database read. func (a *app) allowNow() allowlist { if l := a.allow.Load(); l != nil { return *l } return a.cfg.allow } // isMember is the lone-shape roster read: the allowlist. func (a *app) isMember(p Person) bool { l := a.allowNow() return !p.Anonymous && l.addrs != nil && l.allows(p.Email) } func (a *app) standingFor(ctx context.Context, p Person, m Meeting) (Standing, error) { if p.Anonymous && m.RequireSignin { return StandingNone, nil } if a.isMember(p) { return StandingMember, nil } g, ok, err := a.store.grantFor(ctx, m.ID, p.ID) if err != nil { return StandingNone, err } if ok && g.ExpiresAt.After(a.store.now()) { return StandingGuest, nil } return StandingNone, nil }