diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8c86456da1..028cd23a76 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4248,6 +4248,45 @@ const docTemplate = `{ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/settings/idpsync/organization/mapping": { "patch": { "security": [ @@ -12459,6 +12498,17 @@ const docTemplate = `{ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, "codersdk.PatchOrganizationIDPSyncMappingRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d65a421382..1a45371c38 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3744,6 +3744,39 @@ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/settings/idpsync/organization/mapping": { "patch": { "security": [ @@ -11234,6 +11267,17 @@ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, "codersdk.PatchOrganizationIDPSyncMappingRequest": { "type": "object", "properties": { diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 48127d361f..df49f496af 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -144,6 +144,25 @@ func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req Organ return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchOrganizationIDPSyncConfigRequest struct { + Field string `json:"field"` + AssignDefault bool `json:"assign_default"` +} + +func (c *Client) PatchOrganizationIDPSyncConfig(ctx context.Context, req PatchOrganizationIDPSyncConfigRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/config", 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) +} + // If the same mapping is present in both Add and Remove, Remove will take presidence. type PatchOrganizationIDPSyncMappingRequest struct { Add []IDPSyncMapping[uuid.UUID] diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 96a89c1486..8145331d87 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -2677,6 +2677,62 @@ curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update organization IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/config` + +> Body parameter + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|------------------------------------------------------------------------------------------------------------|----------|-------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncConfigRequest](schemas.md#codersdkpatchorganizationidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "organization_assign_default": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationSyncSettings](schemas.md#codersdkorganizationsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Update organization IdP Sync mapping ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8519397893..61160c03d3 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4180,6 +4180,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `quota_allowance` | integer | false | | | | `remove_users` | array of string | false | | | +## codersdk.PatchOrganizationIDPSyncConfigRequest + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------|---------|----------|--------------|-------------| +| `assign_default` | boolean | false | | | +| `field` | string | false | | | + ## codersdk.PatchOrganizationIDPSyncMappingRequest ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index d8ac046835..74971e265e 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -295,6 +295,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Route("/organization", func(r chi.Router) { r.Get("/", api.organizationIDPSyncSettings) r.Patch("/", api.patchOrganizationIDPSyncSettings) + r.Patch("/config", api.patchOrganizationIDPSyncConfig) r.Patch("/mapping", api.patchOrganizationIDPSyncMapping) }) diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index d6509bb0cd..bda63cf2a7 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -319,6 +319,75 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http }) } +// @Summary Update organization IdP Sync config +// @ID update-organization-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncConfigRequest true "New config values" +// @Router /settings/idpsync/organization/config [patch] +func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchOrganizationIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings *idpsync.OrganizationSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + aReq.Old = *existing + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, idpsync.OrganizationSyncSettings{ + Field: req.Field, + AssignDefault: req.AssignDefault, + Mapping: existing.Mapping, + }) + if err != nil { + return err + } + + settings, err = api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = *settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + AssignDefault: settings.AssignDefault, + }) +} + // @Summary Update organization IdP Sync mapping // @ID update-organization-idp-sync-mapping // @Security CoderSessionToken diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index fb9ece7e45..6c9a838953 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -20,7 +20,7 @@ import ( "github.com/coder/serpent" ) -func TestGetGroupSyncConfig(t *testing.T) { +func TestGetGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -83,7 +83,7 @@ func TestGetGroupSyncConfig(t *testing.T) { }) } -func TestPatchGroupSyncConfig(t *testing.T) { +func TestPatchGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -141,7 +141,7 @@ func TestPatchGroupSyncConfig(t *testing.T) { }) } -func TestGetRoleSyncConfig(t *testing.T) { +func TestGetRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -175,7 +175,7 @@ func TestGetRoleSyncConfig(t *testing.T) { }) } -func TestPatchRoleSyncConfig(t *testing.T) { +func TestPatchRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -323,6 +323,78 @@ func TestPatchOrganizationSyncSettings(t *testing.T) { }) } +func TestPatchOrganizationSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + mapping := map[string][]uuid.UUID{"wibble": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + _, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "wibble", + AssignDefault: true, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, true, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, false, settings.AssignDefault) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, false, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + func TestPatchOrganizationSyncMapping(t *testing.T) { t.Parallel() diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0de33cca7b..3f9cf15a3c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1465,6 +1465,12 @@ export interface PatchGroupRequest { readonly quota_allowance: number | null; } +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncConfigRequest { + readonly field: string; + readonly assign_default: boolean; +} + // From codersdk/idpsync.go export interface PatchOrganizationIDPSyncMappingRequest { readonly Add: readonly IDPSyncMapping[];