diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 028cd23a76557..329951003007b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3438,6 +3438,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3518,6 +3612,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -12469,6 +12657,57 @@ const docTemplate = `{ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "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.PatchGroupRequest": { "type": "object", "properties": { @@ -12546,6 +12785,51 @@ const docTemplate = `{ } } }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "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 1a45371c380d6..63b7146365d9f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3030,6 +3030,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3100,6 +3182,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -11238,6 +11402,57 @@ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "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.PatchGroupRequest": { "type": "object", "properties": { @@ -11315,6 +11530,51 @@ } } }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "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/group.go b/coderd/idpsync/group.go index c14b7655e7e20..4524284260359 100644 --- a/coderd/idpsync/group.go +++ b/coderd/idpsync/group.go @@ -30,7 +30,7 @@ func (AGPLIDPSync) GroupSyncEntitled() bool { return false } -func (s AGPLIDPSync) UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error { +func (s AGPLIDPSync) UpdateGroupSyncSettings(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 { diff --git a/coderd/idpsync/idpsync.go b/coderd/idpsync/idpsync.go index d51613f430e22..4da101635bd23 100644 --- a/coderd/idpsync/idpsync.go +++ b/coderd/idpsync/idpsync.go @@ -48,7 +48,7 @@ type IDPSync interface { // on the settings used by IDPSync. This entry is thread safe and can be // accessed concurrently. The settings are stored in the database. 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 + UpdateGroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error // RoleSyncEntitled returns true if the deployment is entitled to role syncing. RoleSyncEntitled() bool @@ -61,7 +61,7 @@ type IDPSync interface { // RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for // rational. RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error) - UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error + UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error // ParseRoleClaims takes claims from an OIDC provider, and returns the params // for role syncing. Most of the logic happens in SyncRoles. ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError) diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index cf768ee0eb05d..5cb0ac172581c 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -42,7 +42,7 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool { return false } -func (s AGPLIDPSync) UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { +func (s AGPLIDPSync) UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { orgResolver := s.Manager.OrganizationResolver(db, orgID) err := s.SyncSettings.Role.SetRuntimeValue(ctx, orgResolver, &settings) if err != nil { diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index df49f496af4e1..8f92cea680e25 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -68,6 +68,46 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchGroupIDPSyncConfigRequest struct { + Field string `json:"field"` + RegexFilter *regexp.Regexp `json:"regex_filter"` + AutoCreateMissing bool `json:"auto_create_missing_groups"` +} + +func (c *Client) PatchGroupIDPSyncConfig(ctx context.Context, orgID string, req PatchGroupIDPSyncConfigRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/config", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchGroupIDPSyncMappingRequest struct { + Add []IDPSyncMapping[uuid.UUID] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchGroupIDPSyncMapping(ctx context.Context, orgID string, req PatchGroupIDPSyncMappingRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/mapping", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + type RoleSyncSettings struct { // Field is the name of the claim field that specifies what organization roles // a user should be given. If empty, no roles will be synced. @@ -104,6 +144,44 @@ func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchRoleIDPSyncConfigRequest struct { + Field string `json:"field"` +} + +func (c *Client) PatchRoleIDPSyncConfig(ctx context.Context, orgID string, req PatchRoleIDPSyncConfigRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/config", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + var resp RoleSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchRoleIDPSyncMappingRequest struct { + Add []IDPSyncMapping[string] + Remove []IDPSyncMapping[string] +} + +func (c *Client) PatchRoleIDPSyncMapping(ctx context.Context, orgID string, req PatchRoleIDPSyncMappingRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/mapping", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + 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 diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 8145331d878d3..a1a61f4a5b54a 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1953,6 +1953,141 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update group IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/config` + +> Body parameter + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncConfigRequest](schemas.md#codersdkpatchgroupidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update group IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncMappingRequest](schemas.md#codersdkpatchgroupidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get role IdP Sync settings by organization ### Code samples @@ -2061,6 +2196,127 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update role IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/config` + +> Body parameter + +```json +{ + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|--------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncConfigRequest](schemas.md#codersdkpatchroleidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update role IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncMappingRequest](schemas.md#codersdkpatchroleidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Fetch provisioner key details ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 61160c03d3cd3..20ed37f81f7f7 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4152,6 +4152,54 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | array of string | false | | | | `organization_assign_default` | boolean | false | | Organization assign default will ensure the default org is always included for every user, regardless of their claims. This preserves legacy behavior. | +## codersdk.PatchGroupIDPSyncConfigRequest + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------------|--------------------------------|----------|--------------|-------------| +| `auto_create_missing_groups` | boolean | false | | | +| `field` | string | false | | | +| `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | | + +## codersdk.PatchGroupIDPSyncMappingRequest + +```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.PatchGroupRequest ```json @@ -4226,6 +4274,50 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `» 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.PatchRoleIDPSyncConfigRequest + +```json +{ + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|--------|----------|--------------|-------------| +| `field` | string | false | | | + +## codersdk.PatchRoleIDPSyncMappingRequest + +```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 74971e265e0e0..2a91fbbfd6f93 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -312,8 +312,13 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Route("/organizations/{organization}/settings", func(r chi.Router) { r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) + r.Patch("/idpsync/groups/config", api.patchGroupIDPSyncConfig) + r.Patch("/idpsync/groups/mapping", api.patchGroupIDPSyncMapping) + r.Get("/idpsync/roles", api.roleIDPSyncSettings) r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) + r.Patch("/idpsync/roles/config", api.patchRoleIDPSyncConfig) + r.Patch("/idpsync/roles/mapping", api.patchRoleIDPSyncMapping) r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/field-values", api.organizationIDPSyncClaimFieldValues) diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index bda63cf2a7976..2dcee572eb692 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -61,7 +61,6 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques ctx := r.Context() org := httpmw.OrganizationParam(r) auditor := *api.AGPL.Auditor.Load() - aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ Audit: auditor, Log: api.Logger, @@ -104,7 +103,7 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques } aReq.Old = *existing - err = api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ Field: req.Field, Mapping: req.Mapping, RegexFilter: req.RegexFilter, @@ -132,6 +131,153 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Update group IdP Sync config +// @ID update-group-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/groups/config [patch] +func (api *API) patchGroupIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //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.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.GroupSyncSettings{ + Field: req.Field, + RegexFilter: req.RegexFilter, + AutoCreateMissing: req.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, 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.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + +// @Summary Update group IdP Sync mapping +// @ID update-group-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/groups/mapping [patch] +func (api *API) patchGroupIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //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.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.GroupSyncSettings{ + Field: existing.Field, + RegexFilter: existing.RegexFilter, + AutoCreateMissing: existing.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, 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.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + // @Summary Get role IdP Sync settings by organization // @ID get-role-idp-sync-settings-by-organization // @Security CoderSessionToken @@ -203,7 +349,7 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request } aReq.Old = *existing - err = api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ Field: req.Field, Mapping: req.Mapping, }) @@ -225,6 +371,141 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request }) } +// @Summary Update role IdP Sync config +// @ID update-role-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/roles/config [patch] +func (api *API) patchRoleIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //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.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.RoleSyncSettings{ + Field: req.Field, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, 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.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + +// @Summary Update role IdP Sync mapping +// @ID update-role-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/roles/mapping [patch] +func (api *API) patchRoleIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //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.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.RoleSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, 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.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + // @Summary Get organization IdP Sync settings // @ID get-organization-idp-sync-settings // @Security CoderSessionToken @@ -349,7 +630,7 @@ func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.R return } - var settings *idpsync.OrganizationSyncSettings + 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 { @@ -359,16 +640,13 @@ func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.R } aReq.Old = *existing - err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, idpsync.OrganizationSyncSettings{ + settings = idpsync.OrganizationSyncSettings{ Field: req.Field, AssignDefault: req.AssignDefault, Mapping: existing.Mapping, - }) - if err != nil { - return err } - settings, err = api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) if err != nil { return err } @@ -380,7 +658,7 @@ func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.R return } - aReq.New = *settings + aReq.New = settings httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ Field: settings.Field, Mapping: settings.Mapping, @@ -428,27 +706,7 @@ func (api *API) patchOrganizationIDPSyncMapping(rw http.ResponseWriter, r *http. } 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 - }) - } - + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) settings = idpsync.OrganizationSyncSettings{ Field: existing.Field, Mapping: newMapping, @@ -581,3 +839,31 @@ func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter, httpapi.Write(ctx, rw, http.StatusOK, fieldValues) } + +func applyIDPSyncMappingDiff[IDType uuid.UUID | string]( + previous map[string][]IDType, + add, remove []codersdk.IDPSyncMapping[IDType], +) map[string][]IDType { + next := make(map[string][]IDType) + + // Copy existing mapping + for key, ids := range previous { + next[key] = append(next[key], ids...) + } + + // Add unique entries + for _, mapping := range add { + if !slice.Contains(next[mapping.Given], mapping.Gets) { + next[mapping.Given] = append(next[mapping.Given], mapping.Gets) + } + } + + // Remove entries + for _, mapping := range remove { + next[mapping.Given] = slices.DeleteFunc(next[mapping.Given], func(u IDType) bool { + return u == mapping.Gets + }) + } + + return next +} diff --git a/enterprise/coderd/idpsync_internal_test.go b/enterprise/coderd/idpsync_internal_test.go new file mode 100644 index 0000000000000..51db04e74b913 --- /dev/null +++ b/enterprise/coderd/idpsync_internal_test.go @@ -0,0 +1,117 @@ +package coderd + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func TestApplyIDPSyncMappingDiff(t *testing.T) { + t.Parallel() + + t.Run("with UUIDs", func(t *testing.T) { + t.Parallel() + + id := []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"), + } + + mapping := applyIDPSyncMappingDiff(map[string][]uuid.UUID{}, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wibble", Gets: id[1]}, + {Given: "wobble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + {Given: "wobble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: id[3]}, + }, + ) + + expected := map[string][]uuid.UUID{ + "wibble": {id[0], id[1]}, + "wobble": {id[0], id[1], id[2]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + }, + ) + + expected = map[string][]uuid.UUID{ + "wibble": {id[1], id[2]}, + "wobble": {id[0], id[2], id[3]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + }) + + t.Run("with strings", func(t *testing.T) { + t.Parallel() + + mapping := applyIDPSyncMappingDiff(map[string][]string{}, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wibble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-03"}, + }, + ) + + expected := map[string][]string{ + "wibble": {"group-00", "group-01"}, + "wobble": {"group-00", "group-01", "group-02"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + }, + ) + + expected = map[string][]string{ + "wibble": {"group-01", "group-02"}, + "wobble": {"group-00", "group-02", "group-03"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + }) +} diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index 6c9a83895322c..d34701c3f6936 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -141,6 +141,171 @@ func TestPatchGroupSyncSettings(t *testing.T) { }) } +func TestPatchGroupSyncConfig(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, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]uuid.UUID{"wibble": {uuid.New()}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: false, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, false, fetchedSettings.AutoCreateMissing) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncConfig(ctx, orgID.String(), codersdk.PatchGroupIDPSyncConfigRequest{ + Field: "wobble", + RegexFilter: regexp.MustCompile("wob{2,}le"), + AutoCreateMissing: true, + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, "wob{2,}le", settings.RegexFilter.String()) + require.Equal(t, true, settings.AutoCreateMissing) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, "wob{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + 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.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchGroupSyncMapping(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, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + // 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"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: true, + Mapping: map[string][]uuid.UUID{"wobble": {orgs[0]}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncMapping(ctx, orgID.String(), codersdk.PatchGroupIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + 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.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + func TestGetRoleSyncSettings(t *testing.T) { t.Parallel() @@ -233,6 +398,150 @@ func TestPatchRoleSyncSettings(t *testing.T) { }) } +func TestPatchRoleSyncConfig(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, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]string{"wibble": {"group-01"}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncConfig(ctx, orgID.String(), codersdk.PatchRoleIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + 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.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchRoleSyncMapping(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, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: map[string][]string{"wobble": {"group-00"}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncMapping(ctx, orgID.String(), codersdk.PatchRoleIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-01"}, + }, + }) + + expected := map[string][]string{ + "wibble": {"group-00"}, + "wobble": {"group-00", "group-02"}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + 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.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + func TestGetOrganizationSyncSettings(t *testing.T) { t.Parallel() @@ -416,11 +725,6 @@ func TestPatchOrganizationSyncMapping(t *testing.T) { 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) @@ -428,23 +732,18 @@ func TestPatchOrganizationSyncMapping(t *testing.T) { 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]}, + {Given: "wobble", Gets: orgs[1]}, }, }) expected := map[string][]uuid.UUID{ - "wibble": {orgs[0], orgs[1]}, - "wobble": {orgs[0], orgs[1], orgs[2]}, - "wooble": {orgs[0]}, + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, } require.NoError(t, err) @@ -453,33 +752,6 @@ func TestPatchOrganizationSyncMapping(t *testing.T) { 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) { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3f9cf15a3cd1d..de879ee23daa5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1455,6 +1455,19 @@ export interface Pagination { readonly offset?: number; } +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncConfigRequest { + readonly field: string; + readonly regex_filter: string | null; + readonly auto_create_missing_groups: boolean; +} + +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/groups.go export interface PatchGroupRequest { readonly add_users: readonly string[]; @@ -1477,6 +1490,17 @@ export interface PatchOrganizationIDPSyncMappingRequest { readonly Remove: readonly IDPSyncMapping[]; } +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncConfigRequest { + readonly field: string; +} + +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: 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