394 lines
9.5 KiB
Go
394 lines
9.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/titpetric/factory"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
|
|
"github.com/cortezaproject/corteza-server/pkg/logger"
|
|
"github.com/cortezaproject/corteza-server/pkg/permissions"
|
|
"github.com/cortezaproject/corteza-server/system/repository"
|
|
"github.com/cortezaproject/corteza-server/system/types"
|
|
)
|
|
|
|
const (
|
|
ErrUserInvalidCredentials = serviceError("UserInvalidCredentials")
|
|
ErrUserHandleNotUnique = serviceError("UserHandleNotUnique")
|
|
ErrUserUsernameNotUnique = serviceError("UserUsernameNotUnique")
|
|
ErrUserEmailNotUnique = serviceError("UserEmailNotUnique")
|
|
ErrUserLocked = serviceError("UserLocked")
|
|
|
|
maskPrivateDataEmail = "####.#######@######.###"
|
|
maskPrivateDataName = "##### ##########"
|
|
)
|
|
|
|
type (
|
|
user struct {
|
|
db *factory.DB
|
|
ctx context.Context
|
|
logger *zap.Logger
|
|
|
|
settings *types.Settings
|
|
|
|
auth userAuth
|
|
subscription userSubscriptionChecker
|
|
|
|
ac userAccessController
|
|
user repository.UserRepository
|
|
credentials repository.CredentialsRepository
|
|
|
|
// @todo wire this with settings (privacy.mask.email)
|
|
privacyMaskEmail bool
|
|
// @todo wire this with settings (privacy.mask.name)
|
|
privacyMaskName bool
|
|
}
|
|
|
|
userAuth interface {
|
|
checkPasswordStrength(string) error
|
|
changePassword(uint64, string) error
|
|
}
|
|
|
|
userSubscriptionChecker interface {
|
|
CanCreateUser(uint) error
|
|
}
|
|
|
|
userAccessController interface {
|
|
CanAccess(context.Context) bool
|
|
CanCreateUser(context.Context) bool
|
|
CanUpdateUser(context.Context, *types.User) bool
|
|
CanDeleteUser(context.Context, *types.User) bool
|
|
CanSuspendUser(context.Context, *types.User) bool
|
|
CanUnsuspendUser(context.Context, *types.User) bool
|
|
CanUnmaskEmail(context.Context, *types.User) bool
|
|
CanUnmaskName(context.Context, *types.User) bool
|
|
|
|
FilterReadableUsers(ctx context.Context) *permissions.ResourceFilter
|
|
FilterUsersWithUnmaskableEmail(ctx context.Context) *permissions.ResourceFilter
|
|
FilterUsersWithUnmaskableName(ctx context.Context) *permissions.ResourceFilter
|
|
}
|
|
|
|
UserService interface {
|
|
With(ctx context.Context) UserService
|
|
|
|
FindByUsername(username string) (*types.User, error)
|
|
FindByEmail(email string) (*types.User, error)
|
|
FindByHandle(handle string) (*types.User, error)
|
|
FindByID(id uint64) (*types.User, error)
|
|
Find(types.UserFilter) (types.UserSet, types.UserFilter, error)
|
|
|
|
Create(input *types.User) (*types.User, error)
|
|
Update(mod *types.User) (*types.User, error)
|
|
|
|
CreateWithAvatar(input *types.User, avatar io.Reader) (*types.User, error)
|
|
UpdateWithAvatar(mod *types.User, avatar io.Reader) (*types.User, error)
|
|
|
|
Delete(id uint64) error
|
|
Suspend(id uint64) error
|
|
Unsuspend(id uint64) error
|
|
|
|
SetPassword(userID uint64, password string) error
|
|
}
|
|
)
|
|
|
|
func User(ctx context.Context) UserService {
|
|
return (&user{
|
|
logger: DefaultLogger.Named("user"),
|
|
}).With(ctx)
|
|
}
|
|
|
|
// log() returns zap's logger with requestID from current context and fields.
|
|
func (svc user) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger {
|
|
return logger.AddRequestID(ctx, svc.logger).With(fields...)
|
|
}
|
|
|
|
func (svc user) With(ctx context.Context) UserService {
|
|
db := repository.DB(ctx)
|
|
|
|
return &user{
|
|
ctx: ctx,
|
|
db: db,
|
|
logger: svc.logger,
|
|
|
|
ac: DefaultAccessControl,
|
|
settings: CurrentSettings,
|
|
auth: DefaultAuth,
|
|
|
|
subscription: CurrentSubscription,
|
|
|
|
user: repository.User(ctx, db),
|
|
credentials: repository.Credentials(ctx, db),
|
|
|
|
// @todo wire this with settings (privacy.mask.email)
|
|
// new default value will be true!
|
|
privacyMaskEmail: false,
|
|
|
|
// @todo wire this with settings (privacy.mask.name)
|
|
// new default value will be true!
|
|
privacyMaskName: false,
|
|
}
|
|
}
|
|
|
|
func (svc user) FindByID(ID uint64) (*types.User, error) {
|
|
if ID == 0 {
|
|
return nil, ErrInvalidID
|
|
}
|
|
|
|
return svc.proc(svc.user.FindByID(ID))
|
|
}
|
|
|
|
func (svc user) FindByEmail(email string) (*types.User, error) {
|
|
return svc.proc(svc.user.FindByEmail(email))
|
|
}
|
|
|
|
func (svc user) FindByUsername(username string) (*types.User, error) {
|
|
return svc.proc(svc.user.FindByUsername(username))
|
|
}
|
|
|
|
func (svc user) FindByHandle(handle string) (*types.User, error) {
|
|
return svc.proc(svc.user.FindByHandle(handle))
|
|
}
|
|
|
|
func (svc user) proc(u *types.User, err error) (*types.User, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
svc.handlePrivateData(u)
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (svc user) Find(f types.UserFilter) (types.UserSet, types.UserFilter, error) {
|
|
if f.IncDeleted || f.IncSuspended {
|
|
// If list with deleted or suspended users is requested
|
|
// user must have access permissions to system (ie: is admin)
|
|
if !svc.ac.CanAccess(svc.ctx) {
|
|
return nil, f, ErrNoPermissions.withStack()
|
|
}
|
|
}
|
|
|
|
if svc.privacyMaskEmail {
|
|
// Prepare filter for email unmasking check
|
|
f.IsEmailUnmaskable = svc.ac.FilterUsersWithUnmaskableEmail(svc.ctx)
|
|
|
|
}
|
|
|
|
if svc.privacyMaskName {
|
|
// Prepare filter for name unmasking check
|
|
f.IsNameUnmaskable = svc.ac.FilterUsersWithUnmaskableName(svc.ctx)
|
|
}
|
|
|
|
f.IsReadable = svc.ac.FilterReadableUsers(svc.ctx)
|
|
|
|
return svc.procSet(svc.user.Find(f))
|
|
}
|
|
|
|
func (svc user) procSet(u types.UserSet, f types.UserFilter, err error) (types.UserSet, types.UserFilter, error) {
|
|
if err != nil {
|
|
return nil, f, err
|
|
}
|
|
|
|
_ = u.Walk(func(u *types.User) error {
|
|
svc.handlePrivateData(u)
|
|
return nil
|
|
})
|
|
|
|
return u, f, nil
|
|
}
|
|
|
|
func (svc user) Create(input *types.User) (out *types.User, err error) {
|
|
if !svc.ac.CanCreateUser(svc.ctx) {
|
|
return nil, ErrNoCreatePermissions.withStack()
|
|
}
|
|
|
|
if svc.subscription != nil {
|
|
// When we have an active subscription, we need to check
|
|
// if users can be creare or did this deployment hit
|
|
// it's user-limit
|
|
err = svc.subscription.CanCreateUser(svc.user.Total())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return out, svc.db.Transaction(func() (err error) {
|
|
if err = svc.UniqueCheck(input); err != nil {
|
|
return
|
|
}
|
|
|
|
out, err = svc.user.Create(input)
|
|
return
|
|
})
|
|
}
|
|
|
|
func (svc user) CreateWithAvatar(input *types.User, avatar io.Reader) (out *types.User, err error) {
|
|
// @todo: avatar
|
|
return svc.Create(input)
|
|
}
|
|
|
|
func (svc user) Update(mod *types.User) (u *types.User, err error) {
|
|
if mod.ID == 0 {
|
|
return nil, ErrInvalidID
|
|
}
|
|
|
|
if u, err = svc.user.FindByID(mod.ID); err != nil {
|
|
return
|
|
}
|
|
|
|
if mod.ID != internalAuth.GetIdentityFromContext(svc.ctx).Identity() {
|
|
if !svc.ac.CanUpdateUser(svc.ctx, u) {
|
|
return nil, ErrNoUpdatePermissions.withStack()
|
|
}
|
|
}
|
|
|
|
// Assign changed values
|
|
u.Email = mod.Email
|
|
u.Username = mod.Username
|
|
u.Name = mod.Name
|
|
u.Handle = mod.Handle
|
|
u.Kind = mod.Kind
|
|
|
|
return u, svc.db.Transaction(func() (err error) {
|
|
if err = svc.UniqueCheck(u); err != nil {
|
|
return
|
|
}
|
|
|
|
u, err = svc.user.Update(u)
|
|
return
|
|
})
|
|
}
|
|
|
|
func (svc user) UniqueCheck(u *types.User) (err error) {
|
|
if u.Email != "" {
|
|
if ex, _ := svc.user.FindByEmail(u.Email); ex != nil && ex.ID > 0 && ex.ID != u.ID {
|
|
return ErrUserEmailNotUnique
|
|
}
|
|
}
|
|
|
|
if u.Username != "" {
|
|
if ex, _ := svc.user.FindByUsername(u.Username); ex != nil && ex.ID > 0 && ex.ID != u.ID {
|
|
return ErrUserUsernameNotUnique
|
|
}
|
|
}
|
|
|
|
if u.Handle != "" {
|
|
if ex, _ := svc.user.FindByHandle(u.Handle); ex != nil && ex.ID > 0 && ex.ID != u.ID {
|
|
return ErrUserHandleNotUnique
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc user) UpdateWithAvatar(mod *types.User, avatar io.Reader) (out *types.User, err error) {
|
|
// @todo: avatar
|
|
return svc.Create(mod)
|
|
}
|
|
|
|
func (svc user) Delete(ID uint64) (err error) {
|
|
if ID == 0 {
|
|
return ErrInvalidID
|
|
}
|
|
|
|
var u *types.User
|
|
if u, err = svc.user.FindByID(ID); err != nil {
|
|
return
|
|
}
|
|
|
|
if !svc.ac.CanDeleteUser(svc.ctx, u) {
|
|
return ErrNoPermissions.withStack()
|
|
}
|
|
|
|
return svc.db.Transaction(func() (err error) {
|
|
return svc.user.DeleteByID(ID)
|
|
})
|
|
}
|
|
|
|
func (svc user) Suspend(ID uint64) (err error) {
|
|
if ID == 0 {
|
|
return ErrInvalidID
|
|
}
|
|
|
|
var u *types.User
|
|
if u, err = svc.user.FindByID(ID); err != nil {
|
|
return
|
|
}
|
|
|
|
if !svc.ac.CanSuspendUser(svc.ctx, u) {
|
|
return ErrNoPermissions.withStack()
|
|
}
|
|
|
|
return svc.db.Transaction(func() (err error) {
|
|
return svc.user.SuspendByID(ID)
|
|
})
|
|
}
|
|
|
|
func (svc user) Unsuspend(ID uint64) (err error) {
|
|
if ID == 0 {
|
|
return ErrInvalidID
|
|
}
|
|
|
|
var u *types.User
|
|
if u, err = svc.user.FindByID(ID); err != nil {
|
|
return
|
|
}
|
|
|
|
if !svc.ac.CanUnsuspendUser(svc.ctx, u) {
|
|
return ErrNoPermissions.withStack()
|
|
}
|
|
|
|
return svc.db.Transaction(func() (err error) {
|
|
return svc.user.UnsuspendByID(ID)
|
|
})
|
|
}
|
|
|
|
// SetPassword sets new password for a user
|
|
//
|
|
// Expecting setter to have permissions to update modify users and internal authentication enabled
|
|
func (svc user) SetPassword(userID uint64, newPassword string) (err error) {
|
|
log := svc.log(svc.ctx, zap.Uint64("userID", userID))
|
|
|
|
if !svc.settings.Auth.Internal.Enabled {
|
|
return errors.New("internal authentication disabled")
|
|
}
|
|
|
|
var u *types.User
|
|
if u, err = svc.user.FindByID(userID); err != nil {
|
|
return
|
|
}
|
|
|
|
if !svc.ac.CanUpdateUser(svc.ctx, u) {
|
|
return ErrNoPermissions.withStack()
|
|
}
|
|
|
|
if err = svc.auth.checkPasswordStrength(newPassword); err != nil {
|
|
return
|
|
}
|
|
|
|
return svc.db.Transaction(func() error {
|
|
if err := svc.auth.changePassword(userID, newPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info("password changed")
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Masks (or leaves as-is) private data on user
|
|
func (svc user) handlePrivateData(u *types.User) {
|
|
if svc.privacyMaskEmail && !svc.ac.CanUnmaskEmail(svc.ctx, u) {
|
|
u.Email = maskPrivateDataEmail
|
|
}
|
|
|
|
if svc.privacyMaskName && !svc.ac.CanUnmaskEmail(svc.ctx, u) {
|
|
u.Name = maskPrivateDataName
|
|
}
|
|
}
|