mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: support multi-org group sync with runtime configuration (#14578)
- Implement multi-org group sync - Implement runtime configuration to change sync behavior - Legacy group sync migrated to new package
This commit is contained in:
@@ -2892,6 +2892,14 @@ func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams)
|
||||
return insert(q.log, q.auth, obj, q.db.InsertUser)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertUserGroupsByID(ctx context.Context, arg database.InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
|
||||
// This is used by OIDC sync. So only used by a system user.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.InsertUserGroupsByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertUserGroupsByName(ctx context.Context, arg database.InsertUserGroupsByNameParams) error {
|
||||
// This will add the user to all named groups. This counts as updating a group.
|
||||
// NOTE: instead of checking if the user has permission to update each group, we instead
|
||||
@@ -3100,6 +3108,14 @@ func (q *querier) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID)
|
||||
return q.db.RemoveUserFromAllGroups(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) RemoveUserFromGroups(ctx context.Context, arg database.RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
|
||||
// This is a system function to clear user groups in group sync.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.RemoveUserFromGroups(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
|
||||
@@ -388,6 +388,17 @@ func (s *MethodTestSuite) TestGroup() {
|
||||
GroupNames: slice.New(g1.Name, g2.Name),
|
||||
}).Asserts(rbac.ResourceGroup.InOrg(o.ID), policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("InsertUserGroupsByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
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.GroupMemberTable{GroupID: g1.ID, UserID: u1.ID})
|
||||
check.Args(database.InsertUserGroupsByIDParams{
|
||||
UserID: u1.ID,
|
||||
GroupIds: slice.New(g1.ID, g2.ID),
|
||||
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(slice.New(g1.ID, g2.ID))
|
||||
}))
|
||||
s.Run("RemoveUserFromAllGroups", s.Subtest(func(db database.Store, check *expects) {
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
u1 := dbgen.User(s.T(), db, database.User{})
|
||||
@@ -397,6 +408,18 @@ func (s *MethodTestSuite) TestGroup() {
|
||||
_ = 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("RemoveUserFromGroups", s.Subtest(func(db database.Store, check *expects) {
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
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.GroupMemberTable{GroupID: g1.ID, UserID: u1.ID})
|
||||
_ = dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g2.ID, UserID: u1.ID})
|
||||
check.Args(database.RemoveUserFromGroupsParams{
|
||||
UserID: u1.ID,
|
||||
GroupIds: []uuid.UUID{g1.ID, g2.ID},
|
||||
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(slice.New(g1.ID, g2.ID))
|
||||
}))
|
||||
s.Run("UpdateGroupByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
g := dbgen.Group(s.T(), db, database.Group{})
|
||||
check.Args(database.UpdateGroupByIDParams{
|
||||
|
||||
@@ -2695,18 +2695,18 @@ func (q *FakeQuerier) GetGroups(_ context.Context, arg database.GetGroupsParams)
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
groupIDs := make(map[uuid.UUID]struct{})
|
||||
userGroupIDs := make(map[uuid.UUID]struct{})
|
||||
if arg.HasMemberID != uuid.Nil {
|
||||
for _, member := range q.groupMembers {
|
||||
if member.UserID == arg.HasMemberID {
|
||||
groupIDs[member.GroupID] = struct{}{}
|
||||
userGroupIDs[member.GroupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the everyone group
|
||||
for _, orgMember := range q.organizationMembers {
|
||||
if orgMember.UserID == arg.HasMemberID {
|
||||
groupIDs[orgMember.OrganizationID] = struct{}{}
|
||||
userGroupIDs[orgMember.OrganizationID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2718,11 +2718,15 @@ func (q *FakeQuerier) GetGroups(_ context.Context, arg database.GetGroupsParams)
|
||||
continue
|
||||
}
|
||||
|
||||
_, ok := groupIDs[group.ID]
|
||||
_, ok := userGroupIDs[group.ID]
|
||||
if arg.HasMemberID != uuid.Nil && !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(arg.GroupNames) > 0 && !slices.Contains(arg.GroupNames, group.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
orgDetails, ok := orgDetailsCache[group.ID]
|
||||
if !ok {
|
||||
for _, org := range q.organizations {
|
||||
@@ -7015,7 +7019,37 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertUserGroupsByID(_ context.Context, arg database.InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
var groupIDs []uuid.UUID
|
||||
for _, group := range q.groups {
|
||||
for _, groupID := range arg.GroupIds {
|
||||
if group.ID == groupID {
|
||||
q.groupMembers = append(q.groupMembers, database.GroupMemberTable{
|
||||
UserID: arg.UserID,
|
||||
GroupID: groupID,
|
||||
})
|
||||
groupIDs = append(groupIDs, group.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertUserGroupsByName(_ context.Context, arg database.InsertUserGroupsByNameParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
@@ -7607,6 +7641,34 @@ func (q *FakeQuerier) RemoveUserFromAllGroups(_ context.Context, userID uuid.UUI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) RemoveUserFromGroups(_ context.Context, arg database.RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
removed := make([]uuid.UUID, 0)
|
||||
q.data.groupMembers = slices.DeleteFunc(q.data.groupMembers, func(groupMember database.GroupMemberTable) bool {
|
||||
// Delete all group members that match the arguments.
|
||||
if groupMember.UserID != arg.UserID {
|
||||
// Not the right user, ignore.
|
||||
return false
|
||||
}
|
||||
|
||||
if !slices.Contains(arg.GroupIds, groupMember.GroupID) {
|
||||
return false
|
||||
}
|
||||
|
||||
removed = append(removed, groupMember.GroupID)
|
||||
return true
|
||||
})
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) RevokeDBCryptKey(_ context.Context, activeKeyDigest string) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
@@ -1789,6 +1789,13 @@ func (m metricsStore) InsertUser(ctx context.Context, arg database.InsertUserPar
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertUserGroupsByID(ctx context.Context, arg database.InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertUserGroupsByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertUserGroupsByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertUserGroupsByName(ctx context.Context, arg database.InsertUserGroupsByNameParams) error {
|
||||
start := time.Now()
|
||||
err := m.s.InsertUserGroupsByName(ctx, arg)
|
||||
@@ -1943,6 +1950,13 @@ func (m metricsStore) RemoveUserFromAllGroups(ctx context.Context, userID uuid.U
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) RemoveUserFromGroups(ctx context.Context, arg database.RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.RemoveUserFromGroups(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("RemoveUserFromGroups").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.RevokeDBCryptKey(ctx, activeKeyDigest)
|
||||
|
||||
@@ -3766,6 +3766,21 @@ func (mr *MockStoreMockRecorder) InsertUser(arg0, arg1 any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUser", reflect.TypeOf((*MockStore)(nil).InsertUser), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertUserGroupsByID mocks base method.
|
||||
func (m *MockStore) InsertUserGroupsByID(arg0 context.Context, arg1 database.InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertUserGroupsByID", arg0, arg1)
|
||||
ret0, _ := ret[0].([]uuid.UUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertUserGroupsByID indicates an expected call of InsertUserGroupsByID.
|
||||
func (mr *MockStoreMockRecorder) InsertUserGroupsByID(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserGroupsByID", reflect.TypeOf((*MockStore)(nil).InsertUserGroupsByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertUserGroupsByName mocks base method.
|
||||
func (m *MockStore) InsertUserGroupsByName(arg0 context.Context, arg1 database.InsertUserGroupsByNameParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -4103,6 +4118,21 @@ func (mr *MockStoreMockRecorder) RemoveUserFromAllGroups(arg0, arg1 any) *gomock
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserFromAllGroups", reflect.TypeOf((*MockStore)(nil).RemoveUserFromAllGroups), arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveUserFromGroups mocks base method.
|
||||
func (m *MockStore) RemoveUserFromGroups(arg0 context.Context, arg1 database.RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveUserFromGroups", arg0, arg1)
|
||||
ret0, _ := ret[0].([]uuid.UUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RemoveUserFromGroups indicates an expected call of RemoveUserFromGroups.
|
||||
func (mr *MockStoreMockRecorder) RemoveUserFromGroups(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserFromGroups", reflect.TypeOf((*MockStore)(nil).RemoveUserFromGroups), arg0, arg1)
|
||||
}
|
||||
|
||||
// RevokeDBCryptKey mocks base method.
|
||||
func (m *MockStore) RevokeDBCryptKey(arg0 context.Context, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -369,6 +369,9 @@ type sqlcQuerier interface {
|
||||
InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error)
|
||||
InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error)
|
||||
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
|
||||
// InsertUserGroupsByID adds a user to all provided groups, if they exist.
|
||||
// If there is a conflict, the user is already a member
|
||||
InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error)
|
||||
// InsertUserGroupsByName adds a user to all provided groups, if they exist.
|
||||
InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error
|
||||
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
|
||||
@@ -396,6 +399,7 @@ type sqlcQuerier interface {
|
||||
ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error
|
||||
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||
RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error
|
||||
RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error)
|
||||
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
|
||||
// Non blocking lock. Returns true if the lock was acquired, false otherwise.
|
||||
//
|
||||
|
||||
@@ -1446,6 +1446,56 @@ func (q *sqlQuerier) InsertGroupMember(ctx context.Context, arg InsertGroupMembe
|
||||
return err
|
||||
}
|
||||
|
||||
const insertUserGroupsByID = `-- name: InsertUserGroupsByID :many
|
||||
WITH groups AS (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
groups
|
||||
WHERE
|
||||
groups.id = ANY($2 :: uuid [])
|
||||
)
|
||||
INSERT INTO
|
||||
group_members (user_id, group_id)
|
||||
SELECT
|
||||
$1,
|
||||
groups.id
|
||||
FROM
|
||||
groups
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING group_id
|
||||
`
|
||||
|
||||
type InsertUserGroupsByIDParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
|
||||
}
|
||||
|
||||
// InsertUserGroupsByID adds a user to all provided groups, if they exist.
|
||||
// If there is a conflict, the user is already a member
|
||||
func (q *sqlQuerier) InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.QueryContext(ctx, insertUserGroupsByID, arg.UserID, pq.Array(arg.GroupIds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []uuid.UUID
|
||||
for rows.Next() {
|
||||
var group_id uuid.UUID
|
||||
if err := rows.Scan(&group_id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, group_id)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertUserGroupsByName = `-- name: InsertUserGroupsByName :exec
|
||||
WITH groups AS (
|
||||
SELECT
|
||||
@@ -1489,6 +1539,43 @@ func (q *sqlQuerier) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UU
|
||||
return err
|
||||
}
|
||||
|
||||
const removeUserFromGroups = `-- name: RemoveUserFromGroups :many
|
||||
DELETE FROM
|
||||
group_members
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
group_id = ANY($2 :: uuid [])
|
||||
RETURNING group_id
|
||||
`
|
||||
|
||||
type RemoveUserFromGroupsParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
|
||||
rows, err := q.db.QueryContext(ctx, removeUserFromGroups, arg.UserID, pq.Array(arg.GroupIds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []uuid.UUID
|
||||
for rows.Next() {
|
||||
var group_id uuid.UUID
|
||||
if err := rows.Scan(&group_id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, group_id)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const deleteGroupByID = `-- name: DeleteGroupByID :exec
|
||||
DELETE FROM
|
||||
groups
|
||||
@@ -1592,11 +1679,16 @@ WHERE
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
AND CASE WHEN array_length($3 :: text[], 1) > 0 THEN
|
||||
groups.name = ANY($3)
|
||||
ELSE true
|
||||
END
|
||||
`
|
||||
|
||||
type GetGroupsParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
HasMemberID uuid.UUID `db:"has_member_id" json:"has_member_id"`
|
||||
GroupNames []string `db:"group_names" json:"group_names"`
|
||||
}
|
||||
|
||||
type GetGroupsRow struct {
|
||||
@@ -1606,7 +1698,7 @@ type GetGroupsRow struct {
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getGroups, arg.OrganizationID, arg.HasMemberID)
|
||||
rows, err := q.db.QueryContext(ctx, getGroups, arg.OrganizationID, arg.HasMemberID, pq.Array(arg.GroupNames))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -29,12 +29,41 @@ SELECT
|
||||
FROM
|
||||
groups;
|
||||
|
||||
-- InsertUserGroupsByID adds a user to all provided groups, if they exist.
|
||||
-- name: InsertUserGroupsByID :many
|
||||
WITH groups AS (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
groups
|
||||
WHERE
|
||||
groups.id = ANY(@group_ids :: uuid [])
|
||||
)
|
||||
INSERT INTO
|
||||
group_members (user_id, group_id)
|
||||
SELECT
|
||||
@user_id,
|
||||
groups.id
|
||||
FROM
|
||||
groups
|
||||
-- If there is a conflict, the user is already a member
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING group_id;
|
||||
|
||||
-- name: RemoveUserFromAllGroups :exec
|
||||
DELETE FROM
|
||||
group_members
|
||||
WHERE
|
||||
user_id = @user_id;
|
||||
|
||||
-- name: RemoveUserFromGroups :many
|
||||
DELETE FROM
|
||||
group_members
|
||||
WHERE
|
||||
user_id = @user_id AND
|
||||
group_id = ANY(@group_ids :: uuid [])
|
||||
RETURNING group_id;
|
||||
|
||||
-- name: InsertGroupMember :exec
|
||||
INSERT INTO
|
||||
group_members (user_id, group_id)
|
||||
|
||||
@@ -52,6 +52,10 @@ WHERE
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
AND CASE WHEN array_length(@group_names :: text[], 1) > 0 THEN
|
||||
groups.name = ANY(@group_names)
|
||||
ELSE true
|
||||
END
|
||||
;
|
||||
|
||||
-- name: InsertGroup :one
|
||||
|
||||
Reference in New Issue
Block a user