service.go 7.32 KB
Newer Older
1 2 3 4
package accountserver

import (
	"context"
5
	"fmt"
6
	"log"
ale's avatar
ale committed
7
	"time"
8 9

	"git.autistici.org/id/go-sso"
ale's avatar
ale committed
10 11
	umdb "git.autistici.org/id/usermetadb"
	umdbc "git.autistici.org/id/usermetadb/client"
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
)

// 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.
//
ale's avatar
ale committed
35 36 37 38 39
// 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.
//
40 41 42 43 44 45
// We might add more sophisticated resource query methods later, as
// admin-level functionality.
//
type TX interface {
	Commit(context.Context) error

ale's avatar
ale committed
46
	GetResource(context.Context, ResourceID) (*RawResource, error)
47
	UpdateResource(context.Context, *Resource) error
ale's avatar
ale committed
48
	CreateResources(context.Context, *User, []*Resource) ([]*Resource, error)
49
	SetResourcePassword(context.Context, *Resource, string) error
ale's avatar
ale committed
50
	HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
51

52
	GetUser(context.Context, string) (*RawUser, error)
53
	UpdateUser(context.Context, *User) error
ale's avatar
ale committed
54
	CreateUser(context.Context, *User) (*User, error)
55
	SetUserPassword(context.Context, *User, string) error
56 57
	SetAccountRecoveryHint(context.Context, *User, string, string) error
	DeleteAccountRecoveryHint(context.Context, *User) error
58 59 60 61 62 63
	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
64

ale's avatar
ale committed
65 66 67 68
	// Lightweight user search (backend-specific pattern).
	// Returns list of matching usernames.
	SearchUser(context.Context, string) ([]string, error)

ale's avatar
ale committed
69 70 71
	// Resource search (backend-specific pattern).
	SearchResource(context.Context, string) ([]*RawResource, error)

ale's avatar
ale committed
72 73 74
	// Resource ACL check (does not necessarily hit the database).
	CanAccessResource(context.Context, string, *Resource) bool

ale's avatar
ale committed
75 76
	// Return the next (or any, really) available user ID.
	NextUID(context.Context) (int, error)
77 78 79 80 81 82 83 84
}

// FindResourceRequest contains parameters for searching a resource by name.
type FindResourceRequest struct {
	Type string
	Name string
}

85 86 87
// 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.
88
type AccountService struct {
89
	*authService
90

91 92 93
	backend Backend
	audit   auditLogger
	umdb    umdbc.Client
94

95
	fieldValidators   *fieldValidators
96
	resourceValidator *resourceValidator
ale's avatar
ale committed
97
	userValidator     UserValidatorFunc
ale's avatar
ale committed
98
	resourceTemplates *templateContext
99 100

	enableOpportunisticEncryption bool
101 102
}

ale's avatar
ale committed
103
// NewAccountService builds a new AccountService with the specified configuration.
104 105 106 107 108 109
func NewAccountService(backend Backend, config *Config) (*AccountService, error) {
	ssoValidator, err := config.ssoValidator()
	if err != nil {
		return nil, err
	}

110
	return newAccountServiceWithSSO(backend, config, ssoValidator)
111 112
}

113
func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) (*AccountService, error) {
114 115 116 117
	if err := config.compile(); err != nil {
		return nil, fmt.Errorf("configuration error: %v", err)
	}

118
	s := &AccountService{
119 120 121
		authService:                   newAuthService(config, ssoValidator),
		audit:                         &syslogAuditLogger{},
		backend:                       backend,
122
		enableOpportunisticEncryption: config.EnableOpportunisticEncryption,
123 124
	}

ale's avatar
ale committed
125 126 127 128 129 130 131 132
	if config.UserMetaDB != nil {
		var err error
		s.umdb, err = umdbc.New(config.UserMetaDB)
		if err != nil {
			return nil, err
		}
	}

133
	vc, err := config.validationContext(backend)
134 135 136
	if err != nil {
		return nil, err
	}
137 138
	s.fieldValidators = newFieldValidators(vc)
	s.resourceValidator = newResourceValidator(vc)
ale's avatar
ale committed
139
	s.userValidator = vc.validUser()
140

ale's avatar
ale committed
141 142
	s.resourceTemplates = config.templateContext()

143
	return s, nil
144 145
}

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
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) isAdmin(tkt *sso.Ticket) bool {
163 164 165 166 167 168 169 170
	for _, g := range tkt.Groups {
		if g == s.ssoAdminGroup {
			return true
		}
	}
	return false
}

171
func (s *authService) validateSSO(ssoToken string) (*sso.Ticket, error) {
172 173 174
	return s.validator.Validate(ssoToken, "", s.ssoService, s.ssoGroups)
}

175 176 177
// 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.
178
func getUserOrDie(ctx context.Context, tx TX, username string) (*RawUser, error) {
179 180 181 182 183 184 185
	user, err := tx.GetUser(ctx, username)
	if err != nil {
		return nil, newBackendError(err)
	}
	if user == nil {
		return nil, ErrUserNotFound
	}
186 187 188

	user.groupWebResources()

189 190 191
	return user, nil
}

ale's avatar
ale committed
192
func getResourceOrDie(ctx context.Context, tx TX, id ResourceID) (*RawResource, error) {
193 194 195 196 197 198 199 200 201 202
	r, err := tx.GetResource(ctx, id)
	if err != nil {
		return nil, newBackendError(err)
	}
	if r == nil {
		return nil, ErrResourceNotFound
	}
	return r, nil
}

ale's avatar
ale committed
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
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)
		}
	}()
}

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
// 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
262
}