mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-14 01:45:12 +00:00
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:
+62
-25
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user