Skip to content
Snippets Groups Projects
Select Git revision
  • 6b04d2d67aa72df2da29298c023a3be5150e123c
  • master default
  • renovate/golang.org-x-crypto-0.x
  • renovate/go-1.x
  • renovate/golang.org-x-sync-0.x
  • renovate/opentelemetry-go-monorepo
  • renovate/github.com-go-webauthn-webauthn-0.x
  • renovate/github.com-mattn-go-sqlite3-1.x
  • renovate/github.com-go-ldap-ldap-v3-3.x
  • renovate/github.com-prometheus-client_golang-1.x
  • renovate/github.com-google-go-cmp-0.x
  • renovate/github.com-lunixbochs-struc-digest
  • renovate/github.com-duo-labs-webauthn-digest
13 results

dialer_legacy.go

Blame
    • ale's avatar
      c165311f
      Let the configuration override connection and max request timeouts · c165311f
      ale authored
      Add the 'connect_timeout' and 'request_max_timeout' configuration
      fields, to control respectively the initial connection timeout, and
      the maximum time for each individual request. This allows fine-tuning
      of the expected performance of specific backends, for example to let
      optional backends fail fast.
      c165311f
      History
      Let the configuration override connection and max request timeouts
      ale authored
      Add the 'connect_timeout' and 'request_max_timeout' configuration
      fields, to control respectively the initial connection timeout, and
      the maximum time for each individual request. This allows fine-tuning
      of the expected performance of specific backends, for example to let
      optional backends fail fast.
    actions.go 18.57 KiB
    package accountserver
    
    import (
    	"context"
    	"crypto/rand"
    	"encoding/base64"
    	"errors"
    
    	"git.autistici.org/ai3/go-common/pwhash"
    	"git.autistici.org/id/go-sso"
    	"git.autistici.org/id/keystore/userenckey"
    	"github.com/pquerna/otp/totp"
    )
    
    // Backend user database interface.
    //
    // We are using a transactional interface even if the actual backend
    // (LDAP) does not support atomic transactions, just so it is easy to
    // add more backends in the future (like SQL).
    type Backend interface {
    	NewTransaction() (TX, error)
    }
    
    // TX represents a single transaction with the backend and offers a
    // high-level data management abstraction.
    //
    // All methods share similar semantics: Get methods will return nil if
    // the requested object is not found, and only return an error in case
    // of trouble reaching the backend itself.
    //
    // The backend enforces strict public/private data separation by
    // having Get methods return public objects (as defined in types.go),
    // and using specialized methods to modify the private
    // (authentication-related) attributes.
    //
    // We might add more sophisticated resource query methods later, as
    // admin-level functionality.
    //
    type TX interface {
    	Commit(context.Context) error
    
    	GetResource(context.Context, ResourceID) (*Resource, error)
    	UpdateResource(context.Context, *Resource) error
    	SetResourcePassword(context.Context, *Resource, string) error
    
    	GetUser(context.Context, string) (*User, error)
    	SetUserPassword(context.Context, *User, string) error
    	GetUserEncryptionKeys(context.Context, *User) ([]*UserEncryptionKey, error)
    	SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error
    	SetUserEncryptionPublicKey(context.Context, *User, []byte) error
    	SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error
    	DeleteApplicationSpecificPassword(context.Context, *User, string) error
    	SetUserTOTPSecret(context.Context, *User, string) error
    	DeleteUserTOTPSecret(context.Context, *User) error
    
    	HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
    }
    
    // FindResourceRequest contains parameters for searching a resource by name.
    type FindResourceRequest struct {
    	Type string
    	Name string
    }
    
    // AccountService implements the business logic and high-level
    // functionality of the user accounts management service.
    type AccountService struct {
    	validator     sso.Validator
    	ssoService    string
    	ssoGroups     []string
    	ssoAdminGroup string
    
    	passwordValidator   ValidatorFunc
    	dataValidators      map[string]ValidatorFunc
    	adminDataValidators map[string]ValidatorFunc
    }
    
    func NewAccountService(backend Backend, config *Config) (*AccountService, error) {
    	ssoValidator, err := config.ssoValidator()
    	if err != nil {
    		return nil, err
    	}
    
    	return newAccountServiceWithSSO(backend, config, ssoValidator), nil
    }
    
    func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) *AccountService {
    	s := &AccountService{
    		validator:     ssoValidator,
    		ssoService:    config.SSO.Service,
    		ssoGroups:     config.SSO.Groups,
    		ssoAdminGroup: config.SSO.AdminGroup,
    	}
    
    	validationConfig := config.validationConfig()
    	domainBackend := config.domainBackend()
    	s.dataValidators = map[string]ValidatorFunc{
    		ResourceTypeEmail:       validHostedEmail(validationConfig, domainBackend, backend),
    		ResourceTypeMailingList: validHostedMailingList(validationConfig, domainBackend, backend),
    	}
    	s.passwordValidator = validPassword(validationConfig)
    
    	return s
    }
    
    func (s *AccountService) isAdmin(tkt *sso.Ticket) bool {
    	for _, g := range tkt.Groups {
    		if g == s.ssoAdminGroup {
    			return true
    		}
    	}
    	return false
    }
    
    var (
    	ErrUnauthorized     = errors.New("unauthorized")
    	ErrUserNotFound     = errors.New("user not found")
    	ErrResourceNotFound = errors.New("resource not found")
    )
    
    func (s *AccountService) authorizeAdmin(ctx context.Context, tx TX, username, ssoToken string) (*User, error) {
    	// Validate the SSO ticket.
    	tkt, err := s.validator.Validate(ssoToken, "", s.ssoService, s.ssoGroups)
    	if err != nil {
    		return nil, newAuthError(err)
    	}
    
    	// Requests are allowed if the SSO ticket corresponds to an admin, or if
    	// it identifies the same user that we're querying.
    	if !s.isAdmin(tkt) {
    		return nil, newAuthError(ErrUnauthorized)
    	}
    
    	user, err := tx.GetUser(ctx, username)
    	if err != nil {
    		return nil, newBackendError(err)
    	}
    	if user == nil {
    		return nil, ErrUserNotFound
    	}
    	return user, nil
    }
    
    func (s *AccountService) authorizeUser(ctx context.Context, tx TX, username, ssoToken string) (*User, error) {
    	// First, check that the username matches the SSO ticket
    	// username (or that the SSO ticket has admin permissions).
    	tkt, err := s.validator.Validate(ssoToken, "", s.ssoService, s.ssoGroups)
    	if err != nil {
    		return nil, newAuthError(err)
    	}
    
    	// Requests are allowed if the SSO ticket corresponds to an admin, or if
    	// it identifies the same user that we're querying.
    	if !s.isAdmin(tkt) && tkt.User != username {
    		return nil, newAuthError(ErrUnauthorized)
    	}
    
    	user, err := tx.GetUser(ctx, username)
    	if err != nil {
    		return nil, newBackendError(err)
    	}
    	if user == nil {
    		return nil, ErrUserNotFound
    	}
    	return user, nil
    }
    
    // Extended version of authorizeUser that also directly checks the
    // user password. Used for account-privileged operations related to
    // credential manipulation.
    func (s *AccountService) authorizeUserWithPassword(ctx context.Context, tx TX, username, ssoToken, password string) (*User, error) {
    	// TODO: call out to the auth-server?
    	return s.authorizeUser(ctx, tx, username, ssoToken)
    }
    
    // RequestBase contains parameters shared by all request types.
    type RequestBase struct {
    	Username string `json:"username"`
    	SSO      string `json:"sso"`
    }
    
    // PrivilegedRequestBase extends RequestBase with the user password,
    // for privileged endpoints.
    type PrivilegedRequestBase struct {
    	RequestBase
    	CurPassword string `json:"cur_password"`
    }
    
    type GetUserRequest struct {
    	RequestBase
    }
    
    // GetUser returns public information about a user.
    func (s *AccountService) GetUser(ctx context.Context, tx TX, req *GetUserRequest) (*User, error) {
    	return s.authorizeUser(ctx, tx, req.Username, req.SSO)
    }
    
    // setResourceStatus sets the status of a single resource (shared
    // logic between enable / disable resource methods).
    func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, username string, resourceID ResourceID, status string) error {
    	r, err := tx.GetResource(ctx, resourceID)
    	if err != nil {
    		return newBackendError(err)
    	}
    	if r == nil {
    		return ErrResourceNotFound
    	}
    	r.Status = status
    	if err := tx.UpdateResource(ctx, r); err != nil {
    		return newBackendError(err)
    	}
    	return nil
    }
    
    type DisableResourceRequest struct {
    	RequestBase
    	ResourceID ResourceID `json:"resource_id"`
    }
    
    // DisableResource disables a resource belonging to the user.
    func (s *AccountService) DisableResource(ctx context.Context, tx TX, req *DisableResourceRequest) error {
    	if _, err := s.authorizeUser(ctx, tx, req.Username, req.SSO); err != nil {
    		return err
    	}
    	return s.setResourceStatus(ctx, tx, req.Username, req.ResourceID, ResourceStatusInactive)
    }
    
    type EnableResourceRequest struct {
    	RequestBase
    	ResourceID ResourceID `json:"resource_id"`
    }
    
    // EnableResource enables a resource belonging to the user.
    func (s *AccountService) EnableResource(ctx context.Context, tx TX, req *EnableResourceRequest) error {
    	if _, err := s.authorizeUser(ctx, tx, req.Username, req.SSO); err != nil {
    		return err
    	}
    	return s.setResourceStatus(ctx, tx, req.Username, req.ResourceID, ResourceStatusActive)
    }
    
    type ChangeUserPasswordRequest struct {
    	PrivilegedRequestBase
    	Password string `json:"password"`
    }
    
    func (r *ChangeUserPasswordRequest) Validate(ctx context.Context, s *AccountService) error {
    	return s.passwordValidator(ctx, r.Password)
    }
    
    // ChangeUserPassword updates a user's password. It will also take
    // care of re-encrypting the user encryption key, if present.
    func (s *AccountService) ChangeUserPassword(ctx context.Context, tx TX, req *ChangeUserPasswordRequest) error {
    	user, err := s.authorizeUserWithPassword(ctx, tx, req.Username, req.SSO, req.CurPassword)
    	if err != nil {
    		return err
    	}
    
    	if err = req.Validate(ctx, s); err != nil {
    		return newRequestError(err)
    	}
    
    	// If the user does not yet have an encryption key, generate one now.
    	if !user.HasEncryptionKeys {
    		err = s.initializeUserEncryptionKeys(ctx, tx, user, req.CurPassword)
    	} else {
    		err = s.updateUserEncryptionKeys(ctx, tx, user, req.CurPassword, req.Password, UserEncryptionKeyMainID)
    	}
    	if err != nil {
    		return err
    	}
    
    	// Set the encrypted password attribute on the user (will set it on emails too).
    	encPass := pwhash.Encrypt(req.Password)
    	if err := tx.SetUserPassword(ctx, user, encPass); err != nil {
    		return newBackendError(err)
    	}
    
    	return nil
    }
    
    // Initialize the user encryption key list, by creating a new "main" key
    // encrypted with the given password (which must be the primary password for the
    // user).
    func (s *AccountService) initializeUserEncryptionKeys(ctx context.Context, tx TX, user *User, curPassword string) error {
    	// Generate a new key pair.
    	pub, priv, err := userenckey.GenerateKey()
    	if err != nil {
    		return err
    	}
    
    	// Encrypt the private key with the password.
    	enc, err := userenckey.Encrypt(priv, []byte(curPassword))
    	if err != nil {
    		return err
    	}
    	keys := []*UserEncryptionKey{
    		&UserEncryptionKey{
    			ID:  UserEncryptionKeyMainID,
    			Key: enc,
    		},
    	}
    
    	// Update the backend database.
    	if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil {
    		return newBackendError(err)
    	}
    	if err := tx.SetUserEncryptionPublicKey(ctx, user, pub); err != nil {
    		return newBackendError(err)
    	}
    
    	return nil
    }
    
    // Re-encrypt the specified user encryption key with newPassword. For this
    // operation to succeed, we must be able to decrypt one of the keys (not
    // necessarily the same one) with curPassword.
    func (s *AccountService) updateUserEncryptionKeys(ctx context.Context, tx TX, user *User, curPassword, newPassword, keyID string) error {
    	keys, err := tx.GetUserEncryptionKeys(ctx, user)
    	if err != nil {
    		return newBackendError(err)
    	}
    	keys, err = reEncryptUserKeys(keys, curPassword, newPassword, keyID)
    	if err != nil {
    		return newRequestError(err)
    	}
    	if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil {
    		return newBackendError(err)
    	}
    	return nil
    }
    
    // Decode the user encyrption key using the given password, then generate a new
    // list of encryption keys by replacing the specified encryption key with one
    // encrypted with the given password (or adding it if it does not exist).
    func reEncryptUserKeys(keys []*UserEncryptionKey, curPassword, newPassword, keyID string) ([]*UserEncryptionKey, error) {
    	// userenckey.Decrypt wants a slice of []byte.
    	var rawKeys [][]byte
    	for _, k := range keys {
    		rawKeys = append(rawKeys, k.Key)
    	}
    	decrypted, err := userenckey.Decrypt(rawKeys, []byte(curPassword))
    	if err != nil {
    		return nil, err
    	}
    	encrypted, err := userenckey.Encrypt(decrypted, []byte(newPassword))
    	if err != nil {
    		return nil, err
    	}
    
    	var keysOut []*UserEncryptionKey
    	for _, key := range keys {
    		if key.ID != keyID {
    			keysOut = append(keysOut, key)
    		}
    	}
    	keysOut = append(keysOut, &UserEncryptionKey{
    		ID:  keyID,
    		Key: encrypted,
    	})
    	return keysOut, nil
    }
    
    type CreateApplicationSpecificPasswordRequest struct {
    	PrivilegedRequestBase
    	Service string `json:"service"`
    	Comment string `json:"comment"`
    }
    
    func (r *CreateApplicationSpecificPasswordRequest) Validate(_ context.Context, _ *AccountService) error {
    	if r.Service == "" {
    		return errors.New("empty 'service' attribute")
    	}
    	return nil
    }
    
    type CreateApplicationSpecificPasswordResponse struct {
    	Password string `json:"password"`
    }
    
    // CreateApplicationSpecificPassword will generate a new
    // application-specific password for the given service.
    func (s *AccountService) CreateApplicationSpecificPassword(ctx context.Context, tx TX, req *CreateApplicationSpecificPasswordRequest) (*CreateApplicationSpecificPasswordResponse, error) {
    	user, err := s.authorizeUserWithPassword(ctx, tx, req.Username, req.SSO, req.CurPassword)
    	if err != nil {
    		return nil, err
    	}
    
    	if err := req.Validate(ctx, s); err != nil {
    		return nil, newRequestError(err)
    	}
    
    	// No application-specific passwords unless 2FA is enabled.
    	if !user.Has2FA {
    		return nil, newRequestError(errors.New("2FA is not enabled for this user"))
    	}
    
    	// Create a new application-specific password and set it in
    	// the database. We don't need to update the User object as
    	// we're not reusing it.
    	asp := &AppSpecificPasswordInfo{
    		ID:      randomAppSpecificPasswordID(),
    		Service: req.Service,
    		Comment: req.Comment,
    	}
    	password := randomAppSpecificPassword()
    	encPass := pwhash.Encrypt(password)
    	if err := tx.SetApplicationSpecificPassword(ctx, user, asp, encPass); err != nil {
    		return nil, newBackendError(err)
    	}
    
    	// Create or update the user encryption key associated with
    	// this password. The user encryption key IDs for ASPs all
    	// have an 'asp_' prefix, followed by the ASP ID.
    	if user.HasEncryptionKeys {
    		keyID := "asp_" + asp.ID
    		if err := s.updateUserEncryptionKeys(ctx, tx, user, req.CurPassword, password, keyID); err != nil {
    			return nil, err
    		}
    	}
    
    	return &CreateApplicationSpecificPasswordResponse{
    		Password: password,
    	}, nil
    }
    
    type DeleteApplicationSpecificPasswordRequest struct {
    	RequestBase
    	AspID string `json:"asp_id"`
    }
    
    // DeleteApplicationSpecificPassword destroys an application-specific
    // password, identified by its unique ID.
    func (s *AccountService) DeleteApplicationSpecificPassword(ctx context.Context, tx TX, req *DeleteApplicationSpecificPasswordRequest) error {
    	user, err := s.authorizeUser(ctx, tx, req.Username, req.SSO)
    	if err != nil {
    		return err
    	}
    
    	if err = tx.DeleteApplicationSpecificPassword(ctx, user, req.AspID); err != nil {
    		return err
    	}
    
    	// Delete the user encryption key associated with this
    	// password (we're going to find it via its ID).
    	keys, err := tx.GetUserEncryptionKeys(ctx, user)
    	if err != nil {
    		return err
    	}
    	if len(keys) == 0 {
    		return nil
    	}
    	aspKeyID := "asp_" + req.AspID
    	var newKeys []*UserEncryptionKey
    	for _, k := range keys {
    		if k.ID != aspKeyID {
    			newKeys = append(newKeys, k)
    		}
    	}
    	return tx.SetUserEncryptionKeys(ctx, user, newKeys)
    }
    
    type ChangeResourcePasswordRequest struct {
    	RequestBase
    	ResourceID ResourceID `json:"resource_id"`
    	Password   string     `json:"password"`
    }
    
    func (r *ChangeResourcePasswordRequest) Validate(ctx context.Context, s *AccountService) error {
    	return s.passwordValidator(ctx, r.Password)
    }
    
    // ChangeResourcePassword modifies the password associated with a
    // specific resource. Resources that do not support this method should
    // return an error from the backend.
    func (s *AccountService) ChangeResourcePassword(ctx context.Context, tx TX, req *ChangeResourcePasswordRequest) error {
    	_, err := s.authorizeUser(ctx, tx, req.Username, req.SSO)
    	if err != nil {
    		return err
    	}
    
    	if err = req.Validate(ctx, s); err != nil {
    		return newRequestError(err)
    	}
    
    	r, err := tx.GetResource(ctx, req.ResourceID)
    	if err != nil {
    		return newBackendError(err)
    	}
    	if r == nil {
    		return ErrResourceNotFound
    	}
    
    	encPass := pwhash.Encrypt(req.Password)
    	if err := tx.SetResourcePassword(ctx, r, encPass); err != nil {
    		return newBackendError(err)
    	}
    	return nil
    }
    
    type MoveResourceRequest struct {
    	RequestBase
    	ResourceID ResourceID `json:"resource_id"`
    	Shard      string     `json:"shard"`
    }
    
    type MoveResourceResponse struct {
    	MovedIDs []string `json:"moved_ids"`
    }
    
    // MoveResource is an administrative operation to move resources
    // between shards. Resources that are part of a group are moved all at
    // once regardless of which individual ResourceID is provided as long
    // as it belongs to the group.
    func (s *AccountService) MoveResource(ctx context.Context, tx TX, req *MoveResourceRequest) (*MoveResourceResponse, error) {
    	user, err := s.authorizeAdmin(ctx, tx, req.Username, req.SSO)
    	if err != nil {
    		return nil, err
    	}
    
    	// Collect all related resources, as they should all be moved at once.
    	r, err := tx.GetResource(ctx, req.ResourceID)
    	if err != nil {
    		return nil, err
    	}
    	var resources []*Resource
    	if r.Group != "" {
    		resources = append(resources, user.GetResourcesByGroup(r.Group)...)
    	} else {
    		resources = []*Resource{r}
    	}
    
    	var resp MoveResourceResponse
    	for _, r := range resources {
    		r.Shard = req.Shard
    		if err := tx.UpdateResource(ctx, r); err != nil {
    			return nil, err
    		}
    		resp.MovedIDs = append(resp.MovedIDs, r.ID.String())
    	}
    
    	return &resp, nil
    }
    
    type EnableOTPRequest struct {
    	RequestBase
    	TOTPSecret string `json:"totp_secret"`
    }
    
    func (r *EnableOTPRequest) Validate(_ context.Context, _ *AccountService) error {
    	// TODO: the length here is bogus, replace with real value.
    	if r.TOTPSecret != "" && len(r.TOTPSecret) != 32 {
    		return errors.New("bad totp_secret value")
    	}
    	return nil
    }
    
    type EnableOTPResponse struct {
    	TOTPSecret string `json:"totp_secret"`
    }
    
    // EnableOTP enables OTP-based two-factor authentication for a
    // user. The caller can generate the TOTP secret itself if needed
    // (useful for UX that confirms that the user is able to login first),
    // or it can let the server generate a new secret by passing an empty
    // totp_secret.
    func (s *AccountService) EnableOTP(ctx context.Context, tx TX, req *EnableOTPRequest) (*EnableOTPResponse, error) {
    	user, err := s.authorizeUser(ctx, tx, req.Username, req.SSO)
    	if err != nil {
    		return nil, err
    	}
    
    	if err = req.Validate(ctx, s); err != nil {
    		return nil, newRequestError(err)
    	}
    
    	// Replace or initialize the TOTP secret.
    	if req.TOTPSecret == "" {
    		req.TOTPSecret, err = generateTOTPSecret()
    		if err != nil {
    			return nil, err
    		}
    	}
    	if err := tx.SetUserTOTPSecret(ctx, user, req.TOTPSecret); err != nil {
    		return nil, newBackendError(err)
    	}
    
    	return &EnableOTPResponse{
    		TOTPSecret: req.TOTPSecret,
    	}, nil
    }
    
    type DisableOTPRequest struct {
    	RequestBase
    }
    
    // DisableOTP disables two-factor authentication for a user.
    func (s *AccountService) DisableOTP(ctx context.Context, tx TX, req *DisableOTPRequest) error {
    	user, err := s.authorizeUser(ctx, tx, req.Username, req.SSO)
    	if err != nil {
    		return err
    	}
    
    	// Delete the TOTP secret (if present).
    	if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil {
    		return newBackendError(err)
    	}
    	return nil
    }
    
    const appSpecificPasswordLen = 64
    
    func randomBase64(n int) string {
    	b := make([]byte, n/4*3)
    	_, err := rand.Read(b[:])
    	if err != nil {
    		panic(err)
    	}
    	return base64.StdEncoding.EncodeToString(b[:])
    }
    
    func randomAppSpecificPassword() string {
    	return randomBase64(appSpecificPasswordLen)
    }
    
    const appSpecificPasswordIDLen = 4
    
    func randomAppSpecificPasswordID() string {
    	return randomBase64(appSpecificPasswordIDLen)
    }
    
    func generateTOTPSecret() (string, error) {
    	key, err := totp.Generate(totp.GenerateOpts{})
    	if err != nil {
    		return "", err
    	}
    	return key.Secret(), nil
    }