Files
upstream-corteza/server/pkg/rbac/user_groups.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

630 lines
13 KiB
Go

package rbac
import (
"errors"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/stack"
"go.uber.org/zap"
)
type (
orgTree struct {
logger *zap.Logger
root *groupNode
// branchIndex provides all of the available sub-trees in the given orgTree
//
// This is primarily meant to speed up the process of determining what
// permissions should be included in access evaluation
//
// @todo benchmark with/without to see if it makes sense
branchIndex map[id.ID]*groupNode
// memberGroupIndex holds what user group a given member belongs to
memberGroupIndex map[id.ID]*groupNode
// groupMemberIndex holds what members a specific user group holds
groupMemberIndex map[id.ID][]id.ID
}
groupNode struct {
id id.ID
handle string
// Nodes this one is parent of
children []groupNodeConnection
// Nodes this one is child of
//
// Used to simplify lookup
parents []groupNodeConnection
// Slice of paths this node was provided (this node -> parents)
paths []GroupNodePath
roles []id.ID
}
GroupNodePath struct {
SelfID id.ID
Name string
}
groupNodeConnection struct {
node *groupNode
name string
}
GroupMembers struct {
group *groupNode
members []id.ID
}
)
// ConvUserGroup creates internal structures for the org tree
func ConvUserGroup(id id.ID, handle string, members, roles []id.ID, paths []GroupNodePath) GroupMembers {
return GroupMembers{
group: &groupNode{
id: id,
handle: handle,
roles: roles,
paths: paths,
},
members: members,
}
}
// OrgTree prepares the organization tree subservice
func OrgTree(log *zap.Logger, gm ...GroupMembers) (svc *orgTree, err error) {
svc, err = mkOrgTree(gm...)
if err != nil {
return
}
svc.logger = log
err = svc.Rebuild(gm...)
return
}
func mkOrgTree(gm ...GroupMembers) (svc *orgTree, err error) {
svc = &orgTree{
logger: zap.NewNop(),
}
err = svc.Rebuild(gm...)
return
}
// Rebuild rebuilds the current organization tree dropping all currently indexed data
func (svc *orgTree) Rebuild(gm ...GroupMembers) (err error) {
svc.root = nil
svc.branchIndex = nil
svc.memberGroupIndex = nil
svc.groupMemberIndex = nil
gg := make([]*groupNode, 0, len(gm))
for _, m := range gm {
gg = append(gg, m.group)
}
svc.root, svc.branchIndex, err = buildOrgTree(gg...)
if err != nil {
return
}
for _, m := range gm {
err = svc.AssignGroupMembers(m.group.id, m.members...)
if err != nil {
return
}
}
return
}
func (svc *orgTree) AddNode(id id.ID, handle string, paths ...GroupNodePath) (err error) {
svc.logger.Debug("adding node", zap.Any("id", id.Value()), zap.String("handle", handle), zap.Any("paths", formatPaths(paths)))
n := &groupNode{
id: id,
handle: handle,
paths: paths,
}
err = svc.addNode(n)
if err != nil {
return
}
return
}
func (svc *orgTree) addNode(node *groupNode) (err error) {
if svc.root == nil {
svc.root = node
if svc.branchIndex == nil {
svc.branchIndex = make(map[id.ID]*groupNode)
}
svc.branchIndex[node.id] = node
if svc.groupMemberIndex == nil {
svc.groupMemberIndex = make(map[id.ID][]id.ID)
}
svc.groupMemberIndex[node.id] = []id.ID{}
return
}
for _, p := range node.paths {
i, n := svc.findNode(p.SelfID)
if i < 0 {
return fmt.Errorf("cannot add node path: parent node %v not found", p.SelfID)
}
n.children = append(n.children, groupNodeConnection{
node: node,
name: p.Name,
})
node.parents = append(node.parents, groupNodeConnection{
node: n,
name: p.Name,
})
}
svc.branchIndex[node.id] = node
svc.groupMemberIndex[node.id] = []id.ID{}
return
}
func (svc *orgTree) UpdateNode(idx id.ID, handle string, paths ...GroupNodePath) (err error) {
svc.logger.Debug("updating node", zap.Any("id", idx.Value()), zap.String("handle", handle), zap.Any("paths", formatPaths(paths)))
i, n := svc.findNode(idx)
if i < 0 {
return fmt.Errorf("cannot update node %v (%s): not indexed", idx.Value(), handle)
}
oldPaths := n.paths
n.handle = handle
n.paths = paths
if svc.branchIndex == nil {
svc.branchIndex = make(map[id.ID]*groupNode)
}
for _, op := range oldPaths {
i = svc.isInConnections(svc.branchIndex[op.SelfID].children, idx)
svc.branchIndex[op.SelfID].children = append(svc.branchIndex[op.SelfID].children[:i], svc.branchIndex[op.SelfID].children[i+1:]...)
i = svc.isInConnections(svc.branchIndex[idx].parents, op.SelfID)
svc.branchIndex[idx].parents = append(svc.branchIndex[idx].parents[:i], svc.branchIndex[idx].parents[i+1:]...)
}
for _, p := range paths {
svc.branchIndex[p.SelfID].children = append(svc.branchIndex[p.SelfID].children, groupNodeConnection{
node: n,
name: p.Name,
})
svc.branchIndex[idx].parents = append(svc.branchIndex[idx].parents, groupNodeConnection{
node: svc.branchIndex[p.SelfID],
name: p.Name,
})
}
return
}
func (svc *orgTree) RemoveNode(id id.ID) (err error) {
svc.logger.Debug("removing node", zap.Any("id", id.Value()))
i, n := svc.findNode(id)
if i < 0 {
return fmt.Errorf("cannot delete node %v: not indexed", id.Value())
}
if len(svc.groupMemberIndex[id]) > 0 {
return fmt.Errorf("cannot remove node %v: node has members", id)
}
if len(n.children) > 0 {
return fmt.Errorf("cannot remove node %v: node is above a node ", id)
}
for _, n := range svc.root.inline() {
i := svc.isInConnections(n.children, id)
if i < 0 {
continue
}
n.children = append(n.children[:i], n.children[i+1:]...)
}
delete(svc.branchIndex, id)
delete(svc.groupMemberIndex, id)
return
}
// GroupMembers returns all of the members belonging to the specific group
func (svc *orgTree) GroupMembers(group id.ID) (members []id.ID) {
if svc.memberGroupIndex == nil {
return
}
return svc.groupMemberIndex[group]
}
// IsAbove checks if parentUser is above the childUser
func (svc *orgTree) IsAbove(childUser, parentUser id.ID, paths ...string) bool {
childGroup := svc.memberGroupIndex[childUser]
parentGroup := svc.memberGroupIndex[parentUser]
return svc.isAbove(parentGroup, childGroup, paths...)
}
// isAbove checks if parent is above child based on the hierarchy
//
// The check goes bottom up as the number of paths will (in most cases) always be lower.
func (svc *orgTree) isAbove(parent, child *groupNode, paths ...string) (ok bool) {
checkConnection := func(c groupNodeConnection) bool {
if len(paths) == 0 {
return true
}
if c.name == "" {
return true
}
for _, pth := range paths {
if c.name == pth {
return true
}
}
return false
}
stack := stack.Stack[*groupNode]{}
for _, p := range child.parents {
if !checkConnection(p) {
continue
}
stack.Push(p.node)
}
for !stack.Empty() {
n := stack.Pop()
if n.id.Equal(parent.id) {
return true
}
for _, c := range n.parents {
if !checkConnection(c) {
continue
}
stack.Push(c.node)
}
}
return false
}
// MemberBranch returns the branch belonging to the user
func (svc *orgTree) MemberBranch(user id.ID, paths ...string) (group []*groupNode, err error) {
if svc == nil {
return
}
if svc.memberGroupIndex == nil {
return
}
aux, ok := svc.memberGroupIndex[user]
if !ok {
return nil, fmt.Errorf("user %s is not a member of any group", user.Value())
}
ss, ok := svc.branchIndex[aux.id]
if !ok {
return nil, fmt.Errorf("group %s not found in subtree index", aux.id.Value())
}
return ss.inline(paths...), nil
}
func (svc *orgTree) AddGroupRole(group id.ID, roles ...id.ID) (err error) {
svc.logger.Debug("adding group roles", zap.Any("group", group.Value()), zap.Any("roles", id.StringifySlice(roles...)))
i, n := svc.findNode(group)
if i < 0 {
return fmt.Errorf("node %v not found", group)
}
for _, r := range roles {
if id.InSlice(r, n.roles...) {
continue
}
n.roles = append(n.roles, r)
}
return
}
func (svc *orgTree) RemoveGroupRole(group id.ID, roles ...id.ID) (err error) {
svc.logger.Debug("removing group roles", zap.Any("group", group.Value()), zap.Any("roles", id.StringifySlice(roles...)))
i, n := svc.findNode(group)
if i < 0 {
return fmt.Errorf("node %v not found", group)
}
for _, r := range roles {
n.roles = id.RemoveFromSlice(r, n.roles...)
}
return
}
// AssignGroupMembers assigns the members to the specified group
func (svc *orgTree) AssignGroupMembers(group id.ID, members ...id.ID) (err error) {
if group.IsZero() {
return
}
svc.logger.Debug("assigning group members", zap.Any("group", group.Value()), zap.Any("roles", id.StringifySlice(members...)))
if svc.memberGroupIndex == nil {
svc.memberGroupIndex = make(map[id.ID]*groupNode)
}
if svc.groupMemberIndex == nil {
svc.groupMemberIndex = make(map[id.ID][]id.ID)
}
g := svc.branchIndex[group]
if g == nil {
return fmt.Errorf("group %s not found", group.Value())
}
for _, u := range members {
oldGroup := svc.memberGroupIndex[u]
// Cleanup previous state
if oldGroup != nil {
svc.groupMemberIndex[oldGroup.id] = id.RemoveFromSlice(u, svc.groupMemberIndex[oldGroup.id]...)
}
// Handle new state
svc.memberGroupIndex[u] = g
if !id.InSlice(u, svc.groupMemberIndex[group]...) {
svc.groupMemberIndex[group] = append(svc.groupMemberIndex[group], u)
}
}
return
}
// RemoveGroupMembers removes the members from the given user group
func (svc *orgTree) RemoveGroupMembers(group id.ID, members ...id.ID) (err error) {
if svc == nil {
return
}
svc.logger.Debug("assigning group members", zap.Any("group", group.Value()), zap.Any("roles", id.StringifySlice(members...)))
if svc.memberGroupIndex == nil {
return
}
for _, m := range members {
g, ok := svc.memberGroupIndex[m]
if !ok {
err = fmt.Errorf("user %s is not a member of any group", m.Value())
return
}
if !g.id.Equal(group) {
err = fmt.Errorf("user %s is not a member of group %s", m.Value(), group.Value())
return
}
// Cleanup
delete(svc.memberGroupIndex, m)
svc.groupMemberIndex[group] = id.RemoveFromSlice(m, svc.groupMemberIndex[group]...)
}
return
}
func buildOrgTree(gg ...*groupNode) (root *groupNode, index map[id.ID]*groupNode, err error) {
mapped := make(map[id.ID]*groupNode, len(gg))
for _, g := range gg {
mapped[g.id] = g
if len(g.paths) == 0 {
if root != nil {
return nil, nil, errors.New("multiple root nodes detected")
}
root = g
}
}
for _, g := range gg {
if len(g.paths) == 0 {
continue
}
for _, p := range g.paths {
parent, ok := mapped[p.SelfID]
if !ok {
return nil, nil, fmt.Errorf("node %s parent %s not found", g.id.Value(), p.SelfID.Value())
}
parent.children = append(parent.children, groupNodeConnection{
name: p.Name,
node: g,
})
g.parents = append(g.parents, groupNodeConnection{
node: parent,
name: p.Name,
})
}
}
index = make(map[id.ID]*groupNode, len(gg))
for _, g := range gg {
index[g.id] = g
}
return root, index, nil
}
// inline converts the subtree in a slice using BFS
func (root *groupNode) inline(paths ...string) []*groupNode {
if root == nil {
return nil
}
// Helper to check if a connection should be included based on path
//
// - It we don't specify any path, all nodes are used
// - If a connection name is empty, the node is included regardless of defined paths
// - If a connection defines a name, it must occur in the requested paths slice
inclConnection := func(c groupNodeConnection) bool {
if len(paths) == 0 {
return true
}
if c.name == "" {
return true
}
for _, pth := range paths {
if c.name == pth {
return true
}
}
return false
}
// outSeen helps us filter out duplicates
outSeen := make(map[id.ID]bool, 4)
out := make([]*groupNode, 0)
fifo := make([]*groupNode, 0)
fifo = append(fifo, root)
i := 0
for {
n := fifo[i]
if outSeen[n.id] {
if i+1 >= len(fifo) {
break
}
i++
continue
}
for _, c := range n.children {
if !inclConnection(c) {
continue
}
fifo = append(fifo, c.node)
}
out = append(out, n)
outSeen[n.id] = true
if i+1 >= len(fifo) {
break
}
i++
}
return out
}
func formatPaths(paths []GroupNodePath) []map[string]any {
out := make([]map[string]any, 0, len(paths))
for _, p := range paths {
out = append(out, map[string]any{
"selfID": p.SelfID,
"name": p.Name,
})
}
return out
}
func (n *groupNode) format() map[string]any {
out := map[string]any{
"id": n.id.Value(),
"handle": n.handle,
"paths": formatPaths(n.paths),
"roles": id.StringifySlice(n.roles...),
}
cc := make([]id.ID, 0, len(n.children))
for _, c := range n.children {
cc = append(cc, c.node.id)
}
out["children"] = id.StringifySlice(cc...)
return out
}
func (svc *orgTree) findNode(id id.ID) (i int, n *groupNode) {
nn := svc.root.inline()
i = 0
found := false
for i = range nn {
n = nn[i]
if n.id.Equal(id) {
found = true
break
}
}
if !found {
n = nil
i = -1
}
return
}
func (svc *orgTree) isInConnections(ss []groupNodeConnection, id id.ID) int {
for i, s := range ss {
if s.node.id.Equal(id) {
return i
}
}
return -1
}