From ce21b2030af73cb9b8da758415149d1b54d63a84 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 17 Sep 2024 10:38:42 -0500 Subject: [PATCH] feat: implement patch and get api methods for role sync (#14692) * feat: implement patch and get api methods for role sync --- coderd/apidoc/docs.go | 167 +++++++++++++++++++++++------- coderd/apidoc/swagger.json | 159 +++++++++++++++++++++------- coderd/idpsync/idpsync.go | 3 +- coderd/idpsync/role.go | 39 ++++--- codersdk/idpsync.go | 37 +++++++ docs/reference/api/enterprise.md | 90 +++++++++++++++- docs/reference/api/schemas.md | 80 ++++++++------ enterprise/coderd/coderd.go | 9 ++ enterprise/coderd/idpsync.go | 71 ++++++++++++- enterprise/coderd/idpsync_test.go | 119 +++++++++++++++++++++ site/src/api/typesGenerated.ts | 6 ++ 11 files changed, 648 insertions(+), 132 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 12a7321189..996f3b2f2b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3155,7 +3155,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/idpsync.GroupSyncSettings" + "$ref": "#/definitions/codersdk.GroupSyncSettings" } } } @@ -3188,7 +3188,75 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/idpsync.GroupSyncSettings" + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Get role IdP Sync settings by organization", + "operationId": "get-role-idp-sync-settings-by-organization", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync settings by organization", + "operationId": "update-role-idp-sync-settings-by-organization", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" } } } @@ -10523,6 +10591,44 @@ const docTemplate = `{ "GroupSourceOIDC" ] }, + "codersdk.GroupSyncSettings": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "description": "AutoCreateMissing controls whether groups returned by the OIDC provider\nare automatically created in Coder if they are missing.", + "type": "boolean" + }, + "field": { + "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", + "type": "string" + }, + "legacy_group_name_mapping": { + "description": "LegacyNameMapping is deprecated. It remaps an IDP group name to\na Coder group name. Since configuration is now done at runtime,\ngroup IDs are used to account for group renames.\nFor legacy configurations, this config option has to remain.\nDeprecated: Use Mapping instead.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mapping": { + "description": "Mapping maps from an OIDC group --\u003e Coder group ID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "regex_filter": { + "description": "RegexFilter is a regular expression that filters the groups returned by\nthe OIDC provider. Any group not matched by this regex will be ignored.\nIf the group filter is nil, then no group filtering will occur.", + "allOf": [ + { + "$ref": "#/definitions/regexp.Regexp" + } + ] + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -12238,6 +12344,25 @@ const docTemplate = `{ } } }, + "codersdk.RoleSyncSettings": { + "type": "object", + "properties": { + "field": { + "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", + "type": "string" + }, + "mapping": { + "description": "Mapping maps from an OIDC group --\u003e Coder organization role", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "codersdk.SSHConfig": { "type": "object", "properties": { @@ -15253,44 +15378,6 @@ const docTemplate = `{ } } }, - "idpsync.GroupSyncSettings": { - "type": "object", - "properties": { - "auto_create_missing_groups": { - "description": "AutoCreateMissing controls whether groups returned by the OIDC provider\nare automatically created in Coder if they are missing.", - "type": "boolean" - }, - "field": { - "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", - "type": "string" - }, - "legacy_group_name_mapping": { - "description": "LegacyNameMapping is deprecated. It remaps an IDP group name to\na Coder group name. Since configuration is now done at runtime,\ngroup IDs are used to account for group renames.\nFor legacy configurations, this config option has to remain.\nDeprecated: Use Mapping instead.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "mapping": { - "description": "Mapping maps from an OIDC group --\u003e Coder group ID", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "regex_filter": { - "description": "RegexFilter is a regular expression that filters the groups returned by\nthe OIDC provider. Any group not matched by this regex will be ignored.\nIf the group filter is nil, then no group filtering will occur.", - "allOf": [ - { - "$ref": "#/definitions/regexp.Regexp" - } - ] - } - } - }, "key.NodePublic": { "type": "object" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 23a1f369c5..fa9288714f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2773,7 +2773,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/idpsync.GroupSyncSettings" + "$ref": "#/definitions/codersdk.GroupSyncSettings" } } } @@ -2802,7 +2802,67 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/idpsync.GroupSyncSettings" + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Get role IdP Sync settings by organization", + "operationId": "get-role-idp-sync-settings-by-organization", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync settings by organization", + "operationId": "update-role-idp-sync-settings-by-organization", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" } } } @@ -9445,6 +9505,44 @@ "enum": ["user", "oidc"], "x-enum-varnames": ["GroupSourceUser", "GroupSourceOIDC"] }, + "codersdk.GroupSyncSettings": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "description": "AutoCreateMissing controls whether groups returned by the OIDC provider\nare automatically created in Coder if they are missing.", + "type": "boolean" + }, + "field": { + "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", + "type": "string" + }, + "legacy_group_name_mapping": { + "description": "LegacyNameMapping is deprecated. It remaps an IDP group name to\na Coder group name. Since configuration is now done at runtime,\ngroup IDs are used to account for group renames.\nFor legacy configurations, this config option has to remain.\nDeprecated: Use Mapping instead.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mapping": { + "description": "Mapping maps from an OIDC group --\u003e Coder group ID", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "regex_filter": { + "description": "RegexFilter is a regular expression that filters the groups returned by\nthe OIDC provider. Any group not matched by this regex will be ignored.\nIf the group filter is nil, then no group filtering will occur.", + "allOf": [ + { + "$ref": "#/definitions/regexp.Regexp" + } + ] + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -11070,6 +11168,25 @@ } } }, + "codersdk.RoleSyncSettings": { + "type": "object", + "properties": { + "field": { + "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", + "type": "string" + }, + "mapping": { + "description": "Mapping maps from an OIDC group --\u003e Coder organization role", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "codersdk.SSHConfig": { "type": "object", "properties": { @@ -13906,44 +14023,6 @@ } } }, - "idpsync.GroupSyncSettings": { - "type": "object", - "properties": { - "auto_create_missing_groups": { - "description": "AutoCreateMissing controls whether groups returned by the OIDC provider\nare automatically created in Coder if they are missing.", - "type": "boolean" - }, - "field": { - "description": "Field selects the claim field to be used as the created user's\ngroups. If the group field is the empty string, then no group updates\nwill ever come from the OIDC provider.", - "type": "string" - }, - "legacy_group_name_mapping": { - "description": "LegacyNameMapping is deprecated. It remaps an IDP group name to\na Coder group name. Since configuration is now done at runtime,\ngroup IDs are used to account for group renames.\nFor legacy configurations, this config option has to remain.\nDeprecated: Use Mapping instead.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "mapping": { - "description": "Mapping maps from an OIDC group --\u003e Coder group ID", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "regex_filter": { - "description": "RegexFilter is a regular expression that filters the groups returned by\nthe OIDC provider. Any group not matched by this regex will be ignored.\nIf the group filter is nil, then no group filtering will occur.", - "allOf": [ - { - "$ref": "#/definitions/regexp.Regexp" - } - ] - } - } - }, "key.NodePublic": { "type": "object" }, diff --git a/coderd/idpsync/idpsync.go b/coderd/idpsync/idpsync.go index d86f8295b7..f2c9e49ecc 100644 --- a/coderd/idpsync/idpsync.go +++ b/coderd/idpsync/idpsync.go @@ -55,7 +55,8 @@ type IDPSync interface { SiteRoleSyncEnabled() bool // RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for // rational. - RoleSyncSettings() runtimeconfig.RuntimeEntry[*RoleSyncSettings] + 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 // 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 9c63df157a..cf768ee0eb 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac/rolestore" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/codersdk" ) type RoleParams struct { @@ -41,8 +42,26 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool { return false } -func (s AGPLIDPSync) RoleSyncSettings() runtimeconfig.RuntimeEntry[*RoleSyncSettings] { - return s.Role +func (s AGPLIDPSync) UpdateRoleSettings(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 { + return xerrors.Errorf("update role sync settings: %w", err) + } + + return nil +} + +func (s AGPLIDPSync) RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error) { + rlv := s.Manager.OrganizationResolver(db, orgID) + settings, err := s.Role.Resolve(ctx, rlv) + if err != nil { + if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) { + return nil, xerrors.Errorf("resolve role sync settings: %w", err) + } + return &RoleSyncSettings{}, nil + } + return settings, nil } func (s AGPLIDPSync) ParseRoleClaims(_ context.Context, _ jwt.MapClaims) (RoleParams, *HTTPError) { @@ -85,15 +104,12 @@ func (s AGPLIDPSync) SyncRoles(ctx context.Context, db database.Store, user data allExpected := make([]rbac.RoleIdentifier, 0) for _, member := range orgMemberships { orgID := member.OrganizationMember.OrganizationID - orgResolver := s.Manager.OrganizationResolver(tx, orgID) - settings, err := s.RoleSyncSettings().Resolve(ctx, orgResolver) + settings, err := s.RoleSyncSettings(ctx, orgID, tx) if err != nil { - if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) { - return xerrors.Errorf("resolve group sync settings: %w", err) - } // No entry means no role syncing for this organization continue } + if settings.Field == "" { // Explicitly disabled role sync for this organization continue @@ -261,14 +277,7 @@ func (AGPLIDPSync) RolesFromClaim(field string, claims jwt.MapClaims) ([]string, return parsedRoles, nil } -type RoleSyncSettings struct { - // Field selects the claim field to be used as the created user's - // groups. If the group field is the empty string, then no group updates - // will ever come from the OIDC provider. - Field string `json:"field"` - // Mapping maps from an OIDC group --> Coder organization role - Mapping map[string][]string `json:"mapping"` -} +type RoleSyncSettings codersdk.RoleSyncSettings func (s *RoleSyncSettings) Set(v string) error { return json.Unmarshal([]byte(v), s) diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 105efe57c5..380b26336a 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -60,3 +60,40 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re var resp GroupSyncSettings return resp, json.NewDecoder(res.Body).Decode(&resp) } + +type RoleSyncSettings struct { + // Field selects the claim field to be used as the created user's + // groups. If the group field is the empty string, then no group updates + // will ever come from the OIDC provider. + Field string `json:"field"` + // Mapping maps from an OIDC group --> Coder organization role + Mapping map[string][]string `json:"mapping"` +} + +func (c *Client) RoleIDPSyncSettings(ctx context.Context, orgID string) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles", orgID), nil) + 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) +} + +func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req RoleSyncSettings) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles", 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) +} diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 555acc4af8..d7f9a28803 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1817,9 +1817,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/setting ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [idpsync.GroupSyncSettings](schemas.md#idpsyncgroupsyncsettings) | +| 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). @@ -1864,9 +1864,91 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti ### 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 + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/settings/idpsync/roles` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------------ | -------- | --------------- | +| `organization` | path | string(uuid) | true | Organization ID | + +### 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 | [idpsync.GroupSyncSettings](schemas.md#idpsyncgroupsyncsettings) | +| 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 settings by organization + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------------ | -------- | --------------- | +| `organization` | path | string(uuid) | true | Organization ID | + +### 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). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 979779e70a..6377707bdf 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2895,6 +2895,36 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `user` | | `oidc` | +## codersdk.GroupSyncSettings + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": ["string"], + "property2": ["string"] + }, + "regex_filter": {} +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------------- | ------------------------------ | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `auto_create_missing_groups` | boolean | false | | Auto create missing groups controls whether groups returned by the OIDC provider are automatically created in Coder if they are missing. | +| `field` | string | false | | Field selects the claim field to be used as the created user's groups. If the group field is the empty string, then no group updates will ever come from the OIDC provider. | +| `legacy_group_name_mapping` | object | false | | Legacy group name mapping is deprecated. It remaps an IDP group name to a Coder group name. Since configuration is now done at runtime, group IDs are used to account for group renames. For legacy configurations, this config option has to remain. Deprecated: Use Mapping instead. | +| » `[any property]` | string | false | | | +| `mapping` | object | false | | Mapping maps from an OIDC group --> Coder group ID | +| » `[any property]` | array of string | false | | | +| `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | Regex filter is a regular expression that filters the groups returned by the OIDC provider. Any group not matched by this regex will be ignored. If the group filter is nil, then no group filtering will occur. | + ## codersdk.Healthcheck ```json @@ -4660,6 +4690,26 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | | `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | +## codersdk.RoleSyncSettings + +```json +{ + "field": "string", + "mapping": { + "property1": ["string"], + "property2": ["string"] + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | --------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `field` | string | false | | Field selects the claim field to be used as the created user's groups. If the group field is the empty string, then no group updates will ever come from the OIDC provider. | +| `mapping` | object | false | | Mapping maps from an OIDC group --> Coder organization role | +| » `[any property]` | array of string | false | | | + ## codersdk.SSHConfig ```json @@ -8964,36 +9014,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `severity` | `warning` | | `severity` | `error` | -## idpsync.GroupSyncSettings - -```json -{ - "auto_create_missing_groups": true, - "field": "string", - "legacy_group_name_mapping": { - "property1": "string", - "property2": "string" - }, - "mapping": { - "property1": ["string"], - "property2": ["string"] - }, - "regex_filter": {} -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------------------------- | ------------------------------ | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `auto_create_missing_groups` | boolean | false | | Auto create missing groups controls whether groups returned by the OIDC provider are automatically created in Coder if they are missing. | -| `field` | string | false | | Field selects the claim field to be used as the created user's groups. If the group field is the empty string, then no group updates will ever come from the OIDC provider. | -| `legacy_group_name_mapping` | object | false | | Legacy group name mapping is deprecated. It remaps an IDP group name to a Coder group name. Since configuration is now done at runtime, group IDs are used to account for group renames. For legacy configurations, this config option has to remain. Deprecated: Use Mapping instead. | -| » `[any property]` | string | false | | | -| `mapping` | object | false | | Mapping maps from an OIDC group --> Coder group ID | -| » `[any property]` | array of string | false | | | -| `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | Regex filter is a regular expression that filters the groups returned by the OIDC provider. Any group not matched by this regex will be ignored. If the group filter is nil, then no group filtering will occur. | - ## key.NodePublic ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 704964e5fc..c030441253 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -286,9 +286,18 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Post("/organizations/{organization}/members/roles", api.postOrgRoles) r.Put("/organizations/{organization}/members/roles", api.putOrgRoles) r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole) + }) + + r.Group(func(r chi.Router) { + r.Use( + apiKeyMiddleware, + httpmw.ExtractOrganizationParam(api.Database), + ) r.Route("/organizations/{organization}/settings", func(r chi.Router) { r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) + r.Get("/idpsync/roles", api.roleIDPSyncSettings) + r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) }) }) diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 22781856ce..f5b3b6f013 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -17,7 +17,7 @@ import ( // @Produce json // @Tags Enterprise // @Param organization path string true "Organization ID" format(uuid) -// @Success 200 {object} idpsync.GroupSyncSettings +// @Success 200 {object} codersdk.GroupSyncSettings // @Router /organizations/{organization}/settings/idpsync/groups [get] func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -45,7 +45,7 @@ func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Enterprise // @Param organization path string true "Organization ID" format(uuid) -// @Success 200 {object} idpsync.GroupSyncSettings +// @Success 200 {object} codersdk.GroupSyncSettings // @Router /organizations/{organization}/settings/idpsync/groups [patch] func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -77,3 +77,70 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques httpapi.Write(ctx, rw, http.StatusOK, settings) } + +// @Summary Get role IdP Sync settings by organization +// @ID get-role-idp-sync-settings-by-organization +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Success 200 {object} codersdk.RoleSyncSettings +// @Router /organizations/{organization}/settings/idpsync/roles [get] +func (api *API) roleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + + if !api.Authorize(r, policy.ActionRead, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + //nolint:gocritic // Requires system context to read runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + settings, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, api.Database) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, settings) +} + +// @Summary Update role IdP Sync settings by organization +// @ID update-role-idp-sync-settings-by-organization +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Success 200 {object} codersdk.RoleSyncSettings +// @Router /organizations/{organization}/settings/idpsync/roles [patch] +func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req idpsync.RoleSyncSettings + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, req) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + settings, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, api.Database) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, settings) +} diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index 2dc236516d..374e318d23 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -170,3 +170,122 @@ func TestPostGroupSyncConfig(t *testing.T) { require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) } + +func TestGetRoleSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{ + string(codersdk.ExperimentCustomRoles), + string(codersdk.ExperimentMultiOrganization), + } + + owner, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + ctx := testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{ + Field: "august", + Mapping: map[string][]string{ + "foo": {"bar"}, + }, + }) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, map[string][]string{"foo": {"bar"}}, settings.Mapping) + + settings, err = orgAdmin.RoleIDPSyncSettings(ctx, user.OrganizationID.String()) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, map[string][]string{"foo": {"bar"}}, settings.Mapping) + }) +} + +func TestPostRoleSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{ + string(codersdk.ExperimentCustomRoles), + string(codersdk.ExperimentMultiOrganization), + } + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + // Test as org admin + ctx := testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{ + Field: "august", + }) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, user.OrganizationID.String()) + require.NoError(t, err) + require.Equal(t, "august", fetchedSettings.Field) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{ + string(codersdk.ExperimentCustomRoles), + string(codersdk.ExperimentMultiOrganization), + } + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + 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()) + }) +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7c5785cbc5..8e94471b9c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1164,6 +1164,12 @@ export interface Role { readonly user_permissions: Readonly>; } +// From codersdk/idpsync.go +export interface RoleSyncSettings { + readonly field: string; + readonly mapping: Record>>; +} + // From codersdk/deployment.go export interface SSHConfig { readonly DeploymentName: string;