feat: implement patch and get api methods for role sync (#14692)

* feat: implement patch and get api methods for role sync
This commit is contained in:
Steven Masley
2024-09-17 10:38:42 -05:00
committed by GitHub
parent be516f9686
commit ce21b2030a
11 changed files with 648 additions and 132 deletions

167
coderd/apidoc/docs.go generated
View File

@ -3155,7 +3155,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/idpsync.GroupSyncSettings" "$ref": "#/definitions/codersdk.GroupSyncSettings"
} }
} }
} }
@ -3188,7 +3188,75 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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" "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": { "codersdk.Healthcheck": {
"type": "object", "type": "object",
"properties": { "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": { "codersdk.SSHConfig": {
"type": "object", "type": "object",
"properties": { "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": { "key.NodePublic": {
"type": "object" "type": "object"
}, },

View File

@ -2773,7 +2773,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/idpsync.GroupSyncSettings" "$ref": "#/definitions/codersdk.GroupSyncSettings"
} }
} }
} }
@ -2802,7 +2802,67 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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"], "enum": ["user", "oidc"],
"x-enum-varnames": ["GroupSourceUser", "GroupSourceOIDC"] "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": { "codersdk.Healthcheck": {
"type": "object", "type": "object",
"properties": { "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": { "codersdk.SSHConfig": {
"type": "object", "type": "object",
"properties": { "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": { "key.NodePublic": {
"type": "object" "type": "object"
}, },

View File

@ -55,7 +55,8 @@ type IDPSync interface {
SiteRoleSyncEnabled() bool SiteRoleSyncEnabled() bool
// RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for // RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for
// rational. // 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 // ParseRoleClaims takes claims from an OIDC provider, and returns the params
// for role syncing. Most of the logic happens in SyncRoles. // for role syncing. Most of the logic happens in SyncRoles.
ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError) ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError)

View File

@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac/rolestore" "github.com/coder/coder/v2/coderd/rbac/rolestore"
"github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
) )
type RoleParams struct { type RoleParams struct {
@ -41,8 +42,26 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool {
return false return false
} }
func (s AGPLIDPSync) RoleSyncSettings() runtimeconfig.RuntimeEntry[*RoleSyncSettings] { func (s AGPLIDPSync) UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error {
return s.Role 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) { 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) allExpected := make([]rbac.RoleIdentifier, 0)
for _, member := range orgMemberships { for _, member := range orgMemberships {
orgID := member.OrganizationMember.OrganizationID orgID := member.OrganizationMember.OrganizationID
orgResolver := s.Manager.OrganizationResolver(tx, orgID) settings, err := s.RoleSyncSettings(ctx, orgID, tx)
settings, err := s.RoleSyncSettings().Resolve(ctx, orgResolver)
if err != nil { 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 // No entry means no role syncing for this organization
continue continue
} }
if settings.Field == "" { if settings.Field == "" {
// Explicitly disabled role sync for this organization // Explicitly disabled role sync for this organization
continue continue
@ -261,14 +277,7 @@ func (AGPLIDPSync) RolesFromClaim(field string, claims jwt.MapClaims) ([]string,
return parsedRoles, nil return parsedRoles, nil
} }
type RoleSyncSettings struct { type RoleSyncSettings codersdk.RoleSyncSettings
// 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 (s *RoleSyncSettings) Set(v string) error { func (s *RoleSyncSettings) Set(v string) error {
return json.Unmarshal([]byte(v), s) return json.Unmarshal([]byte(v), s)

View File

@ -60,3 +60,40 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re
var resp GroupSyncSettings var resp GroupSyncSettings
return resp, json.NewDecoder(res.Body).Decode(&resp) 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)
}

View File

@ -1817,9 +1817,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/setting
### Responses ### Responses
| Status | Meaning | Description | Schema | | 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.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) |
To perform this operation, you must be authenticated. [Learn more](authentication.md). 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 ### 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 | | 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). To perform this operation, you must be authenticated. [Learn more](authentication.md).

View File

@ -2895,6 +2895,36 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `user` | | `user` |
| `oidc` | | `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 ## codersdk.Healthcheck
```json ```json
@ -4660,6 +4690,26 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | | `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_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 ## codersdk.SSHConfig
```json ```json
@ -8964,36 +9014,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `severity` | `warning` | | `severity` | `warning` |
| `severity` | `error` | | `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 ## key.NodePublic
```json ```json

View File

@ -286,9 +286,18 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Post("/organizations/{organization}/members/roles", api.postOrgRoles) r.Post("/organizations/{organization}/members/roles", api.postOrgRoles)
r.Put("/organizations/{organization}/members/roles", api.putOrgRoles) r.Put("/organizations/{organization}/members/roles", api.putOrgRoles)
r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole) 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.Route("/organizations/{organization}/settings", func(r chi.Router) {
r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Get("/idpsync/groups", api.groupIDPSyncSettings)
r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings)
r.Get("/idpsync/roles", api.roleIDPSyncSettings)
r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings)
}) })
}) })

View File

@ -17,7 +17,7 @@ import (
// @Produce json // @Produce json
// @Tags Enterprise // @Tags Enterprise
// @Param organization path string true "Organization ID" format(uuid) // @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] // @Router /organizations/{organization}/settings/idpsync/groups [get]
func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -45,7 +45,7 @@ func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
// @Produce json // @Produce json
// @Tags Enterprise // @Tags Enterprise
// @Param organization path string true "Organization ID" format(uuid) // @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] // @Router /organizations/{organization}/settings/idpsync/groups [patch]
func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -77,3 +77,70 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques
httpapi.Write(ctx, rw, http.StatusOK, settings) 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)
}

View File

@ -170,3 +170,122 @@ func TestPostGroupSyncConfig(t *testing.T) {
require.Equal(t, http.StatusForbidden, apiError.StatusCode()) 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())
})
}

View File

@ -1164,6 +1164,12 @@ export interface Role {
readonly user_permissions: Readonly<Array<Permission>>; readonly user_permissions: Readonly<Array<Permission>>;
} }
// From codersdk/idpsync.go
export interface RoleSyncSettings {
readonly field: string;
readonly mapping: Record<string, Readonly<Array<string>>>;
}
// From codersdk/deployment.go // From codersdk/deployment.go
export interface SSHConfig { export interface SSHConfig {
readonly DeploymentName: string; readonly DeploymentName: string;