diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0b4778a20cbce..a23488052007a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3170,6 +3170,52 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/field-values": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Get the organization idp sync claim field values", + "operationId": "get-the-organization-idp-sync-claim-field-values", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "string", + "description": "Claim Field", + "name": "claimField", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/groups": { "get": { "security": [ @@ -3952,6 +3998,52 @@ const docTemplate = `{ } } }, + "/settings/idpsync/field-values": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Get the idp sync claim field values", + "operationId": "get-the-idp-sync-claim-field-values", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "string", + "description": "Claim Field", + "name": "claimField", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/settings/idpsync/organization": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e9fbb8a5a9083..67d394ae67c25 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2788,6 +2788,48 @@ } } }, + "/organizations/{organization}/settings/idpsync/field-values": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Get the organization idp sync claim field values", + "operationId": "get-the-organization-idp-sync-claim-field-values", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "string", + "description": "Claim Field", + "name": "claimField", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/groups": { "get": { "security": [ @@ -3478,6 +3520,48 @@ } } }, + "/settings/idpsync/field-values": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Get the idp sync claim field values", + "operationId": "get-the-idp-sync-claim-field-values", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "string", + "description": "Claim Field", + "name": "claimField", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/settings/idpsync/organization": { "get": { "security": [ diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 3a2e707ccb623..2cc1f51ee3011 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "regexp" "github.com/google/uuid" @@ -163,3 +164,35 @@ func (c *Client) GetOrganizationAvailableIDPSyncFields(ctx context.Context, orgI var resp []string return resp, json.NewDecoder(res.Body).Decode(&resp) } + +func (c *Client) GetIDPSyncFieldValues(ctx context.Context, claimField string) ([]string, error) { + qv := url.Values{} + qv.Add("claimField", claimField) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/settings/idpsync/field-values?%s", qv.Encode()), nil) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var resp []string + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +func (c *Client) GetOrganizationIDPSyncFieldValues(ctx context.Context, orgID string, claimField string) ([]string, error) { + qv := url.Values{} + qv.Add("claimField", claimField) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/field-values?%s", orgID, qv.Encode()), nil) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var resp []string + return resp, json.NewDecoder(res.Body).Decode(&resp) +} diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 21a46e92cba08..80515f3ee7bef 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1830,6 +1830,46 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/setting To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get the organization idp sync claim field values + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/field-values?claimField=string \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/settings/idpsync/field-values` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|-------|----------------|----------|-----------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `claimField` | query | string(string) | true | Claim Field | + +### Example responses + +> 200 Response + +```json +[ + "string" +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-----------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of string | + +

Response Schema

+ +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get group IdP Sync settings by organization ### Code samples @@ -2536,6 +2576,46 @@ curl -X GET http://coder-server:8080/api/v2/settings/idpsync/available-fields \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get the idp sync claim field values + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/settings/idpsync/field-values?claimField=string \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /settings/idpsync/field-values` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|-------|----------------|----------|-----------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `claimField` | query | string(string) | true | Claim Field | + +### Example responses + +> 200 Response + +```json +[ + "string" +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-----------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of string | + +

Response Schema

+ +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get organization IdP Sync settings ### Code samples diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 71273ff97fd75..90042bf0997fc 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -297,6 +297,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Patch("/", api.patchOrganizationIDPSyncSettings) }) r.Get("/available-fields", api.deploymentIDPSyncClaimFields) + r.Get("/field-values", api.deploymentIDPSyncClaimFieldValues) }) }) @@ -311,6 +312,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) r.Get("/idpsync/roles", api.roleIDPSyncSettings) r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) + r.Get("/idpsync/field-values", api.organizationIDPSyncClaimFieldValues) }) }) diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index e7346f8406844..192d61ea996c6 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -363,3 +363,63 @@ func (api *API) idpSyncClaimFields(orgID uuid.UUID, rw http.ResponseWriter, r *h httpapi.Write(ctx, rw, http.StatusOK, fields) } + +// @Summary Get the organization idp sync claim field values +// @ID get-the-organization-idp-sync-claim-field-values +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Param claimField query string true "Claim Field" format(string) +// @Success 200 {array} string +// @Router /organizations/{organization}/settings/idpsync/field-values [get] +func (api *API) organizationIDPSyncClaimFieldValues(rw http.ResponseWriter, r *http.Request) { + org := httpmw.OrganizationParam(r) + api.idpSyncClaimFieldValues(org.ID, rw, r) +} + +// @Summary Get the idp sync claim field values +// @ID get-the-idp-sync-claim-field-values +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Param claimField query string true "Claim Field" format(string) +// @Success 200 {array} string +// @Router /settings/idpsync/field-values [get] +func (api *API) deploymentIDPSyncClaimFieldValues(rw http.ResponseWriter, r *http.Request) { + // nil uuid implies all organizations + api.idpSyncClaimFieldValues(uuid.Nil, rw, r) +} + +func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + claimField := r.URL.Query().Get("claimField") + if claimField == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "claimField query parameter is required", + }) + return + } + fieldValues, err := api.Database.OIDCClaimFieldValues(ctx, database.OIDCClaimFieldValuesParams{ + OrganizationID: orgID, + ClaimField: claimField, + }) + + if httpapi.IsUnauthorizedError(err) { + // Give a helpful error. The user could read the org, so this does not + // leak anything. + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: "You do not have permission to view the IDP claim field values", + Detail: fmt.Sprintf("%s.read permission is required", rbac.ResourceIdpsyncSettings.Type), + }) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, fieldValues) +} diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index 28257078ebb36..d3e997608f316 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -178,6 +178,14 @@ func TestUserOIDC(t *testing.T) { require.NoError(t, err) require.ElementsMatch(t, fields, orgFields) + fieldValues, err := runner.AdminClient.GetIDPSyncFieldValues(ctx, "organization") + require.NoError(t, err) + require.ElementsMatch(t, []string{"first", "second"}, fieldValues) + + orgFieldValues, err := runner.AdminClient.GetOrganizationIDPSyncFieldValues(ctx, orgOne.ID.String(), "organization") + require.NoError(t, err) + require.ElementsMatch(t, []string{"first", "second"}, orgFieldValues) + // When: they are manually added to the fourth organization, a new sync // should remove them. _, err = runner.AdminClient.PostOrganizationMember(ctx, orgThree.ID, "alice")