Select Git revision
-
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).
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)
}