Skip to content

fix: allow group members to read group information #14200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c6d037
- allow group members to read basic Group info
hugodutka Aug 6, 2024
2409072
rename ReducedGroup.member_count to ReducedGroup.total_member_count
hugodutka Aug 6, 2024
5391c15
use the reduced groups for user endpoint on the AccountPage
hugodutka Aug 6, 2024
5fcc218
rename UserWithGroupAndOrgID to GroupMemberRBACHelper
hugodutka Aug 7, 2024
2878c21
add the group_members_expanded db view
hugodutka Aug 9, 2024
3aa2a23
add comment on migration
hugodutka Aug 9, 2024
e428d40
rewrite group member queries to use the group_members_expanded view
hugodutka Aug 9, 2024
13ca3a9
add the RBAC ResourceGroupMember and add it to relevant roles
hugodutka Aug 9, 2024
1027d9f
rewrite GetGroupMembersByGroupID permission checks
hugodutka Aug 9, 2024
0aea7f8
- make the GroupMember type contain all user fields
hugodutka Aug 9, 2024
e5c5f3f
simplify RBAC check on group member count
hugodutka Aug 9, 2024
16e95d0
add the MemberTotalCount field to codersdk.Group
hugodutka Aug 9, 2024
d3b4d7c
remove the reduced groups endpoint, revert frontend to use the groups…
hugodutka Aug 9, 2024
b06f943
revert "simplify RBAC check on group member count" - new query didn't…
hugodutka Aug 9, 2024
4a65874
display `group.total_member_count` instead of `group.members.length` …
hugodutka Aug 9, 2024
1f4dcc7
adjust `total_member_count` on `MockGroup`
hugodutka Aug 9, 2024
f1513e0
fixes after rebase
hugodutka Aug 9, 2024
9ef0e0d
simplify RBAC check on GetGroupMembersCountByGroupID
hugodutka Aug 9, 2024
7627933
fix tests
hugodutka Aug 12, 2024
1080b29
resolve lint error
hugodutka Aug 12, 2024
19486da
update the groupsauth test to work with new group member permissions
hugodutka Aug 12, 2024
5373dd7
fix a mistake in test
hugodutka Aug 12, 2024
08646da
make ErrUserDeleted private
hugodutka Aug 13, 2024
4ec6adb
`dbgen.GroupMember` now fails when a supplied user or group doesn't e…
hugodutka Aug 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 39 additions & 9 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,35 @@ func ReducedUser(user database.User) codersdk.ReducedUser {
}
}

func UserFromGroupMember(member database.GroupMember) database.User {
return database.User{
ID: member.UserID,
Email: member.UserEmail,
Username: member.UserUsername,
HashedPassword: member.UserHashedPassword,
CreatedAt: member.UserCreatedAt,
UpdatedAt: member.UserUpdatedAt,
Status: member.UserStatus,
RBACRoles: member.UserRbacRoles,
LoginType: member.UserLoginType,
AvatarURL: member.UserAvatarUrl,
Deleted: member.UserDeleted,
LastSeenAt: member.UserLastSeenAt,
QuietHoursSchedule: member.UserQuietHoursSchedule,
ThemePreference: member.UserThemePreference,
Name: member.UserName,
GithubComUserID: member.UserGithubComUserID,
}
}

func ReducedUserFromGroupMember(member database.GroupMember) codersdk.ReducedUser {
return ReducedUser(UserFromGroupMember(member))
}

func ReducedUsersFromGroupMembers(members []database.GroupMember) []codersdk.ReducedUser {
return List(members, ReducedUserFromGroupMember)
}

func ReducedUsers(users []database.User) []codersdk.ReducedUser {
return List(users, ReducedUser)
}
Expand All @@ -179,16 +208,17 @@ func Users(users []database.User, organizationIDs map[uuid.UUID][]uuid.UUID) []c
})
}

func Group(group database.Group, members []database.User) codersdk.Group {
func Group(group database.Group, members []database.GroupMember, totalMemberCount int) codersdk.Group {
return codersdk.Group{
ID: group.ID,
Name: group.Name,
DisplayName: group.DisplayName,
OrganizationID: group.OrganizationID,
AvatarURL: group.AvatarURL,
Members: ReducedUsers(members),
QuotaAllowance: int(group.QuotaAllowance),
Source: codersdk.GroupSource(group.Source),
ID: group.ID,
Name: group.Name,
DisplayName: group.DisplayName,
OrganizationID: group.OrganizationID,
AvatarURL: group.AvatarURL,
Members: ReducedUsersFromGroupMembers(members),
TotalMemberCount: totalMemberCount,
QuotaAllowance: int(group.QuotaAllowance),
Source: codersdk.GroupSource(group.Source),
}
}

Expand Down
16 changes: 12 additions & 4 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1396,11 +1396,19 @@ func (q *querier) GetGroupMembers(ctx context.Context) ([]database.GroupMember,
return q.db.GetGroupMembers(ctx)
}

func (q *querier) GetGroupMembersByGroupID(ctx context.Context, id uuid.UUID) ([]database.User, error) {
if _, err := q.GetGroupByID(ctx, id); err != nil { // AuthZ check
return nil, err
func (q *querier) GetGroupMembersByGroupID(ctx context.Context, id uuid.UUID) ([]database.GroupMember, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupID)(ctx, id)
}

func (q *querier) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) {
if _, err := q.GetGroupByID(ctx, groupID); err != nil { // AuthZ check
return 0, err
}
memberCount, err := q.db.GetGroupMembersCountByGroupID(ctx, groupID)
if err != nil {
return 0, err
}
return q.db.GetGroupMembersByGroupID(ctx, id)
return memberCount, nil
}

func (q *querier) GetGroups(ctx context.Context) ([]database.Group, error) {
Expand Down
24 changes: 17 additions & 7 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,10 @@ func (s *MethodTestSuite) TestGroup() {
}))
s.Run("DeleteGroupMemberFromGroup", s.Subtest(func(db database.Store, check *expects) {
g := dbgen.Group(s.T(), db, database.Group{})
m := dbgen.GroupMember(s.T(), db, database.GroupMember{
u := dbgen.User(s.T(), db, database.User{})
m := dbgen.GroupMember(s.T(), db, database.GroupMemberTable{
GroupID: g.ID,
UserID: u.ID,
})
check.Args(database.DeleteGroupMemberFromGroupParams{
UserID: m.UserID,
Expand All @@ -326,11 +328,18 @@ func (s *MethodTestSuite) TestGroup() {
}))
s.Run("GetGroupMembersByGroupID", s.Subtest(func(db database.Store, check *expects) {
g := dbgen.Group(s.T(), db, database.Group{})
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{})
u := dbgen.User(s.T(), db, database.User{})
gm := dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID})
check.Args(g.ID).Asserts(gm, policy.ActionRead)
}))
s.Run("GetGroupMembersCountByGroupID", s.Subtest(func(db database.Store, check *expects) {
g := dbgen.Group(s.T(), db, database.Group{})
check.Args(g.ID).Asserts(g, policy.ActionRead)
}))
s.Run("GetGroupMembers", s.Subtest(func(db database.Store, check *expects) {
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{})
g := dbgen.Group(s.T(), db, database.Group{})
u := dbgen.User(s.T(), db, database.User{})
dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID})
check.Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
s.Run("GetGroups", s.Subtest(func(db database.Store, check *expects) {
Expand All @@ -339,7 +348,8 @@ func (s *MethodTestSuite) TestGroup() {
}))
s.Run("GetGroupsByOrganizationAndUserID", s.Subtest(func(db database.Store, check *expects) {
g := dbgen.Group(s.T(), db, database.Group{})
gm := dbgen.GroupMember(s.T(), db, database.GroupMember{GroupID: g.ID})
u := dbgen.User(s.T(), db, database.User{})
gm := dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID})
check.Args(database.GetGroupsByOrganizationAndUserIDParams{
OrganizationID: g.OrganizationID,
UserID: gm.UserID,
Expand Down Expand Up @@ -368,7 +378,7 @@ func (s *MethodTestSuite) TestGroup() {
u1 := dbgen.User(s.T(), db, database.User{})
g1 := dbgen.Group(s.T(), db, database.Group{OrganizationID: o.ID})
g2 := dbgen.Group(s.T(), db, database.Group{OrganizationID: o.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{GroupID: g1.ID, UserID: u1.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g1.ID, UserID: u1.ID})
check.Args(database.InsertUserGroupsByNameParams{
OrganizationID: o.ID,
UserID: u1.ID,
Expand All @@ -380,8 +390,8 @@ func (s *MethodTestSuite) TestGroup() {
u1 := dbgen.User(s.T(), db, database.User{})
g1 := dbgen.Group(s.T(), db, database.Group{OrganizationID: o.ID})
g2 := dbgen.Group(s.T(), db, database.Group{OrganizationID: o.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{GroupID: g1.ID, UserID: u1.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{GroupID: g2.ID, UserID: u1.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g1.ID, UserID: u1.ID})
_ = dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g2.ID, UserID: u1.ID})
check.Args(u1.ID).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns()
}))
s.Run("UpdateGroupByID", s.Subtest(func(db database.Store, check *expects) {
Expand Down
16 changes: 6 additions & 10 deletions coderd/database/dbauthz/groupsauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,15 @@ func TestGroupsAuth(t *testing.T) {
Name: "GroupMember",
Subject: rbac.Subject{
ID: users[0].ID.String(),
Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(org.ID)}.Expand())),
Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(org.ID)}.Expand())),
Groups: []string{
group.Name,
group.ID.String(),
},
Scope: rbac.ExpandableScope(rbac.ScopeAll),
},
// TODO: currently group members cannot see their own groups.
// If this is fixed, these booleans should be flipped to true.
ReadGroup: false,
ReadMembers: false,
// TODO: If fixed, they should only be able to see themselves
// MembersExpected: 1,
ReadGroup: true,
ReadMembers: true,
MembersExpected: 1,
},
{
// Org admin in the incorrect organization
Expand Down Expand Up @@ -160,8 +157,7 @@ func TestGroupsAuth(t *testing.T) {
require.NoError(t, err, "member read")
require.Len(t, members, tc.MembersExpected, "member count found does not match")
} else {
require.Error(t, err, "member read")
require.True(t, dbauthz.IsNotAuthorizedError(err), "not authorized error")
require.Len(t, members, 0, "member count is not 0")
}
})
}
Expand Down
56 changes: 50 additions & 6 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"strings"
Expand Down Expand Up @@ -374,18 +375,61 @@ func Group(t testing.TB, db database.Store, orig database.Group) database.Group
return group
}

func GroupMember(t testing.TB, db database.Store, orig database.GroupMember) database.GroupMember {
member := database.GroupMember{
UserID: takeFirst(orig.UserID, uuid.New()),
GroupID: takeFirst(orig.GroupID, uuid.New()),
}
// GroupMember requires a user + group to already exist.
// Example for creating a group member for a random group + user.
//
// GroupMember(t, db, database.GroupMemberTable{
// UserID: User(t, db, database.User{}).ID,
// GroupID: Group(t, db, database.Group{
// OrganizationID: must(db.GetDefaultOrganization(genCtx)).ID,
// }).ID,
// })
func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTable) database.GroupMember {
require.NotEqual(t, member.UserID, uuid.Nil, "A user id is required to use 'dbgen.GroupMember', use 'dbgen.User'.")
require.NotEqual(t, member.GroupID, uuid.Nil, "A group id is required to use 'dbgen.GroupMember', use 'dbgen.Group'.")

//nolint:gosimple
err := db.InsertGroupMember(genCtx, database.InsertGroupMemberParams{
UserID: member.UserID,
GroupID: member.GroupID,
})
require.NoError(t, err, "insert group member")
return member

user, err := db.GetUserByID(genCtx, member.UserID)
if errors.Is(err, sql.ErrNoRows) {
require.NoErrorf(t, err, "'dbgen.GroupMember' failed as the user with id %s does not exist. A user is required to use this function, use 'dbgen.User'.", member.UserID)
}
require.NoError(t, err, "get user by id")

group, err := db.GetGroupByID(genCtx, member.GroupID)
if errors.Is(err, sql.ErrNoRows) {
require.NoErrorf(t, err, "'dbgen.GroupMember' failed as the group with id %s does not exist. A group is required to use this function, use 'dbgen.Group'.", member.GroupID)
}
require.NoError(t, err, "get group by id")

groupMember := database.GroupMember{
UserID: user.ID,
UserEmail: user.Email,
UserUsername: user.Username,
UserHashedPassword: user.HashedPassword,
UserCreatedAt: user.CreatedAt,
UserUpdatedAt: user.UpdatedAt,
UserStatus: user.Status,
UserRbacRoles: user.RBACRoles,
UserLoginType: user.LoginType,
UserAvatarUrl: user.AvatarURL,
UserDeleted: user.Deleted,
UserLastSeenAt: user.LastSeenAt,
UserQuietHoursSchedule: user.QuietHoursSchedule,
UserThemePreference: user.ThemePreference,
UserName: user.Name,
UserGithubComUserID: user.GithubComUserID,
OrganizationID: group.OrganizationID,
GroupName: group.Name,
GroupID: group.ID,
}

return groupMember
}

// ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps
Expand Down
4 changes: 2 additions & 2 deletions coderd/database/dbgen/dbgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ func TestGenerator(t *testing.T) {
db := dbmem.New()
g := dbgen.Group(t, db, database.Group{})
u := dbgen.User(t, db, database.User{})
exp := []database.User{u}
dbgen.GroupMember(t, db, database.GroupMember{GroupID: g.ID, UserID: u.ID})
gm := dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID})
exp := []database.GroupMember{gm}

require.Equal(t, exp, must(db.GetGroupMembersByGroupID(context.Background(), g.ID)))
})
Expand Down
Loading
Loading