diff --git a/coderd/database/dbauthz/groupsauth_test.go b/coderd/database/dbauthz/groupsauth_test.go new file mode 100644 index 0000000000000..d742e15148fa9 --- /dev/null +++ b/coderd/database/dbauthz/groupsauth_test.go @@ -0,0 +1,168 @@ +package dbauthz_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbmem" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/rbac" +) + +// nolint:tparallel +func TestGroupsAuth(t *testing.T) { + t.Parallel() + + if dbtestutil.WillUsePostgres() { + t.Skip("this test would take too long to run on postgres") + } + + authz := rbac.NewAuthorizer(prometheus.NewRegistry()) + + db := dbauthz.New(dbmem.New(), authz, slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }), coderdtest.AccessControlStorePointer()) + + ownerCtx := dbauthz.As(context.Background(), rbac.Subject{ + ID: "owner", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.RoleOwner()}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }) + + org := dbgen.Organization(t, db, database.Organization{}) + group := dbgen.Group(t, db, database.Group{ + OrganizationID: org.ID, + }) + + var users []database.User + for i := 0; i < 5; i++ { + user := dbgen.User(t, db, database.User{}) + users = append(users, user) + err := db.InsertGroupMember(ownerCtx, database.InsertGroupMemberParams{ + UserID: user.ID, + GroupID: group.ID, + }) + require.NoError(t, err) + } + + totalMembers := len(users) + testCases := []struct { + Name string + Subject rbac.Subject + ReadGroup bool + ReadMembers bool + MembersExpected int + }{ + { + Name: "Owner", + Subject: rbac.Subject{ + ID: "owner", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.RoleOwner()}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }, + ReadGroup: true, + ReadMembers: true, + MembersExpected: totalMembers, + }, + { + Name: "UserAdmin", + Subject: rbac.Subject{ + ID: "useradmin", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.RoleUserAdmin()}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }, + ReadGroup: true, + ReadMembers: true, + MembersExpected: totalMembers, + }, + { + Name: "OrgAdmin", + Subject: rbac.Subject{ + ID: "orgadmin", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.ScopedRoleOrgAdmin(org.ID)}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }, + ReadGroup: true, + ReadMembers: true, + MembersExpected: totalMembers, + }, + { + Name: "OrgUserAdmin", + Subject: rbac.Subject{ + ID: "orgUserAdmin", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.ScopedRoleOrgUserAdmin(org.ID)}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }, + ReadGroup: true, + ReadMembers: true, + MembersExpected: totalMembers, + }, + { + Name: "GroupMember", + Subject: rbac.Subject{ + ID: users[0].ID.String(), + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.ScopedRoleOrgMember(org.ID)}.Expand())), + Groups: []string{ + group.Name, + }, + 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, + }, + { + // Org admin in the incorrect organization + Name: "DifferentOrgAdmin", + Subject: rbac.Subject{ + ID: "orgadmin", + Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.ScopedRoleOrgUserAdmin(uuid.New())}.Expand())), + Groups: []string{}, + Scope: rbac.ExpandableScope(rbac.ScopeAll), + }, + ReadGroup: false, + ReadMembers: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + actorCtx := dbauthz.As(context.Background(), tc.Subject) + _, err := db.GetGroupByID(actorCtx, group.ID) + if tc.ReadGroup { + require.NoError(t, err, "group read") + } else { + require.Error(t, err, "group read") + } + + members, err := db.GetGroupMembersByGroupID(actorCtx, group.ID) + if tc.ReadMembers { + 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") + } + }) + } +}