diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 27514c3a56..83d1fdc2c4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3623,6 +3623,23 @@ const docTemplate = `{ } } }, + "/scim/v2/ServiceProviderConfig": { + "get": { + "produces": [ + "application/scim+json" + ], + "tags": [ + "Enterprise" + ], + "summary": "SCIM 2.0: Service Provider Config", + "operationId": "scim-get-service-provider-config", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/scim/v2/Users": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 9457184c6d..9861e195b7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3189,6 +3189,19 @@ } } }, + "/scim/v2/ServiceProviderConfig": { + "get": { + "produces": ["application/scim+json"], + "tags": ["Enterprise"], + "summary": "SCIM 2.0: Service Provider Config", + "operationId": "scim-get-service-provider-config", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/scim/v2/Users": { "get": { "security": [ diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index a5c0857ede..57ffa5260e 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -2007,6 +2007,24 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## SCIM 2.0: Service Provider Config + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/scim/v2/ServiceProviderConfig + +``` + +`GET /scim/v2/ServiceProviderConfig` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + ## SCIM 2.0: Get users ### Code samples diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 2549a008e5..7e59eb3414 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -457,6 +457,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Use( api.RequireFeatureMW(codersdk.FeatureSCIM), ) + r.Get("/ServiceProviderConfig", api.scimServiceProviderConfig) r.Post("/Users", api.scimPostUser) r.Route("/Users", func(r chi.Router) { r.Get("/", api.scimGetUsers) diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go index 1e2f70b57b..439e6ca322 100644 --- a/enterprise/coderd/scim.go +++ b/enterprise/coderd/scim.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "net/http" + "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -21,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/scim" ) func (api *API) scimVerifyAuthHeader(r *http.Request) bool { @@ -34,6 +36,69 @@ func (api *API) scimVerifyAuthHeader(r *http.Request) bool { return len(api.SCIMAPIKey) != 0 && subtle.ConstantTimeCompare(hdr, api.SCIMAPIKey) == 1 } +// scimServiceProviderConfig returns a static SCIM service provider configuration. +// +// @Summary SCIM 2.0: Service Provider Config +// @ID scim-get-service-provider-config +// @Produce application/scim+json +// @Tags Enterprise +// @Success 200 +// @Router /scim/v2/ServiceProviderConfig [get] +func (api *API) scimServiceProviderConfig(rw http.ResponseWriter, _ *http.Request) { + // No auth needed to query this endpoint. + + rw.Header().Set("Content-Type", spec.ApplicationScimJson) + rw.WriteHeader(http.StatusOK) + + // providerUpdated is the last time the static provider config was updated. + // Increment this time if you make any changes to the provider config. + providerUpdated := time.Date(2024, 10, 25, 17, 0, 0, 0, time.UTC) + var location string + locURL, err := api.AccessURL.Parse("/scim/v2/ServiceProviderConfig") + if err == nil { + location = locURL.String() + } + + enc := json.NewEncoder(rw) + enc.SetEscapeHTML(true) + _ = enc.Encode(scim.ServiceProviderConfig{ + Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"}, + DocURI: "https://coder.com/docs/admin/users/oidc-auth#scim-enterprise-premium", + Patch: scim.Supported{ + Supported: true, + }, + Bulk: scim.BulkSupported{ + Supported: false, + }, + Filter: scim.FilterSupported{ + Supported: false, + }, + ChangePassword: scim.Supported{ + Supported: false, + }, + Sort: scim.Supported{ + Supported: false, + }, + ETag: scim.Supported{ + Supported: false, + }, + AuthSchemes: []scim.AuthenticationScheme{ + { + Type: "oauthbearertoken", + Name: "HTTP Header Authentication", + Description: "Authentication scheme using the Authorization header with the shared token", + DocURI: "https://coder.com/docs/admin/users/oidc-auth#scim-enterprise-premium", + }, + }, + Meta: scim.ServiceProviderMeta{ + Created: providerUpdated, + LastModified: providerUpdated, + Location: location, + ResourceType: "ServiceProviderConfig", + }, + }) +} + // scimGetUsers intentionally always returns no users. This is done to always force // Okta to try and create each user individually, this way we don't need to // implement fetching users twice. diff --git a/enterprise/coderd/scim/scimtypes.go b/enterprise/coderd/scim/scimtypes.go new file mode 100644 index 0000000000..e78b70b3e9 --- /dev/null +++ b/enterprise/coderd/scim/scimtypes.go @@ -0,0 +1,46 @@ +package scim + +import "time" + +type ServiceProviderConfig struct { + Schemas []string `json:"schemas"` + DocURI string `json:"documentationUri"` + Patch Supported `json:"patch"` + Bulk BulkSupported `json:"bulk"` + Filter FilterSupported `json:"filter"` + ChangePassword Supported `json:"changePassword"` + Sort Supported `json:"sort"` + ETag Supported `json:"etag"` + AuthSchemes []AuthenticationScheme `json:"authenticationSchemes"` + Meta ServiceProviderMeta `json:"meta"` +} + +type ServiceProviderMeta struct { + Created time.Time `json:"created"` + LastModified time.Time `json:"lastModified"` + Location string `json:"location"` + ResourceType string `json:"resourceType"` +} + +type Supported struct { + Supported bool `json:"supported"` +} + +type BulkSupported struct { + Supported bool `json:"supported"` + MaxOp int `json:"maxOperations"` + MaxPayload int `json:"maxPayloadSize"` +} + +type FilterSupported struct { + Supported bool `json:"supported"` + MaxResults int `json:"maxResults"` +} + +type AuthenticationScheme struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + SpecURI string `json:"specUri"` + DocURI string `json:"documentationUri"` +} diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go index 016c75d095..82355c3a3b 100644 --- a/enterprise/coderd/scim_test.go +++ b/enterprise/coderd/scim_test.go @@ -140,9 +140,15 @@ func TestScim(t *testing.T) { }) mockAudit.ResetLogs() + // verify scim is enabled + res, err := client.Request(ctx, http.MethodGet, "/scim/v2/ServiceProviderConfig", nil) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + // when sUser := makeScimUser(t) - res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey)) + res, err = client.Request(ctx, http.MethodPost, "/scim/v2/Users", sUser, setScimAuth(scimAPIKey)) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode)