chore: add scim service provider config endpoint (#15235)

Adds a static `/scim/v2/ServiceProviderConfig` endpoint. Our scim
support is static, so the response config is also defined statically.
This commit is contained in:
Steven Masley
2024-10-25 18:27:34 -04:00
committed by GitHub
parent 27f5ff2dd1
commit e03ef62a13
7 changed files with 167 additions and 1 deletions
+1
View File
@@ -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)
+65
View File
@@ -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.
+46
View File
@@ -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"`
}
+7 -1
View File
@@ -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)