mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: get and update group IdP Sync settings (#14647)
--------- Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
co-authored by
Steven Masley
parent
2df9a3e554
commit
5ed065d88d
+50
-52
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
@@ -15,7 +14,9 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
type GroupParams struct {
|
||||
@@ -29,8 +30,45 @@ func (AGPLIDPSync) GroupSyncEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s AGPLIDPSync) GroupSyncSettings() runtimeconfig.RuntimeEntry[*GroupSyncSettings] {
|
||||
return s.Group
|
||||
func (s AGPLIDPSync) UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error {
|
||||
orgResolver := s.Manager.OrganizationResolver(db, orgID)
|
||||
err := s.SyncSettings.Group.SetRuntimeValue(ctx, orgResolver, &settings)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update group sync settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AGPLIDPSync) GroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*GroupSyncSettings, error) {
|
||||
orgResolver := s.Manager.OrganizationResolver(db, orgID)
|
||||
settings, err := s.SyncSettings.Group.Resolve(ctx, orgResolver)
|
||||
if err != nil {
|
||||
if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) {
|
||||
return nil, xerrors.Errorf("resolve group sync settings: %w", err)
|
||||
}
|
||||
|
||||
// Default to not being configured
|
||||
settings = &GroupSyncSettings{}
|
||||
|
||||
// Check for legacy settings if the default org.
|
||||
if s.DeploymentSyncSettings.Legacy.GroupField != "" {
|
||||
defaultOrganization, err := db.GetDefaultOrganization(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get default organization: %w", err)
|
||||
}
|
||||
if defaultOrganization.ID == orgID {
|
||||
settings = ptr.Ref(GroupSyncSettings(codersdk.GroupSyncSettings{
|
||||
Field: s.Legacy.GroupField,
|
||||
LegacyNameMapping: s.Legacy.GroupMapping,
|
||||
RegexFilter: s.Legacy.GroupFilter,
|
||||
AutoCreateMissing: s.Legacy.CreateMissingGroups,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (s AGPLIDPSync) ParseGroupClaims(_ context.Context, _ jwt.MapClaims) (GroupParams, *HTTPError) {
|
||||
@@ -48,18 +86,6 @@ func (s AGPLIDPSync) SyncGroups(ctx context.Context, db database.Store, user dat
|
||||
// nolint:gocritic // all syncing is done as a system user
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
// Only care about the default org for deployment settings if the
|
||||
// legacy deployment settings exist.
|
||||
defaultOrgID := uuid.Nil
|
||||
// Default organization is configured via legacy deployment values
|
||||
if s.DeploymentSyncSettings.Legacy.GroupField != "" {
|
||||
defaultOrganization, err := db.GetDefaultOrganization(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get default organization: %w", err)
|
||||
}
|
||||
defaultOrgID = defaultOrganization.ID
|
||||
}
|
||||
|
||||
err := db.InTx(func(tx database.Store) error {
|
||||
userGroups, err := tx.GetGroups(ctx, database.GetGroupsParams{
|
||||
HasMemberID: user.ID,
|
||||
@@ -82,25 +108,17 @@ func (s AGPLIDPSync) SyncGroups(ctx context.Context, db database.Store, user dat
|
||||
// organization.
|
||||
orgSettings := make(map[uuid.UUID]GroupSyncSettings)
|
||||
for orgID := range userOrgs {
|
||||
orgResolver := s.Manager.OrganizationResolver(tx, orgID)
|
||||
settings, err := s.SyncSettings.Group.Resolve(ctx, orgResolver)
|
||||
settings, err := s.GroupSyncSettings(ctx, orgID, tx)
|
||||
if err != nil {
|
||||
if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) {
|
||||
return xerrors.Errorf("resolve group sync settings: %w", err)
|
||||
}
|
||||
// Default to not being configured
|
||||
// TODO: This error is currently silent to org admins.
|
||||
// We need to come up with a way to notify the org admin of this
|
||||
// error.
|
||||
s.Logger.Error(ctx, "failed to get group sync settings",
|
||||
slog.F("organization_id", orgID),
|
||||
slog.Error(err),
|
||||
)
|
||||
settings = &GroupSyncSettings{}
|
||||
}
|
||||
|
||||
// Legacy deployment settings will override empty settings.
|
||||
if orgID == defaultOrgID && settings.Field == "" {
|
||||
settings = &GroupSyncSettings{
|
||||
Field: s.Legacy.GroupField,
|
||||
LegacyNameMapping: s.Legacy.GroupMapping,
|
||||
RegexFilter: s.Legacy.GroupFilter,
|
||||
AutoCreateMissing: s.Legacy.CreateMissingGroups,
|
||||
}
|
||||
}
|
||||
orgSettings[orgID] = *settings
|
||||
}
|
||||
|
||||
@@ -243,27 +261,7 @@ func (s AGPLIDPSync) ApplyGroupDifference(ctx context.Context, tx database.Store
|
||||
return nil
|
||||
}
|
||||
|
||||
type GroupSyncSettings struct {
|
||||
// Field selects the claim field to be used as the created user's
|
||||
// groups. If the group field is the empty string, then no group updates
|
||||
// will ever come from the OIDC provider.
|
||||
Field string `json:"field"`
|
||||
// Mapping maps from an OIDC group --> Coder group ID
|
||||
Mapping map[string][]uuid.UUID `json:"mapping"`
|
||||
// RegexFilter is a regular expression that filters the groups returned by
|
||||
// the OIDC provider. Any group not matched by this regex will be ignored.
|
||||
// If the group filter is nil, then no group filtering will occur.
|
||||
RegexFilter *regexp.Regexp `json:"regex_filter"`
|
||||
// AutoCreateMissing controls whether groups returned by the OIDC provider
|
||||
// are automatically created in Coder if they are missing.
|
||||
AutoCreateMissing bool `json:"auto_create_missing_groups"`
|
||||
// LegacyNameMapping is deprecated. It remaps an IDP group name to
|
||||
// a Coder group name. Since configuration is now done at runtime,
|
||||
// group IDs are used to account for group renames.
|
||||
// For legacy configurations, this config option has to remain.
|
||||
// Deprecated: Use Mapping instead.
|
||||
LegacyNameMapping map[string]string `json:"legacy_group_name_mapping,omitempty"`
|
||||
}
|
||||
type GroupSyncSettings codersdk.GroupSyncSettings
|
||||
|
||||
func (s *GroupSyncSettings) Set(v string) error {
|
||||
return json.Unmarshal([]byte(v), s)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/idpsync"
|
||||
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -84,7 +85,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
testCases := []orgSetupDefinition{
|
||||
{
|
||||
Name: "SwitchGroups",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
Mapping: map[string][]uuid.UUID{
|
||||
"foo": {ids.ID("sg-foo"), ids.ID("sg-foo-2")},
|
||||
@@ -110,7 +111,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "StayInGroup",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
// Only match foo, so bar does not map
|
||||
RegexFilter: regexp.MustCompile("^foo$"),
|
||||
@@ -130,7 +131,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "UserJoinsGroups",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
Mapping: map[string][]uuid.UUID{
|
||||
"foo": {ids.ID("ng-foo"), uuid.New()},
|
||||
@@ -153,7 +154,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "CreateGroups",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
RegexFilter: regexp.MustCompile("^create"),
|
||||
AutoCreateMissing: true,
|
||||
@@ -166,7 +167,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "GroupNamesNoMapping",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
RegexFilter: regexp.MustCompile(".*"),
|
||||
AutoCreateMissing: false,
|
||||
@@ -183,7 +184,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "NoUser",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
Mapping: map[string][]uuid.UUID{
|
||||
// Extra ID that does not map to a group
|
||||
@@ -205,7 +206,7 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "LegacyMapping",
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
RegexFilter: regexp.MustCompile("^legacy"),
|
||||
LegacyNameMapping: map[string]string{
|
||||
@@ -241,6 +242,15 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
manager,
|
||||
idpsync.DeploymentSyncSettings{
|
||||
GroupField: "groups",
|
||||
Legacy: idpsync.DefaultOrgLegacySettings{
|
||||
GroupField: "groups",
|
||||
GroupMapping: map[string]string{
|
||||
"foo": "legacy-foo",
|
||||
"baz": "legacy-baz",
|
||||
},
|
||||
GroupFilter: regexp.MustCompile("^legacy"),
|
||||
CreateMissingGroups: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -274,6 +284,8 @@ func TestGroupSyncTable(t *testing.T) {
|
||||
// Also sync the default org!
|
||||
idpsync.DeploymentSyncSettings{
|
||||
GroupField: "groups",
|
||||
// This legacy field will fail any tests if the legacy override code
|
||||
// has any bugs.
|
||||
Legacy: idpsync.DefaultOrgLegacySettings{
|
||||
GroupField: "groups",
|
||||
GroupMapping: map[string]string{
|
||||
@@ -373,7 +385,7 @@ func TestSyncDisabled(t *testing.T) {
|
||||
ids.ID("baz"): false,
|
||||
ids.ID("bop"): false,
|
||||
},
|
||||
Settings: &idpsync.GroupSyncSettings{
|
||||
Settings: &codersdk.GroupSyncSettings{
|
||||
Field: "groups",
|
||||
Mapping: map[string][]uuid.UUID{
|
||||
"foo": {ids.ID("foo")},
|
||||
@@ -716,9 +728,11 @@ func SetupOrganization(t *testing.T, s *idpsync.AGPLIDPSync, db database.Store,
|
||||
}
|
||||
|
||||
manager := runtimeconfig.NewManager()
|
||||
orgResolver := manager.OrganizationResolver(db, org.ID)
|
||||
err = s.Group.SetRuntimeValue(context.Background(), orgResolver, def.Settings)
|
||||
require.NoError(t, err)
|
||||
if def.Settings != nil {
|
||||
orgResolver := manager.OrganizationResolver(db, org.ID)
|
||||
err = s.Group.SetRuntimeValue(context.Background(), orgResolver, (*idpsync.GroupSyncSettings)(def.Settings))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if !def.NotMember {
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
@@ -759,7 +773,7 @@ type orgSetupDefinition struct {
|
||||
GroupNames map[string]bool
|
||||
NotMember bool
|
||||
|
||||
Settings *idpsync.GroupSyncSettings
|
||||
Settings *codersdk.GroupSyncSettings
|
||||
ExpectedGroups []uuid.UUID
|
||||
ExpectedGroupNames []string
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ type IDPSync interface {
|
||||
// GroupSyncSettings is exposed for the API to implement CRUD operations
|
||||
// on the settings used by IDPSync. This entry is thread safe and can be
|
||||
// accessed concurrently. The settings are stored in the database.
|
||||
GroupSyncSettings() runtimeconfig.RuntimeEntry[*GroupSyncSettings]
|
||||
GroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*GroupSyncSettings, error)
|
||||
UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error
|
||||
}
|
||||
|
||||
// AGPLIDPSync is the configuration for syncing user information from an external
|
||||
|
||||
Reference in New Issue
Block a user