Skip to content
Snippets Groups Projects
Select Git revision
  • 9ab0cb20b1030c8c9205666b79c4f136ab68d6be
  • master default
2 results

main.go

Blame
    • Janis Meybohm's avatar
      e0adec1f
      Allow to silence "error handling stats line" messages · e0adec1f
      Janis Meybohm authored
      "error handling stats line" messages can be really noisy and can be
      ignored ignored in many cases (where module stats are not required/wanted).
      
      This change adds a "-silent" flag to suppress those messages as well as
      e metric to count them (so an increase in rate can still be detected).
      e0adec1f
      History
      Allow to silence "error handling stats line" messages
      Janis Meybohm authored
      "error handling stats line" messages can be really noisy and can be
      ignored ignored in many cases (where module stats are not required/wanted).
      
      This change adds a "-silent" flag to suppress those messages as well as
      e metric to count them (so an increase in rate can still be detected).
    service.go 9.69 KiB
    package accountserver
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"time"
    
    	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
    	"git.autistici.org/id/auth"
    	authclient "git.autistici.org/id/auth/client"
    	"git.autistici.org/id/go-sso"
    	umdb "git.autistici.org/id/usermetadb"
    	umdbc "git.autistici.org/id/usermetadb/client"
    )
    
    // 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.
    //
    // The API passes around the full User object, where a simple username
    // would usually suffice, because it needs to synchronize things
    // between resources: this is primarily due to the coupling between
    // account and email resource.
    //
    // We might add more sophisticated resource query methods later, as
    // admin-level functionality.
    //
    type TX interface {
    	Commit(context.Context) error
    
    	GetResource(context.Context, ResourceID) (*RawResource, error)
    	UpdateResource(context.Context, *Resource) error
    	CreateResources(context.Context, *User, []*Resource) ([]*Resource, error)
    	SetResourcePassword(context.Context, *Resource, string) error
    	FindResource(context.Context, FindResourceRequest) (*RawResource, error)
    	HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
    
    	GetUser(context.Context, string) (*RawUser, error)
    	UpdateUser(context.Context, *User) error
    	CreateUser(context.Context, *User) (*User, error)
    	SetUserPassword(context.Context, *User, string) error
    	SetAccountRecoveryHint(context.Context, *User, string, string) error
    	DeleteAccountRecoveryHint(context.Context, *User) error
    	SetUserEncryptionKeys(context.Context, *User, []*ct.EncryptedKey) 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
    
    	// Lightweight user search (backend-specific pattern).
    	// Returns list of matching usernames.
    	SearchUser(context.Context, string) ([]string, error)
    
    	// Resource search (backend-specific pattern).
    	SearchResource(context.Context, string) ([]*RawResource, error)
    
    	// Resource ACL check (does not necessarily hit the database).
    	CanAccessResource(context.Context, string, *Resource) bool
    
    	// Return the next (or any, really) available user ID.
    	NextUID(context.Context) (int, 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. It provides common services that
    // the various action handlers can use, such as validation, auditing, etc.
    //
    // Action handlers aren't implemented as methods on AccountService,
    // instead they get access to this object via the RequestContext.
    type AccountService struct {
    	*authService
    
    	backend Backend
    	audit   auditLogger
    	umdb    umdbc.Client
    
    	authClient                 authclient.Client
    	userAuthService            string
    	accountRecoveryAuthService string
    
    	validationCtx     *validationContext
    	fieldValidators   *fieldValidators
    	resourceValidator *resourceValidator
    	userValidator     UserValidatorFunc
    	resourceTemplates *templateContext
    
    	enableOpportunisticEncryption bool
    }
    
    // NewAccountService builds a new AccountService with the specified configuration.
    func NewAccountService(backend Backend, config *Config) (*AccountService, error) {
    	ssoValidator, err := config.ssoValidator()
    	if err != nil {
    		return nil, err
    	}
    
    	var authSocket = authclient.DefaultSocketPath
    	if config.AuthSocket != "" {
    		authSocket = config.AuthSocket
    	}
    	authClient := authclient.New(authSocket)
    
    	return newAccountServiceInternal(backend, config, ssoValidator, authClient)
    }
    
    const (
    	defaultUserAuthService            = "accountserver"
    	defaultAccountRecoveryAuthService = "accountserver-recovery"
    )
    
    func defaultStr(value, dflt string) string {
    	if value == "" {
    		return dflt
    	}
    	return value
    }
    
    // This function is split out from the above so we can pass stubs for
    // some backends when testing.
    func newAccountServiceInternal(backend Backend, config *Config, ssoValidator sso.Validator, authClient authclient.Client) (*AccountService, error) {
    	if err := config.compile(); err != nil {
    		return nil, fmt.Errorf("configuration error: %v", err)
    	}
    
    	s := &AccountService{
    		authService:                   newAuthService(config, ssoValidator),
    		backend:                       backend,
    		authClient:                    authClient,
    		userAuthService:               defaultStr(config.UserAuthService, defaultUserAuthService),
    		accountRecoveryAuthService:    defaultStr(config.AccountRecoveryAuthService, defaultAccountRecoveryAuthService),
    		enableOpportunisticEncryption: config.EnableOpportunisticEncryption,
    	}
    	if config.AuditLogsToSyslog {
    		s.audit = newSyslogAuditLogger()
    	} else {
    		s.audit = newStderrAuditLogger()
    	}
    
    	if config.UserMetaDB != nil {
    		var err error
    		s.umdb, err = umdbc.New(config.UserMetaDB)
    		if err != nil {
    			return nil, err
    		}
    	}
    
    	vc, err := config.validationContext(backend)
    	if err != nil {
    		return nil, err
    	}
    	s.validationCtx = vc
    	s.fieldValidators = newFieldValidators(vc)
    	s.resourceValidator = newResourceValidator(vc)
    	s.userValidator = vc.validUser()
    
    	s.resourceTemplates = config.templateContext()
    
    	return s, nil
    }
    
    // The authService parses identity data from a request (usually in the
    // form of a SSO token), validates it, and creates an Auth object to
    // go along the RequestContext.
    type authService struct {
    	validator     sso.Validator
    	ssoService    string
    	ssoGroups     []string
    	ssoAdminGroup string
    }
    
    func newAuthService(config *Config, v sso.Validator) *authService {
    	return &authService{
    		validator:     v,
    		ssoService:    config.SSO.Service,
    		ssoGroups:     config.SSO.Groups,
    		ssoAdminGroup: config.SSO.AdminGroup,
    	}
    }
    
    func (s *authService) authFromSSO(ssoToken string) (Auth, error) {
    	tkt, err := s.validator.Validate(ssoToken, "", s.ssoService, s.ssoGroups)
    	if err != nil {
    		return Auth{}, err
    	}
    
    	a := Auth{Username: tkt.User}
    
    	for _, g := range tkt.Groups {
    		if g == s.ssoAdminGroup {
    			a.IsAdmin = true
    			break
    		}
    	}
    
    	return a, nil
    }
    
    // Fetch a user from the backend and return an error if not found
    // (instead of returning a nil User). Also, set dynamic attributes
    // like resource Groups.
    func getUserOrDie(ctx context.Context, tx TX, username string) (*RawUser, error) {
    	user, err := tx.GetUser(ctx, username)
    	if err != nil {
    		return nil, newBackendError(err)
    	}
    	if user == nil {
    		return nil, ErrUserNotFound
    	}
    
    	user.groupWebResources()
    
    	return user, nil
    }
    
    func getResourceOrDie(ctx context.Context, tx TX, id ResourceID) (*RawResource, error) {
    	r, err := tx.GetResource(ctx, id)
    	if err != nil {
    		return nil, newBackendError(err)
    	}
    	if r == nil {
    		return nil, ErrResourceNotFound
    	}
    	return r, nil
    }
    
    var umdbLogTimeout = 30 * time.Second
    
    func (s *AccountService) logUserAction(user *User, logtype, logmsg string) {
    	if s.umdb == nil {
    		return
    	}
    	go func() {
    		ctx, cancel := context.WithTimeout(context.Background(), umdbLogTimeout)
    		defer cancel()
    		if err := s.umdb.AddLog(ctx, "", &umdb.LogEntry{
    			Timestamp: time.Now(),
    			Username:  user.Name,
    			Type:      logtype,
    			Message:   logmsg,
    			Service:   "accountserver",
    		}); err != nil {
    			log.Printf("usermetadb.AddLog error for %s: %v", user.Name, err)
    		}
    	}()
    }
    
    // Handle the given Request. Returns its result.
    func (s *AccountService) Handle(ctx context.Context, r Request) (interface{}, error) {
    	tx, err := s.backend.NewTransaction()
    	if err != nil {
    		return nil, err
    	}
    	rctx := &RequestContext{
    		AccountService: s,
    		Context:        ctx,
    		TX:             tx,
    	}
    
    	// Populate the context.
    	if err = r.PopulateContext(rctx); err != nil {
    		return nil, err
    	}
    
    	// Validate the request.
    	if err = r.Validate(rctx); err != nil {
    		return nil, newRequestError(err)
    	}
    
    	// Authorize the request.
    	if err = r.Authorize(rctx); err != nil {
    		return nil, newAuthError(err)
    	}
    
    	// Perform the actions.
    	resp, err := r.Serve(rctx)
    	if err != nil {
    		return nil, err
    	}
    
    	if err := tx.Commit(ctx); err != nil {
    		return nil, err
    	}
    
    	return resp, nil
    }
    
    func (s *AccountService) callAuthServer(ctx context.Context, service, username, password, remoteAddr string) error {
    	resp, err := s.authClient.Authenticate(ctx, &auth.Request{
    		Service:  service,
    		Username: username,
    		Password: []byte(password),
    		DeviceInfo: &auth.DeviceInfo{
    			RemoteAddr: remoteAddr,
    		},
    	})
    	if err != nil {
    		return err
    	}
    	if resp.Status != auth.StatusOK {
    		return errors.New("authentication failure")
    	}
    	return nil
    }
    
    func (s *AccountService) authorizeUser(ctx context.Context, username, password, remoteAddr string) error {
    	return s.callAuthServer(ctx, s.userAuthService, username, password, remoteAddr)
    }
    
    func (s *AccountService) authorizeAccountRecovery(ctx context.Context, username, password, remoteAddr string) error {
    	return s.callAuthServer(ctx, s.accountRecoveryAuthService, username, password, remoteAddr)
    }