Documentation
¶
Overview ¶
Package signupgate keeps platform sign-up invites and implements the systemauth.SignupGate that enforces them. With the gate installed a deployment creates accounts only for people who are invited; people who already have an account sign in as before.
There are two kinds of invite. An email invite (Store) names an address: the person proves control of it by signing in through the upstream provider, and the gate compares that verified address with the invites. An invite code (CodeStore) is shared as a link and admits up to its use limit, whoever holds it; operators create codes for cohorts, and members create single-use codes from their allowance. Either way a verified email is required.
Index ¶
- Constants
- Variables
- func CodeLink(signupURL, code string) string
- func FormatCode(normalized string) string
- func NormalizeCode(raw string) (string, error)
- func NormalizeEmail(raw string) (string, error)
- func WriteWaitlistCSV(out io.Writer, entries []WaitlistEntry) error
- type AddOptions
- type Allowance
- type Code
- type CodeFilter
- type CodeKind
- type CodeOptions
- type CodeStore
- func (s *CodeStore) Allowances(ctx context.Context) ([]Allowance, error)
- func (s *CodeStore) CreateCode(ctx context.Context, opts CodeOptions) (Code, string, error)
- func (s *CodeStore) CreateMemberCode(ctx context.Context, principalID uuid.UUID) (Code, string, error)
- func (s *CodeStore) GetCode(ctx context.Context, id uuid.UUID) (Code, error)
- func (s *CodeStore) ListCodes(ctx context.Context, f CodeFilter) ([]Code, error)
- func (s *CodeStore) Lookup(ctx context.Context, code string) (Code, error)
- func (s *CodeStore) MemberInvites(ctx context.Context, principalID uuid.UUID) (MemberSummary, error)
- func (s *CodeStore) MemberOptions() MemberOptions
- func (s *CodeStore) PrincipalByEmail(ctx context.Context, email string) (uuid.UUID, error)
- func (s *CodeStore) Redeem(ctx context.Context, id, principalID uuid.UUID, email string) error
- func (s *CodeStore) Redemptions(ctx context.Context, f RedemptionFilter) ([]Redemption, error)
- func (s *CodeStore) Release(ctx context.Context, id uuid.UUID) error
- func (s *CodeStore) Reserve(ctx context.Context, code string) (uuid.UUID, error)
- func (s *CodeStore) RevokeCode(ctx context.Context, id uuid.UUID) error
- func (s *CodeStore) RevokeMemberCode(ctx context.Context, principalID, id uuid.UUID) error
- func (s *CodeStore) SetAllowance(ctx context.Context, principalID uuid.UUID, granted *int, note string, ...) error
- func (s *CodeStore) SignupCodeUsable(ctx context.Context, code string) (string, bool, error)
- type Gate
- type GateOption
- type Invite
- type MemberOptions
- type MemberSummary
- type Redemption
- type RedemptionFilter
- type Store
- func (s *Store) Add(ctx context.Context, email string, opts AddOptions) (*Invite, error)
- func (s *Store) Allowed(ctx context.Context, email string) (bool, error)
- func (s *Store) List(ctx context.Context, includeRedeemed bool) ([]Invite, error)
- func (s *Store) Redeem(ctx context.Context, email string, principalID uuid.UUID) error
- func (s *Store) Revoke(ctx context.Context, email string) error
- type Waitlist
- func (w *Waitlist) Invite(ctx context.Context, invites *Store, emails []string, opts AddOptions) ([]Invite, error)
- func (w *Waitlist) Join(ctx context.Context, email, consentVersion, consentText string) error
- func (w *Waitlist) List(ctx context.Context) ([]WaitlistEntry, error)
- func (w *Waitlist) Remove(ctx context.Context, email string) error
- type WaitlistEntry
Constants ¶
const ( CodeActive = "active" CodeUsedUp = "used_up" CodeExpired = "expired" CodeRevoked = "revoked" )
Code statuses, as returned by Code.Status.
const ( EventInviteCreated = "signup_invite.created" EventInviteRevoked = "signup_invite.revoked" EventCodeCreated = "signup_code.created" EventCodeRevoked = "signup_code.revoked" EventCodeRedeemed = "signup_code.redeemed" EventAllowanceSet = "signup_allowance.set" EventWaitlistInvited = "signup_waitlist.invited" )
Audit event types for sign-up invites.
const DefaultMemberCodeTTL = 14 * 24 * time.Hour
DefaultMemberCodeTTL is how long a member's invite code works when MemberOptions.CodeTTL is not set.
Variables ¶
var ( // ErrCodeInvalid means a code cannot be used: it is malformed, unknown, // used up, expired or revoked. The reason is not given, so a holder // cannot learn which codes exist. ErrCodeInvalid = errors.New("signupgate: invite code is not valid") // ErrCodeNotFound means no code has the given ID. ErrCodeNotFound = errors.New("signupgate: no such invite code") )
var ( // ErrInvalidEmail means the address is not a plain email address. ErrInvalidEmail = errors.New("signupgate: not a plain email address") // ErrAlreadyRedeemed means an account was already created with the // invite. Revoke it to invite the address again. ErrAlreadyRedeemed = errors.New("signupgate: invite already redeemed") // ErrNotFound means no invite exists for the address. ErrNotFound = errors.New("signupgate: no invite for that address") )
var ErrAllowanceExhausted = errors.New("signupgate: no invites left")
ErrAllowanceExhausted means a member already holds as many invite codes as their allowance permits.
var ErrNoAccount = errors.New("signupgate: no account with that email address")
ErrNoAccount means no account has the given email address.
Functions ¶
func CodeLink ¶
CodeLink returns the invite link for a code: the sign-up page URL with the code as its code query parameter.
func FormatCode ¶
FormatCode returns a normalized code in groups of four, as shown to people: XXXX-XXXX-XXXX-XXXX.
func NormalizeCode ¶
NormalizeCode returns the stored form of a code: upper-case, without dashes or spaces, with O read as 0 and I and L read as 1. It returns ErrCodeInvalid for anything that is not a code.
func NormalizeEmail ¶
NormalizeEmail returns the stored form of an address: trimmed and lower-case. A display name, a comment or a missing domain is refused.
func WriteWaitlistCSV ¶
func WriteWaitlistCSV(out io.Writer, entries []WaitlistEntry) error
WriteWaitlistCSV writes entries as CSV with a header row: email, joined and last submitted times (RFC 3339, UTC), the consent version, and when the address was invited (empty until then).
Types ¶
type AddOptions ¶
type AddOptions struct {
// Note is free-text context for operators.
Note string
// InvitedBy labels the operator creating the invite.
InvitedBy string
// InvitedByPrincipal is the person creating the invite through the API.
InvitedByPrincipal *uuid.UUID
// ExpiresAt ends the invite; nil means it does not expire.
ExpiresAt *time.Time
}
AddOptions are the optional parts of a new invite.
type Allowance ¶
type Allowance struct {
PrincipalID uuid.UUID
Email string
// Granted is the allowance an operator set; nil means the default.
Granted *int
// Effective is Granted, or the deployment's default when it is nil.
Effective int
// Held is how many of the member's codes count against it.
Held int
Note string
SetBy *uuid.UUID
UpdatedAt time.Time
}
Allowance is a member's invite allowance as listed to operators.
type Code ¶
type Code struct {
ID uuid.UUID
Kind CodeKind
Hint string // last four characters
Label string
Note string
CreatedByPrincipal *uuid.UUID
CreatedByLabel string
MaxUses int
UseCount int
CreatedAt time.Time
ExpiresAt *time.Time
RevokedAt *time.Time
}
Code is an invite code as listed to operators and members. It never carries the code itself, which is shown only once, when created.
type CodeFilter ¶
type CodeFilter struct {
// Kind keeps codes of one kind.
Kind CodeKind
// Label keeps codes with this label.
Label string
// CreatedBy keeps codes created by this principal.
CreatedBy *uuid.UUID
// ActiveOnly keeps codes that can still be used.
ActiveOnly bool
}
CodeFilter narrows ListCodes. The zero value lists every code.
type CodeOptions ¶
type CodeOptions struct {
// Kind is CodeOperator or CodeMember.
Kind CodeKind
// Label names the cohort, such as "launch-week".
Label string
// Note is free-text context for operators.
Note string
// CreatedByPrincipal is the person creating the code through the API.
CreatedByPrincipal *uuid.UUID
// CreatedByLabel labels an operator creating the code from the command
// line.
CreatedByLabel string
// MaxUses is how many accounts the code may create; at least 1.
MaxUses int
// ExpiresAt ends the code; nil means it does not expire.
ExpiresAt *time.Time
}
CodeOptions describe a new code.
type CodeStore ¶
type CodeStore struct {
// contains filtered or unexported fields
}
CodeStore reads and writes invite codes, their redemptions and member allowances.
func NewCodeStore ¶
func NewCodeStore(client *ent.Client, member MemberOptions) *CodeStore
NewCodeStore returns a CodeStore over client. Member invites use the given options, with defaults for zero fields.
func (*CodeStore) Allowances ¶
Allowances returns the allowances operators have set, with each member's email and held count, ordered by email.
func (*CodeStore) CreateCode ¶
CreateCode creates a code and returns it with the code itself, formatted for people. The code is not stored and cannot be shown again.
func (*CodeStore) CreateMemberCode ¶
func (s *CodeStore) CreateMemberCode(ctx context.Context, principalID uuid.UUID) (Code, string, error)
CreateMemberCode creates a single-use code for a member, expiring after the member code TTL, and returns it with the code itself, shown once. It returns ErrAllowanceExhausted when the member holds their allowance already. The member's allowance row is written first inside the transaction, which locks it, so concurrent requests cannot both pass.
func (*CodeStore) Lookup ¶
Lookup returns the code a person holds, whatever its status, or ErrCodeInvalid when it is malformed or unknown.
func (*CodeStore) MemberInvites ¶
func (s *CodeStore) MemberInvites(ctx context.Context, principalID uuid.UUID) (MemberSummary, error)
MemberInvites returns a member's allowance, how many codes they hold, and their codes.
func (*CodeStore) MemberOptions ¶
func (s *CodeStore) MemberOptions() MemberOptions
MemberOptions returns the member invite settings the store applies.
func (*CodeStore) PrincipalByEmail ¶
PrincipalByEmail returns the principal ID of the account with the email address, or ErrNoAccount.
func (*CodeStore) Redeem ¶
Redeem records that the account was created with a code reserved by Reserve.
func (*CodeStore) Redemptions ¶
func (s *CodeStore) Redemptions(ctx context.Context, f RedemptionFilter) ([]Redemption, error)
Redemptions returns accounts created with codes, newest first.
func (*CodeStore) Release ¶
Release gives back a use taken by Reserve, when the account it was reserved for was not created.
func (*CodeStore) Reserve ¶
Reserve takes one use of a code and returns the code's ID. It is one conditional update, so a code with N uses is reserved at most N times however many sign-ups race for it. It returns ErrCodeInvalid when the code cannot be used, for any reason.
func (*CodeStore) RevokeCode ¶
RevokeCode revokes a code, so it admits nobody else. Accounts already created with it are not touched. Revoking a revoked code changes nothing. It returns ErrCodeNotFound when there is no such code.
func (*CodeStore) RevokeMemberCode ¶
RevokeMemberCode revokes one of a member's own codes. A code that was never used gives its slot back. It returns ErrCodeNotFound for a code the member did not create.
type Gate ¶
type Gate struct {
// contains filtered or unexported fields
}
Gate is the systemauth.SignupGate over a Store and, optionally, a CodeStore. A sign-up is allowed when the login's verified email holds a pending invite, or else when the person brought an invite code with a use left. An email invite wins: a person invited by address does not use up a code they also hold.
func NewGate ¶
func NewGate(store *Store, opts ...GateOption) *Gate
NewGate returns a gate that allows a sign-up when the login's verified email holds a pending invite, or with WithCodes, when it brings a usable invite code.
func (*Gate) AllowSignup ¶
func (g *Gate) AllowSignup(ctx context.Context, login systemauth.ExternalLogin) error
AllowSignup implements systemauth.SignupGate. Only a verified email can sign up: an unverified address is never trusted, and the directory refuses to create an account for one anyway. A code is reserved here, one use, so a code's limit holds however many sign-ups race for it.
func (*Gate) SignedUp ¶
func (g *Gate) SignedUp(ctx context.Context, login systemauth.ExternalLogin, principalID uuid.UUID) error
SignedUp implements systemauth.SignupGate by consuming what admitted the sign-up: the email invite, or the reserved code use.
func (*Gate) SignupAborted ¶
func (g *Gate) SignupAborted(ctx context.Context, login systemauth.ExternalLogin)
SignupAborted implements systemauth.SignupGateAborter by giving back a reserved code use when the account was not created, for example because the person has yet to answer the sign-up age question.
type GateOption ¶
type GateOption func(*Gate)
GateOption configures a Gate.
func WithCodes ¶
func WithCodes(codes *CodeStore) GateOption
WithCodes lets the gate admit people who bring an invite code from codes.
func WithGateAudit ¶
func WithGateAudit(emitter audit.Emitter) GateOption
WithGateAudit emits signup_code.redeemed through emitter when a code admits an account.
type Invite ¶
type Invite struct {
Email string
Note string
InvitedBy string
// InvitedByPrincipal is the person who created the invite through the
// API; nil for invites created from the command line.
InvitedByPrincipal *uuid.UUID
CreatedAt time.Time
ExpiresAt *time.Time
RedeemedAt *time.Time
RedeemedBy *uuid.UUID
}
Invite is a sign-up invite as listed to operators.
type MemberOptions ¶
type MemberOptions struct {
// DefaultAllowance is how many invite codes a member may hold when an
// operator has not set their allowance. The default is 0: members invite
// nobody until an operator allows it.
DefaultAllowance int
// CodeTTL is how long a member's invite code works. Zero means
// DefaultMemberCodeTTL.
CodeTTL time.Duration
}
MemberOptions configure member invites.
type MemberSummary ¶
type MemberSummary struct {
Allowance int
Held int
Remaining int
// Codes are the member's codes, newest first, whatever their status.
Codes []Code
// CodeTTL is how long a new member code works.
CodeTTL time.Duration
}
MemberSummary is what a member sees of their own invites.
type Redemption ¶
type Redemption struct {
ID uuid.UUID
CodeID uuid.UUID
PrincipalID uuid.UUID
Email string
CreatedAt time.Time
// The code's kind, label, hint and creator, for attribution.
CodeKind CodeKind
CodeLabel string
CodeHint string
CodeCreatedBy *uuid.UUID
CodeCreatedByLabel string
}
Redemption is an account created with a code.
type RedemptionFilter ¶
type RedemptionFilter struct {
// CodeID keeps accounts created with this code.
CodeID *uuid.UUID
// CreatedBy keeps accounts created with codes this principal created.
CreatedBy *uuid.UUID
// Label keeps accounts created with codes carrying this label.
Label string
}
RedemptionFilter narrows Redemptions. The zero value lists every redemption.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes invites.
func (*Store) Add ¶
Add creates an invite, or updates the note, label and expiry of a pending one. It returns ErrAlreadyRedeemed for an address that already has an account created through its invite.
func (*Store) Allowed ¶
Allowed reports whether the address holds an invite that is neither redeemed nor expired.
func (*Store) List ¶
List returns invites ordered by address. Redeemed invites are included only when includeRedeemed is set.
func (*Store) Redeem ¶
Redeem marks the invite for an address as used by the principal. It does nothing, and reports no error, when the invite was already used, so a repeated call is harmless.
type Waitlist ¶
type Waitlist struct {
// contains filtered or unexported fields
}
Waitlist stores the addresses that asked to be notified while a deployment is invite-only. It sends nothing itself: operators export it.
func NewWaitlist ¶
NewWaitlist returns a Waitlist over client.
func (*Waitlist) Invite ¶
func (w *Waitlist) Invite(ctx context.Context, invites *Store, emails []string, opts AddOptions) ([]Invite, error)
Invite creates an email invite in invites for each address on the waitlist and marks the entry invited; the entry stays listed. It returns the invites created and an error joining the failures, such as an address that is not listed or one whose invite was already redeemed.
func (*Waitlist) Join ¶
Join adds an address, or refreshes the consent recorded for one already listed. Either way the caller learns nothing about whether it was listed before, so a public form built on it cannot be used to probe addresses.
type WaitlistEntry ¶
type WaitlistEntry struct {
Email string
ConsentVersion string
ConsentText string
// CreatedAt is when the address first joined; UpdatedAt when it was
// last submitted.
CreatedAt time.Time
UpdatedAt time.Time
// InvitedAt is when an operator invited the address from the waitlist;
// nil until then.
InvitedAt *time.Time
}
WaitlistEntry is an address waiting to be told that sign-up is open.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package signupapi serves the HTTP API for invite-gated sign-up: operator routes for email invites, invite codes, the waitlist, member allowances and the accounts invites admitted, and member routes for a person's own invite codes.
|
Package signupapi serves the HTTP API for invite-gated sign-up: operator routes for email invites, invite codes, the waitlist, member allowances and the accounts invites admitted, and member routes for a person's own invite codes. |
|
Package signupcmd provides the operator commands for invite-only sign-up as cobra commands a host CLI mounts: "invite" (add, list, revoke), "code" (create, list, revoke, joined), "allowance" (set, list) and "waitlist" (list, invite, remove).
|
Package signupcmd provides the operator commands for invite-only sign-up as cobra commands a host CLI mounts: "invite" (add, list, revoke), "code" (create, list, revoke, joined), "allowance" (set, list) and "waitlist" (list, invite, remove). |