From f09d4cf4fbb0a41b9543d44024d9669d90540a7e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 29 Jan 2025 22:40:32 +0000 Subject: [PATCH 1/2] squish --- coderd/apidoc/docs.go | 76 +++++++ coderd/apidoc/swagger.json | 70 ++++++ coderd/idpsync/idpsync.go | 5 +- coderd/idpsync/organization.go | 2 +- coderd/runtimeconfig/resolver.go | 6 + coderd/telemetry/telemetry_test.go | 2 +- codersdk/idpsync.go | 29 +++ docs/reference/api/enterprise.md | 66 ++++++ docs/reference/api/schemas.md | 30 +++ enterprise/coderd/coderd.go | 5 +- enterprise/coderd/enidpsync/enidpsync.go | 2 + .../coderd/enidpsync/organizations_test.go | 2 +- enterprise/coderd/idpsync.go | 91 +++++++- enterprise/coderd/idpsync_test.go | 204 +++++++++++++++++- go.mod | 2 +- go.sum | 4 +- site/src/api/typesGenerated.ts | 12 ++ 17 files changed, 597 insertions(+), 11 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f16653c1c834b..8c86456da1619 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4248,6 +4248,45 @@ const docTemplate = `{ } } }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -12420,6 +12459,43 @@ const docTemplate = `{ } } }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7859d7ffdc5e5..d65a421382fda 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3744,6 +3744,39 @@ } } }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -11201,6 +11234,43 @@ } } }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/idpsync/idpsync.go b/coderd/idpsync/idpsync.go index e936bada73752..d51613f430e22 100644 --- a/coderd/idpsync/idpsync.go +++ b/coderd/idpsync/idpsync.go @@ -26,7 +26,7 @@ import ( type IDPSync interface { OrganizationSyncEntitled() bool OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) - UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error + UpdateOrganizationSyncSettings(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. @@ -70,6 +70,9 @@ type IDPSync interface { SyncRoles(ctx context.Context, db database.Store, user database.User, params RoleParams) error } +// AGPLIDPSync implements the IDPSync interface +var _ IDPSync = AGPLIDPSync{} + // AGPLIDPSync is the configuration for syncing user information from an external // IDP. All related code to syncing user information should be in this package. type AGPLIDPSync struct { diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 8b430fe84a3e6..6f755529cdde7 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -34,7 +34,7 @@ func (AGPLIDPSync) OrganizationSyncEnabled(_ context.Context, _ database.Store) return false } -func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { +func (s AGPLIDPSync) UpdateOrganizationSyncSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { rlv := s.Manager.Resolver(db) err := s.SyncSettings.Organization.SetRuntimeValue(ctx, rlv, &settings) if err != nil { diff --git a/coderd/runtimeconfig/resolver.go b/coderd/runtimeconfig/resolver.go index d899680f034a4..5d06a156bfb41 100644 --- a/coderd/runtimeconfig/resolver.go +++ b/coderd/runtimeconfig/resolver.go @@ -12,6 +12,9 @@ import ( "github.com/coder/coder/v2/coderd/database" ) +// NoopResolver implements the Resolver interface +var _ Resolver = &NoopResolver{} + // NoopResolver is a useful test device. type NoopResolver struct{} @@ -31,6 +34,9 @@ func (NoopResolver) DeleteRuntimeConfig(context.Context, string) error { return ErrEntryNotFound } +// StoreResolver implements the Resolver interface +var _ Resolver = &StoreResolver{} + // StoreResolver uses the database as the underlying store for runtime settings. type StoreResolver struct { db Store diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index b892e28e89d58..1ac0d4fd412e0 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -295,7 +295,7 @@ func TestTelemetry(t *testing.T) { org, err := db.GetDefaultOrganization(ctx) require.NoError(t, err) sync := idpsync.NewAGPLSync(testutil.Logger(t), runtimeconfig.NewManager(), idpsync.DeploymentSyncSettings{}) - err = sync.UpdateOrganizationSettings(ctx, db, idpsync.OrganizationSyncSettings{ + err = sync.UpdateOrganizationSyncSettings(ctx, db, idpsync.OrganizationSyncSettings{ Field: "organizations", Mapping: map[string][]uuid.UUID{ "first": {org.ID}, diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 2cc1f51ee3011..8753b7b505da0 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -12,6 +12,15 @@ import ( "golang.org/x/xerrors" ) +// constraining to `uuid.UUID | string` here would be nice but `make gen` will +// yell at you for it. +type IDPSyncMapping[ResourceIdType uuid.UUID | string] struct { + // The IdP claim the user has + Given string + // The ID of the Coder resource the user should be added to + Gets ResourceIdType +} + type GroupSyncSettings struct { // Field is the name of the claim field that specifies what groups a user // should be in. If empty, no groups will be synced. @@ -137,6 +146,26 @@ func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req Organ 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] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchOrganizationIDPSyncMapping(ctx context.Context, req PatchOrganizationIDPSyncMappingRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/mapping", 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) +} + func (c *Client) GetAvailableIDPSyncFields(ctx context.Context) ([]string, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/settings/idpsync/available-fields", nil) if err != nil { diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 6f8b061ed9025..96a89c1486d8a 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -2677,6 +2677,72 @@ 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 mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|--------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncMappingRequest](schemas.md#codersdkpatchorganizationidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### 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). + ## Get template ACLs ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index db6fc2a51f58e..85193978930f0 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4180,6 +4180,36 @@ 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.PatchOrganizationIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + ## codersdk.PatchTemplateVersionRequest ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index b32f763720b9d..d8ac0468358d3 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -295,7 +295,9 @@ 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("/mapping", api.patchOrganizationIDPSyncMapping) }) + r.Get("/available-fields", api.deploymentIDPSyncClaimFields) r.Get("/field-values", api.deploymentIDPSyncClaimFieldValues) }) @@ -307,11 +309,12 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { httpmw.ExtractOrganizationParam(api.Database), ) r.Route("/organizations/{organization}/settings", func(r chi.Router) { - r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) r.Get("/idpsync/roles", api.roleIDPSyncSettings) r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) + + r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/field-values", api.organizationIDPSyncClaimFieldValues) }) }) diff --git a/enterprise/coderd/enidpsync/enidpsync.go b/enterprise/coderd/enidpsync/enidpsync.go index c7ba8dd3ecdc6..2020a4300ebc6 100644 --- a/enterprise/coderd/enidpsync/enidpsync.go +++ b/enterprise/coderd/enidpsync/enidpsync.go @@ -7,6 +7,8 @@ import ( "github.com/coder/coder/v2/coderd/runtimeconfig" ) +var _ idpsync.IDPSync = &EnterpriseIDPSync{} + // EnterpriseIDPSync enabled syncing user information from an external IDP. // The sync is an enterprise feature, so this struct wraps the AGPL implementation // and extends it with enterprise capabilities. These capabilities can entirely diff --git a/enterprise/coderd/enidpsync/organizations_test.go b/enterprise/coderd/enidpsync/organizations_test.go index 36dbedf3a466d..391535c9478d7 100644 --- a/enterprise/coderd/enidpsync/organizations_test.go +++ b/enterprise/coderd/enidpsync/organizations_test.go @@ -300,7 +300,7 @@ func TestOrganizationSync(t *testing.T) { // Create a new sync object sync := enidpsync.NewSync(logger, runtimeconfig.NewManager(), caseData.Entitlements, caseData.Settings) if caseData.RuntimeSettings != nil { - err := sync.UpdateOrganizationSettings(ctx, rdb, *caseData.RuntimeSettings) + err := sync.UpdateOrganizationSyncSettings(ctx, rdb, *caseData.RuntimeSettings) require.NoError(t, err) } diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 192d61ea996c6..d6509bb0cda68 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -3,6 +3,7 @@ package coderd import ( "fmt" "net/http" + "slices" "github.com/google/uuid" @@ -14,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" ) @@ -292,7 +294,7 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http } aReq.Old = *existing - err = api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ Field: req.Field, // We do not check if the mappings point to actual organizations. Mapping: req.Mapping, @@ -317,6 +319,93 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http }) } +// @Summary Update organization IdP Sync mapping +// @ID update-organization-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /settings/idpsync/organization/mapping [patch] +func (api *API) patchOrganizationIDPSyncMapping(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.PatchOrganizationIDPSyncMappingRequest + 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 + + newMapping := make(map[string][]uuid.UUID) + + // Copy existing mapping + for key, ids := range existing.Mapping { + newMapping[key] = append(newMapping[key], ids...) + } + + // Add unique entries + for _, mapping := range req.Add { + if !slice.Contains(newMapping[mapping.Given], mapping.Gets) { + newMapping[mapping.Given] = append(newMapping[mapping.Given], mapping.Gets) + } + } + + // Remove entries + for _, mapping := range req.Remove { + newMapping[mapping.Given] = slices.DeleteFunc(newMapping[mapping.Given], func(u uuid.UUID) bool { + return u == mapping.Gets + }) + } + + settings = idpsync.OrganizationSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + AssignDefault: existing.AssignDefault, + } + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) + 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 Get the available organization idp sync claim fields // @ID get-the-available-organization-idp-sync-claim-fields // @Security CoderSessionToken diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index 41a8db2dd0792..fb9ece7e45285 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -82,7 +83,7 @@ func TestGetGroupSyncConfig(t *testing.T) { }) } -func TestPostGroupSyncConfig(t *testing.T) { +func TestPatchGroupSyncConfig(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -174,7 +175,7 @@ func TestGetRoleSyncConfig(t *testing.T) { }) } -func TestPostRoleSyncConfig(t *testing.T) { +func TestPatchRoleSyncConfig(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -231,3 +232,202 @@ func TestPostRoleSyncConfig(t *testing.T) { require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) } + +func TestGetOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + expected := map[string][]uuid.UUID{"foo": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + Mapping: expected, + }) + + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + + settings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + }) +} + +func TestPatchOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + }) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", fetchedSettings.Field) + }) + + 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.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{ + Field: "august", + }) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + + _, err = member.RoleIDPSyncSettings(ctx, user.OrganizationID.String()) + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchOrganizationSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + // These IDs are easier to visually diff if the test fails than truly random + // ones. + orgs := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + uuid.MustParse("03000000-92f6-4bfd-bba6-0f54667b131c"), + uuid.MustParse("04000000-b9d0-46fe-910f-6e2ea0c62caa"), + uuid.MustParse("05000000-67c0-4c19-a52d-0dc3f65abee0"), + uuid.MustParse("06000000-a8a8-4a2c-bdd0-b59aa6882b55"), + uuid.MustParse("07000000-5390-4cc7-a9c8-e4330a683ae7"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wibble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + {Given: "wobble", Gets: orgs[3]}, + {Given: "wooble", Gets: orgs[0]}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[3]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0], orgs[1]}, + "wobble": {orgs[0], orgs[1], orgs[2]}, + "wooble": {orgs[0]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err = owner.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[2]}, + {Given: "wobble", Gets: orgs[3]}, + {Given: "wooble", Gets: orgs[0]}, + }, + // Remove takes priority over Add, so `f` should not actually be added. + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected = map[string][]uuid.UUID{ + "wibble": {orgs[1], orgs[2]}, + "wobble": {orgs[0], orgs[2], orgs[3]}, + "wooble": {orgs[0]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, 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.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} diff --git a/go.mod b/go.mod index 89c0caa82af62..0b01b91f131cf 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/chromedp/chromedp v0.11.0 github.com/cli/safeexec v1.0.1 github.com/coder/flog v1.1.0 - github.com/coder/guts v1.0.0 + github.com/coder/guts v1.0.1 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 diff --git a/go.sum b/go.sum index f8cd711ba9d4f..603336bd01281 100644 --- a/go.sum +++ b/go.sum @@ -226,8 +226,8 @@ github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVp github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs24WOxc3PBvygSNTQurm0PYPujJjLLOzs0= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc= -github.com/coder/guts v1.0.0 h1:Ba6TBOeED+96Dv8IdISjbGhCzHKicqSc4SEYVV+4zeE= -github.com/coder/guts v1.0.0/go.mod h1:SfmxjDaSfPjzKJ9mGU4sA/1OHU+u66uRfhFF+y4BARQ= +github.com/coder/guts v1.0.1 h1:tU9pW+1jftCSX1eBxnNHiouQBSBJIej3I+kqfjIyeJU= +github.com/coder/guts v1.0.1/go.mod h1:z8LHbF6vwDOXQOReDvay7Rpwp/jHwCZiZwjd6wfLcJg= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggXhnTnP05FCYiAFeQpoN+gNR5I= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d5093587ad527..0de33cca7b66e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1055,6 +1055,12 @@ export interface HealthcheckReport { readonly coder_version: string; } +// From codersdk/idpsync.go +export interface IDPSyncMapping { + readonly Given: string; + readonly Gets: ResourceIdType; +} + // From codersdk/insights.go export type InsightsReportInterval = "day" | "week"; @@ -1459,6 +1465,12 @@ export interface PatchGroupRequest { readonly quota_allowance: number | null; } +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: string; From 27495b096cbec2861018cbf862abe38b17f63968 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 30 Jan 2025 17:32:11 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codersdk/idpsync.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 8753b7b505da0..48127d361f7a8 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -12,8 +12,6 @@ import ( "golang.org/x/xerrors" ) -// constraining to `uuid.UUID | string` here would be nice but `make gen` will -// yell at you for it. type IDPSyncMapping[ResourceIdType uuid.UUID | string] struct { // The IdP claim the user has Given string pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy