Documentation
¶
Overview ¶
Package extaccount is the accounts the platform creates for people outside the organization (#2092).
An external person does not come from the identity provider. An administrator creates the account with an email, a name, an external persona and an optional expiry; the platform emails an invitation holding a one-time sign-in link, and every later sign-in is another emailed link the person asks for on the sign-in screen. Claiming a link opens a portal session for the account, signed like any other browser session, whose user id is UserIDPrefix and the account id. Holding the mailbox is the proof of identity: there is no password.
A session is a signed cookie, so it cannot be recalled. What makes Suspend, Remove and expiry take effect at once is Admit: the portal authenticator asks it on every request an external session makes, and it answers with the persona's current roles only while the account is active.
The first time the same address signs in through the identity provider the account converts: it is closed, its links die, and the person is internal from then on. An account is never created for an address the platform already knows from the identity provider.
Index ¶
- Constants
- Variables
- func AccountID(userID string) (string, bool)
- type Account
- type Config
- type Mailer
- type NewAccount
- type Personas
- type PostgresStore
- func (s *PostgresStore) ClaimLink(ctx context.Context, tokenHash string, now time.Time) (accountID string, claimed bool, err error)
- func (s *PostgresStore) Convert(ctx context.Context, email string, at time.Time) (bool, error)
- func (s *PostgresStore) CountLinksSince(ctx context.Context, accountID string, since time.Time) (int, error)
- func (s *PostgresStore) CountSharesTo(ctx context.Context, email string) (int, error)
- func (s *PostgresStore) Delete(ctx context.Context, id string) error
- func (s *PostgresStore) Get(ctx context.Context, id string) (*Account, error)
- func (s *PostgresStore) GetByEmail(ctx context.Context, email string) (*Account, error)
- func (s *PostgresStore) Insert(ctx context.Context, a Account) error
- func (s *PostgresStore) InsertLink(ctx context.Context, tokenHash, accountID string, expiresAt time.Time) error
- func (s *PostgresStore) List(ctx context.Context) ([]Account, error)
- func (s *PostgresStore) RevokeSharesTo(ctx context.Context, email string) error
- func (s *PostgresStore) SetExpiry(ctx context.Context, id string, expiresAt *time.Time) error
- func (s *PostgresStore) SetSuspended(ctx context.Context, id string, at *time.Time) error
- func (s *PostgresStore) TouchSignIn(ctx context.Context, id string, at time.Time) error
- type Service
- func (s *Service) Admit(ctx context.Context, userID string, roles []string) ([]string, bool)
- func (s *Service) Claim(ctx context.Context, w http.ResponseWriter, token string) (Account, error)
- func (s *Service) ConvertOnIdentityProviderSignIn(ctx context.Context, email string)
- func (s *Service) Create(ctx context.Context, n NewAccount, by string) (Account, error)
- func (s *Service) Extend(ctx context.Context, id string, expiresAt *time.Time) error
- func (s *Service) Get(ctx context.Context, id string) (*Account, error)
- func (s *Service) IsExternal(ctx context.Context, email string) bool
- func (s *Service) List(ctx context.Context) ([]Account, error)
- func (s *Service) Remove(ctx context.Context, id string) error
- func (s *Service) RequestLink(ctx context.Context, email string)
- func (s *Service) Resume(ctx context.Context, id string) error
- func (s *Service) Suspend(ctx context.Context, id string) error
- type Settings
- type Store
Constants ¶
const ( DefaultSessionHours = 12 DefaultInviteHours = 7 * 24 DefaultLinkMinutes = 15 )
Defaults of Settings: a session as long as a share guest's, an invitation that waits a week, and a sign-in link short-lived like a guest link.
const ClaimPath = "/portal/auth/email-link/claim"
ClaimPath is the route a sign-in link opens.
const SettingsSection = "external_accounts"
SettingsSection is the platform_settings section holding Settings, which administrators edit under Admin > Settings > External accounts.
const UserIDPrefix = "ext:"
UserIDPrefix starts the user id of every external account's session.
Variables ¶
var ( // ErrNotFound is an account that does not exist. ErrNotFound = errors.New("external account not found") // ErrExists is an address that already has an account. ErrExists = errors.New("an external account already exists for that address") // ErrInternal is an address the platform already knows from the // identity provider. ErrInternal = errors.New("that address already signs in through the identity provider") // ErrPersona is a persona that is not external, or that the persona's // own roles do not resolve to. ErrPersona = errors.New("the persona is not an external persona its roles resolve to") // ErrInvalid is input that fails validation; it wraps the reason. ErrInvalid = errors.New("invalid external account") // ErrLink is a sign-in link that is unknown, used, expired, or for an // account that may not sign in. ErrLink = errors.New("that sign-in link cannot be used") )
Errors a caller maps to a status.
Functions ¶
Types ¶
type Account ¶
type Account struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Persona string `json:"persona"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
SuspendedAt *time.Time `json:"suspended_at,omitempty"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
LastSignInAt *time.Time `json:"last_sign_in_at,omitempty"`
// ConvertedAt is when the person first signed in through the identity
// provider, which closed the account.
ConvertedAt *time.Time `json:"converted_at,omitempty"`
}
Account is one external person.
type Config ¶
type Config struct {
Store Store
Personas Personas
// StoredPersona reads a persona from the persona store, which another
// replica writes before this one's registry hears of it; nil reads the
// registry alone. Create asks it, so an account can be created the moment
// its persona is saved on any replica.
StoredPersona func(ctx context.Context, name string) (*persona.Persona, bool)
// Settings reads the stored configuration; nil uses Defaults.
Settings func(ctx context.Context) Settings
// KnownInternal reports whether an address already signs in through
// the identity provider; nil knows none.
KnownInternal func(ctx context.Context, email string) (bool, error)
// Mail sends the links; nil sends none, and Create still stores the
// account.
Mail Mailer
// is removed so no guest link reopens what was shared.
RevokeShares func(ctx context.Context, email string) error
// BaseURL is the portal's public origin a link is built on.
BaseURL string
// Cookie is the browser-session cookie a claimed link sets; its TTL is
// replaced by the session setting.
Cookie browsersession.CookieConfig
// Now is the clock; nil is time.Now.
Now func() time.Time
}
Config is what the service is built from.
type Mailer ¶
type Mailer func(ctx context.Context, to, name, link string, invitation bool, valid time.Duration) error
Mailer sends a sign-in link: the invitation when invitation is true, and a link the person asked for otherwise. valid is how long the link works.
type NewAccount ¶
type NewAccount struct {
Email string `json:"email"`
Name string `json:"name"`
Persona string `json:"persona"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
NewAccount is what an administrator gives to create an account.
type Personas ¶
type Personas interface {
Get(name string) (*persona.Persona, bool)
GetForRoles(roles []string) (*persona.Persona, bool)
}
Personas is the persona registry the service resolves an account's persona through.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore is the Store over external_accounts and external_signin_links.
func NewPostgresStore ¶
func NewPostgresStore(db *sql.DB) *PostgresStore
NewPostgresStore returns the store over db.
func (*PostgresStore) ClaimLink ¶
func (s *PostgresStore) ClaimLink(ctx context.Context, tokenHash string, now time.Time) (accountID string, claimed bool, err error)
ClaimLink claims a live link and returns its account id; claimed is false for a link that is unknown, used or expired.
func (*PostgresStore) CountLinksSince ¶
func (s *PostgresStore) CountLinksSince(ctx context.Context, accountID string, since time.Time) (int, error)
CountLinksSince counts the links issued for an account since a moment.
func (*PostgresStore) CountSharesTo ¶
CountSharesTo counts the live shares to an address.
func (*PostgresStore) Delete ¶
func (s *PostgresStore) Delete(ctx context.Context, id string) error
Delete removes the account and its links.
func (*PostgresStore) GetByEmail ¶
GetByEmail returns the account for an address, compared case-insensitively.
func (*PostgresStore) Insert ¶
func (s *PostgresStore) Insert(ctx context.Context, a Account) error
Insert stores a new account; an address with one already is ErrExists.
func (*PostgresStore) InsertLink ¶
func (s *PostgresStore) InsertLink(ctx context.Context, tokenHash, accountID string, expiresAt time.Time) error
InsertLink stores a link's hash.
func (*PostgresStore) List ¶
func (s *PostgresStore) List(ctx context.Context) ([]Account, error)
List returns every account, newest first.
func (*PostgresStore) RevokeSharesTo ¶
func (s *PostgresStore) RevokeSharesTo(ctx context.Context, email string) error
RevokeSharesTo revokes every share to an address.
func (*PostgresStore) SetSuspended ¶
SetSuspended suspends the account at at, or resumes it when at is nil.
func (*PostgresStore) TouchSignIn ¶
TouchSignIn records a sign-in.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service manages external accounts.
func (*Service) Admit ¶
Admit answers for one request an external session makes: the roles of the account's persona as they stand now, while the account is active, and false otherwise. A user id that names no external account is not this service's to judge and is admitted with the roles it carried.
func (*Service) ConvertOnIdentityProviderSignIn ¶
ConvertOnIdentityProviderSignIn closes the account for an address that has just signed in through the identity provider. Best effort: the sign-in does not depend on it.
func (*Service) IsExternal ¶
IsExternal reports whether an address has an external account, open or not. The share guest path asks it: such an address signs in, or is refused, and is never sent a guest link.
func (*Service) RequestLink ¶
RequestLink emails a sign-in link to an active account's address. It answers the same whether or not the address has one, so the sign-in screen tells nobody which addresses do; a mailbox is sent at most a few links an hour.
type Settings ¶
type Settings struct {
// SessionHours is how long a signed-in session lasts.
SessionHours int `json:"session_hours" example:"12"`
// InviteHours is how long the invitation email's link can be used.
InviteHours int `json:"invite_hours" example:"168"`
// LinkMinutes is how long a link asked for on the sign-in screen can be
// used.
LinkMinutes int `json:"link_minutes" example:"15"`
}
Settings is the stored configuration.
func Defaults ¶
func Defaults() Settings
Defaults is the configuration before an administrator writes the section.
type Store ¶
type Store interface {
Insert(ctx context.Context, a Account) error
Get(ctx context.Context, id string) (*Account, error)
GetByEmail(ctx context.Context, email string) (*Account, error)
List(ctx context.Context) ([]Account, error)
SetExpiry(ctx context.Context, id string, expiresAt *time.Time) error
SetSuspended(ctx context.Context, id string, at *time.Time) error
Delete(ctx context.Context, id string) error
// Convert closes the account for the address, if it has an open one,
// and reports whether it did.
Convert(ctx context.Context, email string, at time.Time) (bool, error)
TouchSignIn(ctx context.Context, id string, at time.Time) error
InsertLink(ctx context.Context, tokenHash, accountID string, expiresAt time.Time) error
// ClaimLink marks an unclaimed, unexpired link claimed and returns its
// account, in one statement so a link is claimed once on any replica.
ClaimLink(ctx context.Context, tokenHash string, now time.Time) (string, bool, error)
CountLinksSince(ctx context.Context, accountID string, since time.Time) (int, error)
}
Store keeps the accounts and their sign-in links.