Files
upstream-corteza/server/system/service/user_group.go
upstream-mirror 7e385fc126
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

733 lines
17 KiB
Go

package service
import (
"context"
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service/event"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
userGroup struct {
actionlog actionlog.Recorder
ac userGroupAccessController
eventbus eventDispatcher
rbac rbacUserGroupService
user UserService
role RoleService
rootUserGroup id.ID
store store.Storer
}
userGroupAccessController interface {
CanGrant(context.Context) bool
CanSearchUserGroups(context.Context) bool
CanCreateUserGroup(context.Context) bool
CanReadUserGroup(context.Context, *types.UserGroup) bool
CanUpdateUserGroup(context.Context, *types.UserGroup) bool
CanDeleteUserGroup(context.Context, *types.UserGroup) bool
CanManageMembersOnUserGroup(context.Context, *types.UserGroup) bool
}
UserGroupService interface {
FindByID(ctx context.Context, userGroupID uint64) (*types.UserGroup, error)
FindByHandle(ctx context.Context, handle string) (*types.UserGroup, error)
Find(context.Context, types.UserGroupFilter) (types.UserGroupSet, types.UserGroupFilter, error)
Create(ctx context.Context, userGroup *types.UserGroup) (*types.UserGroup, error)
Update(ctx context.Context, userGroup *types.UserGroup) (*types.UserGroup, error)
Delete(ctx context.Context, ID uint64) error
Undelete(ctx context.Context, ID uint64) error
MemberList(ctx context.Context, userGroupID uint64) (types.UserSet, error)
MemberAdd(ctx context.Context, userGroupID, userID uint64) error
}
eventbusUserGroupChangeRegistry interface {
Register(eventbus.HandlerFn, ...eventbus.HandlerRegOp) uintptr
}
rbacUserGroupService interface {
UpdateUserGroups(rr ...rbac.GroupMembers) (err error)
AssignGroupMembers(group id.ID, members ...id.ID) (err error)
AddNode(id id.ID, handle string, paths ...rbac.GroupNodePath) (err error)
UpdateNode(id id.ID, handle string, paths ...rbac.GroupNodePath) (err error)
RemoveNode(id id.ID) (err error)
}
userGroupAuth interface {
RemoveAccessTokens(context.Context, *types.User) error
}
)
func UserGroup(rbac rbacUserGroupService) *userGroup {
return &userGroup{
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
rbac: rbac,
actionlog: DefaultActionlog,
user: DefaultUser,
role: DefaultRole,
store: DefaultStore,
}
}
func (svc *userGroup) Activate(ctx context.Context) (err error) {
gMembers := []rbac.GroupMembers{}
groups, _, err := svc.Find(ctx, types.UserGroupFilter{})
if err != nil {
return
}
for _, g := range groups {
if len(g.Config.Paths) == 0 {
svc.rootUserGroup = id.MustNumID(g.ID)
}
roles, _, err := svc.role.Find(ctx, types.RoleFilter{
Resource: fmt.Sprintf("corteza::system:user-group/%d", g.ID),
})
if err != nil {
return err
}
var mm types.UserSet
mm, err = svc.MemberList(ctx, g.ID)
if err != nil {
return err
}
var rr []id.ID
for _, r := range roles {
rr = append(rr, id.MustNumID(r.ID))
}
members := make([]id.ID, len(mm))
for i, m := range mm {
members[i] = id.MustNumID(m.ID)
}
pp := []rbac.GroupNodePath{}
for _, p := range g.Config.Paths {
pp = append(pp, rbac.GroupNodePath{
SelfID: id.MustNumID(p.SelfID),
Name: p.Name,
})
}
// @todo we need to reload this on specific changes
gMembers = append(gMembers, rbac.ConvUserGroup(id.MustNumID(g.ID), g.Handle, members, rr, pp))
}
err = svc.rbac.UpdateUserGroups(gMembers...)
if err != nil {
return
}
return
}
func (svc *userGroup) Find(ctx context.Context, filter types.UserGroupFilter) (rr types.UserGroupSet, f types.UserGroupFilter, err error) {
var (
raProps = &userGroupActionProps{filter: &filter}
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = func(res *types.UserGroup) (bool, error) {
if !svc.ac.CanReadUserGroup(ctx, res) {
return false, nil
}
return true, nil
}
err = func() error {
if !svc.ac.CanSearchUserGroups(ctx) {
return UserGroupErrNotAllowedToSearch()
}
if filter.Deleted > 0 {
// If list with deleted or suspended 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 or archived userGroups
//if !svc.ac.CanAccess(ctx) {
// return UserGroupErrNotAllowedToListUserGroups()
//}
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.UserGroup{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if rr, f, err = store.SearchUserGroups(ctx, svc.store, filter); err != nil {
return err
}
for _, r := range rr {
r.IsRoot = id.MustNumID(r.ID).Equal(svc.rootUserGroup)
}
if err = label.Load(ctx, svc.store, toLabeledUserGroups(rr)...); err != nil {
return err
}
return nil
}()
return rr, f, svc.recordAction(ctx, raProps, UserGroupActionSearch, err)
}
func (svc *userGroup) FindByID(ctx context.Context, userGroupID uint64) (r *types.UserGroup, err error) {
var (
raProps = &userGroupActionProps{userGroup: &types.UserGroup{ID: userGroupID}}
)
err = func() error {
if r, err = svc.findByID(ctx, userGroupID); err != nil {
return err
}
r.IsRoot = id.MustNumID(r.ID).Equal(svc.rootUserGroup)
raProps.setUserGroup(r)
return nil
}()
return r, svc.recordAction(ctx, raProps, UserGroupActionLookup, err)
}
func (svc *userGroup) findByID(ctx context.Context, userGroupID uint64) (*types.UserGroup, error) {
r, err := loadUserGroup(ctx, svc.store, userGroupID)
return svc.proc(ctx, r, err)
}
func (svc *userGroup) FindByHandle(ctx context.Context, h string) (r *types.UserGroup, err error) {
var (
raProps = &userGroupActionProps{userGroup: &types.UserGroup{Handle: h}}
)
err = func() error {
r, err = store.LookupUserGroupByHandle(ctx, svc.store, h)
if r, err = svc.proc(ctx, r, err); err != nil {
return err
}
raProps.setUserGroup(r)
return nil
}()
return r, svc.recordAction(ctx, raProps, UserGroupActionLookup, err)
}
// FindByAny finds userGroup by given identifier (id, handle, name)
func (svc *userGroup) FindByAny(ctx context.Context, identifier interface{}) (r *types.UserGroup, err error) {
if ID, ok := identifier.(uint64); ok {
return svc.FindByID(ctx, ID)
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
return svc.FindByID(ctx, ID)
} else {
r, err = svc.FindByHandle(ctx, strIdentifier)
return r, err
}
} else {
return nil, UserGroupErrInvalidID()
}
}
func (svc *userGroup) proc(ctx context.Context, r *types.UserGroup, err error) (*types.UserGroup, error) {
if err != nil {
if errors.IsNotFound(err) {
return nil, UserGroupErrNotFound()
}
return nil, err
}
if err = label.Load(ctx, svc.store, r); err != nil {
return nil, err
}
return r, nil
}
func (svc *userGroup) Create(ctx context.Context, new *types.UserGroup) (r *types.UserGroup, err error) {
var (
raProps = &userGroupActionProps{userGroup: new}
)
err = func() (err error) {
if !handle.IsValid(new.Handle) {
return UserGroupErrInvalidHandle()
}
if !svc.checkPaths(new) {
return UserGroupErrInvalidSelfID()
}
if !svc.isValidStructure(ctx, new) {
return UserGroupErrInvalidUpdateStructure()
}
if !svc.ac.CanCreateUserGroup(ctx) {
return UserGroupErrNotAllowedToCreate()
}
if err = svc.eventbus.WaitFor(ctx, event.UserGroupBeforeCreate(new, r)); err != nil {
return
}
if err = svc.UniqueCheck(ctx, new); err != nil {
return
}
new.ID = nextID()
new.CreatedAt = *now()
raProps.setNew(new)
if err = store.CreateUserGroup(ctx, svc.store, new); err != nil {
return
}
if err = label.Create(ctx, svc.store, new); err != nil {
return
}
r = new
pp := []rbac.GroupNodePath{}
for _, p := range new.Config.Paths {
pp = append(pp, rbac.GroupNodePath{
SelfID: id.MustNumID(p.SelfID),
Name: p.Name,
})
}
err = svc.rbac.AddNode(id.MustNumID(new.ID), new.Handle, pp...)
if err != nil {
return
}
svc.eventbus.Dispatch(ctx, event.UserGroupAfterCreate(new, r))
return
}()
return r, svc.recordAction(ctx, raProps, UserGroupActionCreate, err)
}
func (svc *userGroup) Update(ctx context.Context, upd *types.UserGroup) (r *types.UserGroup, err error) {
var (
raProps = &userGroupActionProps{update: upd}
)
err = func() (err error) {
if r, err = loadUserGroup(ctx, svc.store, upd.ID); err != nil {
return
}
raProps.setUserGroup(r)
if !handle.IsValid(upd.Handle) {
return UserGroupErrInvalidHandle()
}
if !svc.ac.CanUpdateUserGroup(ctx, upd) {
return UserGroupErrNotAllowedToUpdate()
}
if len(upd.Config.Paths) == 0 {
return UserGroupErrMissingSelfID()
}
if !svc.checkSelfID(ctx, upd) {
return UserGroupErrInvalidSelfID()
}
if !svc.isValidStructure(ctx, upd) {
return UserGroupErrInvalidUpdateStructure()
}
// Test if stale (update has an older version of data)
if isStale(upd.UpdatedAt, r.UpdatedAt, r.CreatedAt) {
return UserGroupErrStaleData()
}
if err = svc.eventbus.WaitFor(ctx, event.UserGroupBeforeUpdate(upd, r)); err != nil {
return
}
if err = svc.UniqueCheck(ctx, upd); err != nil {
return
}
r.Handle = upd.Handle
r.Meta = upd.Meta
r.UpdatedAt = now()
r.Config = upd.Config
// Assign changed values
if err = store.UpdateUserGroup(ctx, svc.store, r); err != nil {
return err
}
if label.Changed(r.Labels, upd.Labels) {
if err = label.Update(ctx, svc.store, upd); err != nil {
return
}
r.Labels = upd.Labels
}
pp := []rbac.GroupNodePath{}
for _, p := range r.Config.Paths {
pp = append(pp, rbac.GroupNodePath{
SelfID: id.MustNumID(p.SelfID),
Name: p.Name,
})
}
err = svc.rbac.UpdateNode(id.MustNumID(r.ID), r.Handle, pp...)
if err != nil {
return
}
svc.eventbus.Dispatch(ctx, event.UserGroupAfterUpdate(upd, r))
return nil
}()
return r, svc.recordAction(ctx, raProps, UserGroupActionUpdate, err)
}
func (svc *userGroup) UniqueCheck(ctx context.Context, r *types.UserGroup) (err error) {
var (
raProps = &userGroupActionProps{userGroup: r}
)
if r.Handle != "" {
if ex, _ := store.LookupUserGroupByHandle(ctx, svc.store, r.Handle); ex != nil && ex.ID > 0 && ex.ID != r.ID {
raProps.setExisting(ex)
return UserGroupErrHandleNotUnique()
}
}
return nil
}
func (svc *userGroup) Delete(ctx context.Context, userGroupID uint64) (err error) {
var (
r *types.UserGroup
raProps = &userGroupActionProps{userGroup: &types.UserGroup{ID: userGroupID}}
)
err = func() (err error) {
if r, err = svc.findByID(ctx, userGroupID); err != nil {
return err
}
raProps.setUserGroup(r)
if !svc.ac.CanDeleteUserGroup(ctx, r) {
return UserGroupErrNotAllowedToDelete()
}
if err = svc.referencedByAuthClient(ctx, r.ID); err != nil {
return err
}
if err = svc.eventbus.WaitFor(ctx, event.UserGroupBeforeDelete(nil, r)); err != nil {
return
}
r.DeletedAt = now()
if err = store.UpdateUserGroup(ctx, svc.store, r); err != nil {
return
}
err = svc.rbac.RemoveNode(id.MustNumID(r.ID))
if err != nil {
return
}
svc.eventbus.Dispatch(ctx, event.UserGroupAfterDelete(nil, r))
return
}()
return svc.recordAction(ctx, raProps, UserGroupActionDelete, err)
}
// referencedByAuthClient prevents deleting a user group that is used as the
// security user group on any auth client
func (svc *userGroup) referencedByAuthClient(ctx context.Context, userGroupID uint64) error {
if userGroupID == 0 {
return nil
}
cc, _, err := store.SearchAuthClients(ctx, svc.store, types.AuthClientFilter{})
if err != nil {
return err
}
for _, c := range cc {
if c.Security != nil && c.Security.UserGroup == userGroupID {
return UserGroupErrNotAllowedToDelete()
}
}
return nil
}
func (svc *userGroup) Undelete(ctx context.Context, userGroupID uint64) (err error) {
var (
r, upd *types.UserGroup
raProps = &userGroupActionProps{userGroup: &types.UserGroup{ID: userGroupID}}
)
err = func() (err error) {
if r, err = svc.findByID(ctx, userGroupID); err != nil {
return err
}
upd = r.Clone()
if err = svc.eventbus.WaitFor(ctx, event.UserGroupBeforeUpdate(upd, r)); err != nil {
return
}
raProps.setUserGroup(upd)
if !svc.ac.CanDeleteUserGroup(ctx, upd) {
return UserGroupErrNotAllowedToDelete()
}
upd.DeletedAt = nil
if err = store.UpdateUserGroup(ctx, svc.store, upd); err != nil {
return
}
pp := []rbac.GroupNodePath{}
for _, p := range upd.Config.Paths {
pp = append(pp, rbac.GroupNodePath{
SelfID: id.MustNumID(p.SelfID),
Name: p.Name,
})
}
err = svc.rbac.AddNode(id.MustNumID(upd.ID), upd.Handle, pp...)
if err != nil {
return
}
svc.eventbus.Dispatch(ctx, event.UserGroupAfterUpdate(upd, r))
return nil
}()
return svc.recordAction(ctx, raProps, UserGroupActionUndelete, err)
}
func (svc *userGroup) MemberList(ctx context.Context, userGroupID uint64) (mm types.UserSet, err error) {
var (
r *types.UserGroup
raProps = &userGroupActionProps{
userGroup: &types.UserGroup{ID: userGroupID},
}
)
err = func() error {
if userGroupID == 0 {
return UserGroupErrInvalidID()
}
if r, err = svc.findByID(ctx, userGroupID); err != nil {
return err
}
if !svc.ac.CanReadUserGroup(ctx, r) {
return UserGroupErrNotAllowedToRead()
}
mm, _, err = store.SearchUsers(ctx, svc.store, types.UserFilter{
UserGroupID: userGroupID,
})
return err
}()
return mm, svc.recordAction(ctx, raProps, UserGroupActionMembers, err)
}
// MemberAdd adds member (user) to a userGroup
func (svc *userGroup) MemberAdd(ctx context.Context, userGroupID, memberID uint64) (err error) {
var (
g *types.UserGroup
m *types.User
raProps = &userGroupActionProps{
userGroup: &types.UserGroup{ID: userGroupID},
member: &types.User{ID: memberID},
}
)
err = func() (err error) {
if userGroupID == 0 || memberID == 0 {
return UserGroupErrInvalidID()
}
if g, err = svc.findByID(ctx, userGroupID); err != nil {
return
}
raProps.setUserGroup(g)
if m, err = svc.user.FindByID(ctx, memberID); err != nil {
return
}
raProps.setMember(m)
m.UserGroupID = g.ID
if err = svc.eventbus.WaitFor(ctx, event.UserGroupBeforeMemberAdd(g, g)); err != nil {
return
}
if !svc.ac.CanManageMembersOnUserGroup(ctx, g) {
return UserGroupErrNotAllowedToManageMembers()
}
if err = store.UpdateUser(ctx, svc.store, m); err != nil {
return
}
err = svc.rbac.AssignGroupMembers(id.MustNumID(m.UserGroupID), id.MustNumID(m.ID))
if err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserGroupAfterMemberAdd(g, g))
return nil
}()
return svc.recordAction(ctx, raProps, UserGroupActionMemberAdd, err)
}
func loadUserGroup(ctx context.Context, s store.UserGroups, ID uint64) (res *types.UserGroup, err error) {
if ID == 0 {
return nil, UserGroupErrInvalidID()
}
if res, err = store.LookupUserGroupByID(ctx, s, ID); errors.IsNotFound(err) {
return nil, UserGroupErrNotFound()
}
return
}
// toLabeledUserGroups converts to []label.LabeledResource
//
// This function is auto-generated.
func toLabeledUserGroups(set []*types.UserGroup) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}
func (svc *userGroup) isValidStructure(ctx context.Context, g *types.UserGroup) (ok bool) {
// @todo :)
return true
}
func (svc *userGroup) checkSelfID(ctx context.Context, g *types.UserGroup) bool {
for _, p := range g.Config.Paths {
// Can't point to itself
if p.SelfID == g.ID {
return false
}
// The pointed to selfID exists
_, err := svc.FindByID(ctx, p.SelfID)
if err != nil {
return false
}
}
return true
}
func (svc *userGroup) checkPaths(g *types.UserGroup) (ok bool) {
if id.MustNumID(g.ID).Equal(svc.rootUserGroup) {
return len(g.Config.Paths) == 0
}
if g.Config == nil {
g.Config = &types.UserGroupConfig{}
}
if len(g.Config.Paths) == 0 {
return false
}
names := make(map[string]bool, len(g.Config.Paths)/2)
for _, p := range g.Config.Paths {
if p.SelfID == 0 {
return false
}
// Do not allow duplicate paths
if names[p.Name] {
return false
}
names[p.Name] = true
}
return true
}