Skip to content

chore: move organizatinon sync to runtime configuration #15431

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 9 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
chore: add endpoint to mutate org sync settings
  • Loading branch information
Emyrk committed Nov 7, 2024
commit d1d041904cd79f8dc24642af1b4b954c2976ea2c
3 changes: 2 additions & 1 deletion coderd/idpsync/idpsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
// claims to the internal representation of a user in Coder.
// TODO: Move group + role sync into this interface.
type IDPSync interface {
AssignDefaultOrganization() bool
OrganizationSyncEntitled() bool
OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error)
UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error
// OrganizationSyncEnabled returns true if all OIDC users are assigned
// to organizations via org sync settings.
// This is used to know when to disable manual org membership assignment.
Expand Down
30 changes: 13 additions & 17 deletions coderd/idpsync/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ func (AGPLIDPSync) OrganizationSyncEnabled(_ context.Context, _ database.Store)
return false
}

func (s AGPLIDPSync) AssignDefaultOrganization() bool {
return s.OrganizationAssignDefault
}

func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error {
rlv := s.Manager.Resolver(db)
err := s.SyncSettings.Organization.SetRuntimeValue(ctx, rlv, &settings)
Expand All @@ -62,9 +58,9 @@ func (s AGPLIDPSync) OrganizationSyncSettings(ctx context.Context, db database.S
if s.DeploymentSyncSettings.OrganizationField != "" {
// Use static settings if set
orgSettings = &OrganizationSyncSettings{
OrganizationField: s.DeploymentSyncSettings.OrganizationField,
OrganizationMapping: s.DeploymentSyncSettings.OrganizationMapping,
OrganizationAssignDefault: s.DeploymentSyncSettings.OrganizationAssignDefault,
Field: s.DeploymentSyncSettings.OrganizationField,
Mapping: s.DeploymentSyncSettings.OrganizationMapping,
AssignDefault: s.DeploymentSyncSettings.OrganizationAssignDefault,
}
}
}
Expand Down Expand Up @@ -92,7 +88,7 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u
return xerrors.Errorf("failed to get org sync settings: %w", err)
}

if orgSettings.OrganizationField == "" {
if orgSettings.Field == "" {
return nil // No sync configured, nothing to do
}

Expand Down Expand Up @@ -156,16 +152,16 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u
}

type OrganizationSyncSettings struct {
// OrganizationField selects the claim field to be used as the created user's
// Field selects the claim field to be used as the created user's
// organizations. If the field is the empty string, then no organization updates
// will ever come from the OIDC provider.
OrganizationField string
// OrganizationMapping controls how organizations returned by the OIDC provider get mapped
OrganizationMapping map[string][]uuid.UUID
// OrganizationAssignDefault will ensure all users that authenticate will be
Field string
// Mapping controls how organizations returned by the OIDC provider get mapped
Mapping map[string][]uuid.UUID
// AssignDefault will ensure all users that authenticate will be
// placed into the default organization. This is mostly a hack to support
// legacy deployments.
OrganizationAssignDefault bool
AssignDefault bool
}

func (s *OrganizationSyncSettings) Set(v string) error {
Expand All @@ -181,7 +177,7 @@ func (s *OrganizationSyncSettings) String() string {
func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.Store, mergedClaims jwt.MapClaims) ([]uuid.UUID, error) {
userOrganizations := make([]uuid.UUID, 0)

if s.OrganizationAssignDefault {
if s.AssignDefault {
// This is a bit hacky, but if AssignDefault is included, then always
// make sure to include the default org in the list of expected.
defaultOrg, err := db.GetDefaultOrganization(ctx)
Expand All @@ -193,7 +189,7 @@ func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.
userOrganizations = append(userOrganizations, defaultOrg.ID)
}

organizationRaw, ok := mergedClaims[s.OrganizationField]
organizationRaw, ok := mergedClaims[s.Field]
if !ok {
return userOrganizations, nil
}
Expand All @@ -205,7 +201,7 @@ func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.

// add any mapped organizations
for _, parsedOrg := range parsedOrganizations {
if mappedOrganization, ok := s.OrganizationMapping[parsedOrg]; ok {
if mappedOrganization, ok := s.Mapping[parsedOrg]; ok {
// parsedOrg is in the mapping, so add the mapped organizations to the
// user's organizations.
userOrganizations = append(userOrganizations, mappedOrganization...)
Expand Down
2 changes: 2 additions & 0 deletions coderd/rbac/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceGroupMember.Type: {policy.ActionRead},
// Manage org membership based on OIDC claims
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
}),
Org: map[string][]Permission{},
User: []Permission{},
Expand Down
19 changes: 17 additions & 2 deletions coderd/rbac/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,10 +733,25 @@ func TestRolePermissions(t *testing.T) {
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgUserAdmin},
true: {owner, orgAdmin, orgUserAdmin, userAdmin},
false: {
orgMemberMe, otherOrgAdmin,
memberMe, userAdmin, templateAdmin,
memberMe, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
{
Name: "OrganizationIDPSyncSettings",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin},
false: {
orgAdmin, orgUserAdmin,
orgMemberMe, otherOrgAdmin,
memberMe, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
Expand Down
40 changes: 40 additions & 0 deletions codersdk/idpsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,43 @@ func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req
var resp RoleSyncSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}

type OrganizationSyncSettings struct {
// Field selects the claim field to be used as the created user's
// organizations. If the field is the empty string, then no organization
// updates will ever come from the OIDC provider.
Field string `json:"field"`
// Mapping maps from an OIDC claim --> Coder organization uuid
Mapping map[string][]uuid.UUID `json:"mapping"`
// AssignDefault will ensure the default org is always included
// for every user, regardless of their claims. This preserves legacy behavior.
AssignDefault bool `json:"organization_assign_default"`
}

func (c *Client) OrganizationIDPSyncSettings(ctx context.Context) (OrganizationSyncSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/settings/idpsync/organization", nil)
if err != nil {
return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return OrganizationSyncSettings{}, ReadBodyAsError(res)
}
var resp OrganizationSyncSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}

func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req OrganizationSyncSettings) (OrganizationSyncSettings, error) {
res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization", req)
if err != nil {
return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return OrganizationSyncSettings{}, ReadBodyAsError(res)
}
var resp OrganizationSyncSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
10 changes: 10 additions & 0 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole)
})

r.Group(func(r chi.Router) {
r.Use(
apiKeyMiddleware,
)
r.Route("/settings/idpsync/organization", func(r chi.Router) {
r.Get("/", api.organizationIDPSyncSettings)
r.Patch("/", api.patchOrganizationIDPSyncSettings)
})
})

r.Group(func(r chi.Router) {
r.Use(
apiKeyMiddleware,
Expand Down
2 changes: 1 addition & 1 deletion enterprise/coderd/enidpsync/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (e EnterpriseIDPSync) OrganizationSyncEnabled(ctx context.Context, db datab
}

settings, err := e.OrganizationSyncSettings(ctx, db)
if err == nil && settings.OrganizationField != "" {
if err == nil && settings.Field != "" {
return true
}
return false
Expand Down
6 changes: 3 additions & 3 deletions enterprise/coderd/enidpsync/organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,11 @@ func TestOrganizationSync(t *testing.T) {
},
// Override
RuntimeSettings: &idpsync.OrganizationSyncSettings{
OrganizationField: "dynamic",
OrganizationMapping: map[string][]uuid.UUID{
Field: "dynamic",
Mapping: map[string][]uuid.UUID{
"third": {three.ID},
},
OrganizationAssignDefault: false,
AssignDefault: false,
},
Exps: []Expectations{
{
Expand Down
103 changes: 98 additions & 5 deletions enterprise/coderd/idpsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Enterprise
// @Param organization path string true "Organization ID" format(uuid)
// @Param request body codersdk.GroupSyncSettings true
// @Success 200 {object} codersdk.GroupSyncSettings
// @Router /organizations/{organization}/settings/idpsync/groups [patch]
func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
Expand All @@ -57,7 +58,7 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques
return
}

var req idpsync.GroupSyncSettings
var req codersdk.GroupSyncSettings
if !httpapi.Read(ctx, rw, r, &req) {
return
}
Expand All @@ -78,7 +79,13 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques

//nolint:gocritic // Requires system context to update runtime config
sysCtx := dbauthz.AsSystemRestricted(ctx)
err := api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, req)
err := api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{
Field: req.Field,
Mapping: req.Mapping,
RegexFilter: req.RegexFilter,
AutoCreateMissing: req.AutoCreateMissing,
LegacyNameMapping: req.LegacyNameMapping,
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
Expand All @@ -90,7 +97,13 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques
return
}

httpapi.Write(ctx, rw, http.StatusOK, settings)
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{
Field: settings.Field,
Mapping: settings.Mapping,
RegexFilter: settings.RegexFilter,
AutoCreateMissing: settings.AutoCreateMissing,
LegacyNameMapping: settings.LegacyNameMapping,
})
}

// @Summary Get role IdP Sync settings by organization
Expand Down Expand Up @@ -127,6 +140,7 @@ func (api *API) roleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Enterprise
// @Param organization path string true "Organization ID" format(uuid)
// @Param request body codersdk.RoleSyncSettings true
// @Success 200 {object} codersdk.RoleSyncSettings
// @Router /organizations/{organization}/settings/idpsync/roles [patch]
func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
Expand All @@ -138,14 +152,17 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request
return
}

var req idpsync.RoleSyncSettings
var req codersdk.RoleSyncSettings
if !httpapi.Read(ctx, rw, r, &req) {
return
}

//nolint:gocritic // Requires system context to update runtime config
sysCtx := dbauthz.AsSystemRestricted(ctx)
err := api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, req)
err := api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{
Field: req.Field,
Mapping: req.Mapping,
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
Expand All @@ -157,5 +174,81 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request
return
}

httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{
Field: settings.Field,
Mapping: settings.Mapping,
})
}

// @Summary Get organization IdP Sync settings
// @ID get-organization-idp-sync-settings
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Success 200 {object} codersdk.OrganizationSyncSettings
// @Router /settings/idpsync/organization [get]
func (api *API) organizationIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

if !api.Authorize(r, policy.ActionRead, rbac.ResourceIdpsyncSettings) {
httpapi.Forbidden(rw)
return
}

//nolint:gocritic // Requires system context to read runtime config
sysCtx := dbauthz.AsSystemRestricted(ctx)
settings, err := api.IDPSync.OrganizationSyncSettings(sysCtx, api.Database)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}

httpapi.Write(ctx, rw, http.StatusOK, settings)
}

// @Summary Update organization IdP Sync settings
// @ID update-organization-idp-sync-settings
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Success 200 {object} codersdk.OrganizationSyncSettings
// @Param request body codersdk.OrganizationSyncSettings true
// @Router /settings/idpsync/organization [patch]
func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) {
httpapi.Forbidden(rw)
return
}

var req codersdk.OrganizationSyncSettings
if !httpapi.Read(ctx, rw, r, &req) {
return
}

//nolint:gocritic // Requires system context to update runtime config
sysCtx := dbauthz.AsSystemRestricted(ctx)
err := api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{
Field: req.Field,
// We do not check if the mappings point to actual organizations.
Mapping: req.Mapping,
AssignDefault: req.AssignDefault,
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}

settings, err := api.IDPSync.OrganizationSyncSettings(sysCtx, api.Database)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}

httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{
Field: settings.Field,
Mapping: settings.Mapping,
AssignDefault: settings.AssignDefault,
})
}
8 changes: 7 additions & 1 deletion enterprise/coderd/scim.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,13 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
// the default org, regardless if sync is enabled or not.
// This is to preserve single org deployment behavior.
organizations := []uuid.UUID{}
if api.IDPSync.AssignDefaultOrganization() {
//nolint:gocritic // SCIM operations are a system user
orgSync, err := api.IDPSync.OrganizationSyncSettings(dbauthz.AsSystemRestricted(ctx), api.Database)
if err != nil {
_ = handlerutil.WriteError(rw, xerrors.Errorf("failed to get organization sync settings: %w", err))
return
}
if orgSync.AssignDefault {
//nolint:gocritic // SCIM operations are a system user
defaultOrganization, err := api.Database.GetDefaultOrganization(dbauthz.AsSystemRestricted(ctx))
if err != nil {
Expand Down
Loading
Loading