3
0

890 lines
20 KiB
Go

package service
import (
"context"
"io"
"net/mail"
"regexp"
"strconv"
"strings"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service/event"
"github.com/cortezaproject/corteza-server/system/types"
)
const (
maskPrivateDataEmail = "####.#######@######.###"
maskPrivateDataName = "##### ##########"
)
type (
user struct {
actionlog actionlog.Recorder
settings *types.AppSettings
auth userAuth
ac userAccessController
eventbus eventDispatcher
store store.Storer
}
userAuth interface {
CheckPasswordStrength(string) bool
SetPasswordCredentials(context.Context, uint64, string) error
}
userAccessController interface {
CanCreateUser(context.Context) bool
CanReadUser(context.Context, *types.User) 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
}
// Temp types to support user.Preloader
userIdGetter func(chan uint64)
userSetter func(*types.User) error
UserService interface {
FindByUsername(ctx context.Context, username string) (*types.User, error)
FindByEmail(ctx context.Context, email string) (*types.User, error)
FindByHandle(ctx context.Context, handle string) (*types.User, error)
FindByID(ctx context.Context, id uint64) (*types.User, error)
FindByAny(ctx context.Context, identifier interface{}) (*types.User, error)
Find(context.Context, types.UserFilter) (types.UserSet, types.UserFilter, error)
Create(ctx context.Context, input *types.User) (*types.User, error)
Update(ctx context.Context, mod *types.User) (*types.User, error)
ToggleEmailConfirmation(ctx context.Context, userID uint64, confirm bool) error
CreateWithAvatar(ctx context.Context, input *types.User, avatar io.Reader) (*types.User, error)
UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (*types.User, error)
Delete(ctx context.Context, id uint64) error
Suspend(ctx context.Context, id uint64) error
Unsuspend(ctx context.Context, id uint64) error
Undelete(ctx context.Context, id uint64) error
SetPassword(ctx context.Context, userID uint64, password string) error
Preloader(context.Context, userIdGetter, types.UserFilter, userSetter) error
DeleteAuthTokensByUserID(ctx context.Context, userID uint64) (err error)
DeleteAuthSessionsByUserID(ctx context.Context, userID uint64) (err error)
}
)
func User(ctx context.Context) UserService {
return &user{
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
settings: CurrentSettings,
auth: DefaultAuth,
store: DefaultStore,
actionlog: DefaultActionlog,
}
}
func (svc user) FindByID(ctx context.Context, userID uint64) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() error {
if userID == 0 {
return UserErrInvalidID()
}
su := internalAuth.NewIdentity(userID)
if internalAuth.IsSuperUser(su) {
// Handling case when looking for a super-user
//
// Currently, superuser is a virtual entity
// and does not exists in the db
u = &types.User{ID: userID}
return nil
}
u, err = store.LookupUserByID(ctx, svc.store, userID)
if u, err = svc.proc(ctx, u, err); err != nil {
return err
}
uaProps.setUser(u)
if err = label.Load(ctx, svc.store, u); err != nil {
return err
}
return nil
}()
return u, svc.recordAction(ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByEmail(ctx context.Context, email string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Email: email}}
)
err = func() error {
u, err = store.LookupUserByEmail(ctx, svc.store, email)
if u, err = svc.proc(ctx, u, err); err != nil {
return err
}
uaProps.setUser(u)
if err = label.Load(ctx, svc.store, u); err != nil {
return err
}
return nil
}()
return u, svc.recordAction(ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByUsername(ctx context.Context, username string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Username: username}}
)
err = func() error {
u, err = store.LookupUserByUsername(ctx, svc.store, username)
if u, err = svc.proc(ctx, u, err); err != nil {
return err
}
uaProps.setUser(u)
if err = label.Load(ctx, svc.store, u); err != nil {
return err
}
return nil
}()
return u, svc.recordAction(ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByHandle(ctx context.Context, handle string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Handle: handle}}
)
err = func() error {
u, err = store.LookupUserByHandle(ctx, svc.store, handle)
if u, err = svc.proc(ctx, u, err); err != nil {
return err
}
uaProps.setUser(u)
if err = label.Load(ctx, svc.store, u); err != nil {
return err
}
return nil
}()
return u, svc.recordAction(ctx, uaProps, UserActionLookup, err)
}
// FindByAny finds user by given identifier (context, id, handle, email)
//
// This function goes against the context anti (!!!) pattern we're using
// (and trying to get rid of)
//
// Main reason to push ctx here as the 1st arg is allow (simple) interface definition
// in the consumers that reside under the pkg/
func (svc user) FindByAny(ctx context.Context, identifier interface{}) (u *types.User, err error) {
if ctx, ok := identifier.(context.Context); ok {
identifier = internalAuth.GetIdentityFromContext(ctx).Identity()
}
if ID, ok := identifier.(uint64); ok {
u, err = svc.FindByID(ctx, ID)
} else if identity, ok := identifier.(internalAuth.Identifiable); ok {
u, err = svc.FindByID(ctx, identity.Identity())
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
u, err = svc.FindByID(ctx, ID)
} else if strings.Contains(strIdentifier, "@") {
u, err = svc.FindByEmail(ctx, strIdentifier)
} else {
u, err = svc.FindByHandle(ctx, strIdentifier)
}
} else {
err = UserErrInvalidID()
}
if err != nil {
return
}
rr, _, err := store.SearchRoles(ctx, svc.store, types.RoleFilter{MemberID: u.ID})
if err != nil {
return nil, err
}
u.SetRoles(rr.IDs())
return
}
func (svc user) proc(ctx context.Context, u *types.User, err error) (*types.User, error) {
if err != nil {
if errors.IsNotFound(err) {
return nil, UserErrNotFound()
}
return nil, err
}
svc.handlePrivateData(ctx, u)
return u, nil
}
// Find interacts with backend storage and
//
// @todo rename to Search() for consistency
func (svc user) Find(ctx context.Context, filter types.UserFilter) (uu types.UserSet, f types.UserFilter, err error) {
var (
uaProps = &userActionProps{filter: &filter}
)
// For each fetched item, store backend will check if it is valid or not
filter.MaskedEmailsEnabled = svc.settings.Privacy.Mask.Email
filter.MaskedNamesEnabled = svc.settings.Privacy.Mask.Name
filter.Check = func(res *types.User) (bool, error) {
if !svc.ac.CanReadUser(ctx, res) {
return false, nil
}
if svc.maskEmail(ctx, res) && ((len(filter.Query) > 0 && strings.HasPrefix(res.Email, filter.Query)) || res.Email == filter.Email) {
// user email matched but it will be masked later on, so exclude it to prevent data probing
return false, nil
}
if svc.maskName(ctx, res) && (len(filter.Query) > 0 && strings.HasPrefix(res.Name, filter.Query)) {
// user mail matched but it will be masked later on, so exclude it to prevent data probing
return false, nil
}
return true, nil
}
err = func() error {
if filter.Deleted > 0 {
// If list with deleted users is requested
// user must have access permissions to system (ie: is admin)
//
// not the best solution but ATM it allows us to have at least
// some kind of control over who can see deleted users
//if !svc.ac.CanAccess(ctx) {
// return UserErrNotAllowedToListUsers()
//}
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.User{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
uu, f, err = store.SearchUsers(ctx, svc.store, filter)
if err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledUsers(uu)...); err != nil {
return err
}
return uu.Walk(func(u *types.User) error {
svc.handlePrivateData(ctx, u)
return nil
})
}()
return uu, f, svc.recordAction(ctx, uaProps, UserActionSearch, err)
}
func (svc user) Create(ctx context.Context, new *types.User) (u *types.User, err error) {
var (
uaProps = &userActionProps{new: new}
)
err = func() (err error) {
if !svc.ac.CanCreateUser(ctx) {
return UserErrNotAllowedToCreate()
}
if !handle.IsValid(new.Handle) {
return UserErrInvalidHandle()
}
if _, err := mail.ParseAddress(new.Email); err != nil {
return UserErrInvalidEmail()
}
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeCreate(new, u)); err != nil {
return
}
if new.Handle == "" {
createUserHandle(ctx, DefaultStore, new)
}
if err = uniqueUserCheck(ctx, svc.store, new); err != nil {
return
}
new.ID = nextID()
new.CreatedAt = *now()
// consider email confirmed
// when creating user like this
new.EmailConfirmed = true
if err = store.CreateUser(ctx, svc.store, new); err != nil {
return
}
if err = label.Create(ctx, svc.store, new); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterCreate(new, u))
return
}()
return new, svc.recordAction(ctx, uaProps, UserActionCreate, err)
}
func (svc user) CreateWithAvatar(ctx context.Context, input *types.User, avatar io.Reader) (out *types.User, err error) {
// @todo: avatar
return svc.Create(ctx, input)
}
func (svc user) Update(ctx context.Context, upd *types.User) (u *types.User, err error) {
var (
uaProps = &userActionProps{update: upd}
)
err = func() (err error) {
if upd.ID == 0 {
return UserErrInvalidID()
}
if !handle.IsValid(upd.Handle) {
return UserErrInvalidHandle()
}
if _, err := mail.ParseAddress(upd.Email); err != nil {
return UserErrInvalidEmail()
}
if u, err = store.LookupUserByID(ctx, svc.store, upd.ID); err != nil {
return
}
uaProps.setUser(u)
if upd.ID != internalAuth.GetIdentityFromContext(ctx).Identity() {
if !svc.ac.CanUpdateUser(ctx, u) {
return UserErrNotAllowedToUpdate()
}
}
// Assign changed values
u.Email = upd.Email
u.Username = upd.Username
u.Name = upd.Name
u.Handle = upd.Handle
u.Kind = upd.Kind
u.UpdatedAt = now()
if upd.Meta != nil {
// Only update meta when set
u.Meta = upd.Meta
}
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeUpdate(upd, u)); err != nil {
return
}
if err = uniqueUserCheck(ctx, svc.store, u); err != nil {
return
}
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
if label.Changed(u.Labels, upd.Labels) {
if err = label.Update(ctx, svc.store, upd); err != nil {
return
}
u.Labels = upd.Labels
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterUpdate(upd, u))
return
}()
return u, svc.recordAction(ctx, uaProps, UserActionUpdate, err)
}
func (svc user) ToggleEmailConfirmation(ctx context.Context, userID uint64, confirmed bool) (err error) {
var (
u *types.User
uaProps = &userActionProps{}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return
}
uaProps.setUser(u)
if userID != internalAuth.GetIdentityFromContext(ctx).Identity() {
if !svc.ac.CanUpdateUser(ctx, u) {
return UserErrNotAllowedToUpdate()
}
}
u.EmailConfirmed = confirmed
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
return
}()
return svc.recordAction(ctx, uaProps, UserActionUpdate, err)
}
func (svc user) UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (out *types.User, err error) {
// @todo: avatar
return svc.Create(ctx, mod)
}
func (svc user) Delete(ctx context.Context, userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return
}
if !svc.ac.CanDeleteUser(ctx, u) {
return UserErrNotAllowedToDelete()
}
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeDelete(nil, u)); err != nil {
return
}
u.DeletedAt = now()
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterDelete(nil, u))
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionDelete, err)
}
func (svc user) Undelete(ctx context.Context, userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return
}
uaProps.setUser(u)
if err = uniqueUserCheck(ctx, svc.store, u); err != nil {
return err
}
if !svc.ac.CanDeleteUser(ctx, u) {
return UserErrNotAllowedToDelete()
}
u.DeletedAt = nil
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionUndelete, err)
}
func (svc user) Suspend(ctx context.Context, userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return
}
uaProps.setUser(u)
if !svc.ac.CanSuspendUser(ctx, u) {
return UserErrNotAllowedToSuspend()
}
// Clone u to oldUser
oldUser := *u
u.SuspendedAt = now()
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeSuspend(u, &oldUser)); err != nil {
return
}
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterSuspend(u, &oldUser))
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionSuspend, err)
}
func (svc user) Unsuspend(ctx context.Context, userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return
}
uaProps.setUser(u)
if !svc.ac.CanUnsuspendUser(ctx, u) {
return UserErrNotAllowedToUnsuspend()
}
u.SuspendedAt = nil
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionUnsuspend, err)
}
// SetPassword sets new password for a user
//
// Expecting setter to have permissions to update modify users and internal authentication enabled
func (svc user) SetPassword(ctx context.Context, userID uint64, newPassword string) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() error {
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
return err
}
uaProps.setUser(u)
if !svc.ac.CanUpdateUser(ctx, u) {
return UserErrNotAllowedToUpdate()
}
if !svc.auth.CheckPasswordStrength(newPassword) {
return UserErrPasswordNotSecure()
}
if err := svc.auth.SetPasswordCredentials(ctx, userID, newPassword); err != nil {
return err
}
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionSetPassword, err)
}
// Masks (or leaves as-is) private data on user
func (svc user) handlePrivateData(ctx context.Context, u *types.User) {
if svc.maskEmail(ctx, u) {
u.Email = maskPrivateDataEmail
}
if svc.maskName(ctx, u) {
u.Name = maskPrivateDataName
}
}
func (svc user) maskEmail(ctx context.Context, u *types.User) bool {
return svc.settings.Privacy.Mask.Email && !svc.ac.CanUnmaskEmail(ctx, u)
}
func (svc user) maskName(ctx context.Context, u *types.User) bool {
return svc.settings.Privacy.Mask.Name && !svc.ac.CanUnmaskName(ctx, u)
}
// Preloader collects all ids of users, loads them and sets them back
//
//
// @todo this kind of preloader is useful and can be implemented in bunch
// of places and replace old code
func (svc user) Preloader(ctx context.Context, g userIdGetter, f types.UserFilter, s userSetter) error {
var (
// channel that will collect the IDs in the getter
ch = make(chan uint64, 0)
// unique index for IDs
unq = make(map[uint64]bool)
)
// Reset the collection of the IDs
f.UserID = make([]uint64, 0)
// Call getter and collect the IDs
go g(ch)
rangeLoop:
for {
select {
case <-ctx.Done():
close(ch)
break rangeLoop
case id, ok := <-ch:
if !ok {
// Channel closed
break rangeLoop
}
if !unq[id] {
unq[id] = true
f.UserID = append(f.UserID, id)
}
}
}
// Load all users (even if deleted, suspended) from the given list of IDs
uu, _, err := svc.Find(ctx, f)
if err != nil {
return err
}
return uu.Walk(s)
}
// DeleteAuthTokensByUserID will delete all auth tokens of user which will un-authorize all auth clients of user
func (svc user) DeleteAuthTokensByUserID(ctx context.Context, userID uint64) (err error) {
var (
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if err = svc.store.DeleteAuthOA2TokenByUserID(ctx, userID); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionDeleteAuthTokens, err)
}
// DeleteAuthSessionsByUserID will delete all auth session of user
func (svc user) DeleteAuthSessionsByUserID(ctx context.Context, userID uint64) (err error) {
var (
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
err = func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if err = svc.store.DeleteAuthSessionsByUserID(ctx, userID); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, uaProps, UserActionDeleteAuthSessions, err)
}
// UniqueCheck verifies user's email, username and handle
func uniqueUserCheck(ctx context.Context, s store.Storer, u *types.User) (err error) {
isUnique := func(field string) bool {
f := types.UserFilter{
// If user exists and is deleted -- not a dup
Deleted: filter.StateExcluded,
// If user exists and is suspended -- duplicate
Suspended: filter.StateInclusive,
}
switch field {
case "email":
if u.Email == "" {
return true
}
f.Email = u.Email
case "username":
if u.Username == "" {
return true
}
f.Username = u.Username
case "handle":
if u.Handle == "" {
return true
}
f.Handle = u.Handle
}
set, _, err := store.SearchUsers(ctx, s, f)
if err != nil || len(set) > 1 {
// In case of error or multiple users returned
return false
}
return len(set) == 0 || set[0].ID == u.ID
}
if !isUnique("email") {
return UserErrEmailNotUnique()
}
if !isUnique("username") {
return UserErrUsernameNotUnique()
}
if !isUnique("handle") {
return UserErrHandleNotUnique()
}
return nil
}
func createUserHandle(ctx context.Context, s store.Users, u *types.User) {
if u.Handle == "" {
u.Handle, _ = handle.Cast(
// Must not exist before
func(lookup string) bool {
e, err := s.LookupUserByHandle(ctx, lookup)
return err == store.ErrNotFound && (e == nil || e.ID == u.ID)
},
// use name or username
u.Name,
u.Username,
// use email w/o domain
regexp.
MustCompile("(@.*)$").
ReplaceAllString(u.Email, ""),
//
)
}
}
// toLabeledUsers converts to []label.LabeledResource
//
// This function is auto-generated.
func toLabeledUsers(set []*types.User) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}