Help us shape the future of Go! Take the 2026 Go Developer Survey

extaccount

package
v1.149.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
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.

View Source
const ClaimPath = "/portal/auth/email-link/claim"

ClaimPath is the route a sign-in link opens.

View Source
const SettingsSection = "external_accounts"

SettingsSection is the platform_settings section holding Settings, which administrators edit under Admin > Settings > External accounts.

View Source
const UserIDPrefix = "ext:"

UserIDPrefix starts the user id of every external account's session.

Variables

View Source
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

func AccountID

func AccountID(userID string) (string, bool)

AccountID is the account id a user id names, and whether it names one.

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.

func (Account) Active

func (a Account) Active(now time.Time) bool

Active reports whether the account may sign in and be served at now.

func (Account) Status

func (a Account) Status(now time.Time) string

Status is the one word the admin list shows for the account.

func (Account) UserID

func (a Account) UserID() string

UserID is the user id the account's sessions carry.

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
	// RevokeShares revokes every share to an address, run when its account
	// 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 (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) Convert

func (s *PostgresStore) Convert(ctx context.Context, email string, at time.Time) (bool, error)

Convert closes the open account for an address and kills its links.

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

func (s *PostgresStore) CountSharesTo(ctx context.Context, email string) (int, error)

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) Get

func (s *PostgresStore) Get(ctx context.Context, id string) (*Account, error)

Get returns the account with id, or ErrNotFound.

func (*PostgresStore) GetByEmail

func (s *PostgresStore) GetByEmail(ctx context.Context, email string) (*Account, error)

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 (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) SetExpiry

func (s *PostgresStore) SetExpiry(ctx context.Context, id string, expiresAt *time.Time) error

SetExpiry sets or clears the account's expiry.

func (*PostgresStore) SetSuspended

func (s *PostgresStore) SetSuspended(ctx context.Context, id string, at *time.Time) error

SetSuspended suspends the account at at, or resumes it when at is nil.

func (*PostgresStore) TouchSignIn

func (s *PostgresStore) TouchSignIn(ctx context.Context, id string, at time.Time) error

TouchSignIn records a sign-in.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service manages external accounts.

func New

func New(cfg Config) *Service

New returns the service.

func (*Service) Admit

func (s *Service) Admit(ctx context.Context, userID string, roles []string) ([]string, bool)

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) Claim

func (s *Service) Claim(ctx context.Context, w http.ResponseWriter, token string) (Account, error)

Claim spends a sign-in link and sets the session cookie for its account.

func (*Service) ConvertOnIdentityProviderSignIn

func (s *Service) ConvertOnIdentityProviderSignIn(ctx context.Context, email string)

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) Create

func (s *Service) Create(ctx context.Context, n NewAccount, by string) (Account, error)

Create stores a new account and emails its invitation.

func (*Service) Extend

func (s *Service) Extend(ctx context.Context, id string, expiresAt *time.Time) error

Extend sets the account's expiry; nil removes it.

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (*Account, error)

Get returns one account.

func (*Service) IsExternal

func (s *Service) IsExternal(ctx context.Context, email string) bool

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) List

func (s *Service) List(ctx context.Context) ([]Account, error)

List returns every account.

func (*Service) Remove

func (s *Service) Remove(ctx context.Context, id string) error

Remove deletes the account and revokes every share to its address.

func (s *Service) RequestLink(ctx context.Context, email string)

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.

func (*Service) Resume

func (s *Service) Resume(ctx context.Context, id string) error

Resume lets a suspended account sign in again.

func (*Service) Suspend

func (s *Service) Suspend(ctx context.Context, id string) error

Suspend stops the account signing in and ends its sessions on their next request.

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.

func (Settings) Invite

func (s Settings) Invite() time.Duration

Invite is the invitation link's lifetime.

func (s Settings) Link() time.Duration

Link is a requested link's lifetime.

func (Settings) Session

func (s Settings) Session() time.Duration

Session is the session length.

func (Settings) Validate

func (s Settings) Validate() error

Validate refuses a duration out of range.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL