feat(group): add group permissions (#10853)

# Which Problems Are Solved
Ensuring the user group resource is managed with appropriate
permissions.

# How the Problems Are Solved
By configuring and checking for the relevant permissions needed to
create, read, update, and delete the user groups resource.

# Additional Changes
N/A

# Additional Context
- Related to #9702 
- Follow-up for PRs #10455,  #10758
This commit is contained in:
Gayathri Vijayan
2025-10-21 11:18:21 +02:00
committed by GitHub
parent ff869482b1
commit ebc6c503a3
13 changed files with 1065 additions and 145 deletions
+62 -25
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
@@ -81,41 +82,34 @@ type GroupSearchQuery struct {
Queries []SearchQuery
}
func (q *Queries) GetGroupByID(ctx context.Context, id string) (group *Group, err error) {
func (q *Queries) GetGroupByID(ctx context.Context, id string, permissionCheck domain.PermissionCheck) (group *Group, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
// todo: add permission check
stmt, scan := prepareGroupQuery()
eq := sq.Eq{
GroupColumnID.identifier(): id,
GroupColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
query, args, err := stmt.Where(eq).ToSql()
group, err = q.getGroupByID(ctx, id, group)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-8bde1", "Errors.Query.SQLStatement")
return nil, err
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
group, err = scan(row)
return err
}, query, args...)
return group, err
if err = groupCheckPermission(ctx, group.ResourceOwner, group.ID, permissionCheck); err != nil {
return nil, err
}
return group, nil
}
// SearchGroups returns the list of groups that match the search criteria
func (q *Queries) SearchGroups(ctx context.Context, queries *GroupSearchQuery) (_ *Groups, err error) {
func (q *Queries) SearchGroups(ctx context.Context, queries *GroupSearchQuery, permissionCheck domain.PermissionCheck) (_ *Groups, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
// todo: add permission check
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
groups, err := q.searchGroups(ctx, queries)
groups, err := q.searchGroups(ctx, queries, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !permissionCheckV2 {
groupsCheckPermission(ctx, groups, permissionCheck)
}
return groups, nil
}
@@ -135,6 +129,36 @@ func NewGroupOrganizationIdSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(GroupColumnResourceOwner, id, TextEquals)
}
func groupCheckPermission(ctx context.Context, resourceOwner, groupID string, permissionCheck domain.PermissionCheck) error {
return permissionCheck(ctx, domain.PermissionGroupRead, resourceOwner, groupID)
}
func groupsCheckPermission(ctx context.Context, groups *Groups, permissionCheck domain.PermissionCheck) {
groups.Groups = slices.DeleteFunc(groups.Groups,
func(group *Group) bool {
return groupCheckPermission(ctx, group.ResourceOwner, group.ID, permissionCheck) != nil
},
)
}
func (q *Queries) getGroupByID(ctx context.Context, id string, group *Group) (*Group, error) {
stmt, scan := prepareGroupQuery()
eq := sq.Eq{
GroupColumnID.identifier(): id,
GroupColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
query, args, err := stmt.Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-8bde1", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
group, err = scan(row)
return err
}, query, args...)
return group, err
}
func prepareGroupQuery() (sq.SelectBuilder, func(*sql.Row) (*Group, error)) {
return sq.Select(
GroupColumnID.identifier(),
@@ -171,15 +195,14 @@ func prepareGroupQuery() (sq.SelectBuilder, func(*sql.Row) (*Group, error)) {
}
}
func (q *Queries) searchGroups(ctx context.Context, queries *GroupSearchQuery) (groups *Groups, err error) {
func (q *Queries) searchGroups(ctx context.Context, queries *GroupSearchQuery, permissionCheckV2 bool) (groups *Groups, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareGroupsQuery()
eq := sq.And{
sq.Eq{
GroupColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
},
query = groupPermissionCheckV2(ctx, query, queries.Queries, permissionCheckV2)
eq := sq.Eq{
GroupColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
@@ -197,6 +220,20 @@ func (q *Queries) searchGroups(ctx context.Context, queries *GroupSearchQuery) (
return groups, err
}
func groupPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, queries []SearchQuery, permissionCheckV2 bool) sq.SelectBuilder {
if !permissionCheckV2 {
return query
}
join, args := PermissionClause(
ctx,
GroupColumnResourceOwner,
domain.PermissionGroupRead,
SingleOrgPermissionOption(queries),
)
return query.JoinClause(join, args...)
}
func prepareGroupsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Groups, error)) {
return sq.Select(
GroupColumnID.identifier(),
+122
View File
@@ -1,6 +1,7 @@
package query
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
@@ -8,6 +9,8 @@ import (
"regexp"
"testing"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -303,3 +306,122 @@ func Test_GroupPrepares(t *testing.T) {
})
}
}
func Test_GroupsCheckPermission(t *testing.T) {
tests := []struct {
name string
want []*Group
groups *Groups
permissions []string
}{
{
name: "no permissions",
want: []*Group{},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{},
},
{
name: "permissions for all groups",
want: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group1", "group2", "group3"},
},
{
name: "permissions for group1",
want: []*Group{
{ID: "group1"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group1"},
},
{
name: "permissions for group2",
want: []*Group{
{ID: "group2"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group2"},
},
{
name: "permissions for group3",
want: []*Group{
{ID: "group3"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group3"},
},
{
name: "permissions for group1 and group2",
want: []*Group{
{ID: "group1"}, {ID: "group2"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group1", "group2"},
},
{
name: "permissions for group1 and group3",
want: []*Group{
{ID: "group1"}, {ID: "group3"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group1", "group3"},
},
{
name: "permissions for group2 and group3",
want: []*Group{
{ID: "group2"}, {ID: "group3"},
},
groups: &Groups{
Groups: []*Group{
{ID: "group1"}, {ID: "group2"}, {ID: "group3"},
},
},
permissions: []string{"group2", "group3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
for _, perm := range tt.permissions {
if resourceID == perm {
return nil
}
}
return errors.New("failed")
}
groupsCheckPermission(context.Background(), tt.groups, checkPermission)
require.Equal(t, tt.want, tt.groups.Groups)
})
}
}