Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
api.oauthServer = oauthserver.NewServer(globalConfig, db, api.tokenService)
}

api.scim = scim.NewServer(globalConfig)

if api.config.Password.HIBP.Enabled {
httpClient := &http.Client{
// all HIBP API requests should finish quickly to avoid
Expand Down Expand Up @@ -213,14 +215,6 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.Post("/", api.ExternalProviderCallback)
})

api.scim = scim.NewServer(globalConfig)
r.Route("/scim/v2", func(r *router) {
r.Use(api.scim.Middleware)
r.Get("/ServiceProviderConfig", api.scim.ServiceProviderConfig)
r.Get("/ResourceTypes", api.scim.ResourceTypes)
r.Get("/Schemas", api.scim.Schemas)
})

r.Route("/", func(r *router) {

r.Use(api.isValidExternalHost)
Expand Down Expand Up @@ -456,6 +450,14 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.With(api.requireAuthentication).Get("/authorizations/{authorization_id}", api.oauthServer.OAuthServerGetAuthorization)
r.With(api.requireAuthentication).Post("/authorizations/{authorization_id}/consent", api.oauthServer.OAuthServerConsent)
})

r.Route(scim.BasePath, func(r *router) {
r.Use(api.requireScimServerEnabled)

r.Get("/ServiceProviderConfig", api.scim.ServiceProviderConfig)
r.Get("/ResourceTypes", api.scim.ResourceTypes)
r.Get("/Schemas", api.scim.Schemas)
})
})

corsHandler := cors.New(cors.Options{
Expand Down
8 changes: 8 additions & 0 deletions internal/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ func (a *API) requirePasskeyEnabled(w http.ResponseWriter, req *http.Request) (c
return ctx, nil
}

func (a *API) requireScimServerEnabled(w http.ResponseWriter, req *http.Request) (context.Context, error) {
ctx := req.Context()
if !a.config.Experimental.ScimEnabled {
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeFeatureDisabled, "SCIM server is disabled")
}
return ctx, nil
}

func (a *API) databaseCleanup(cleanup models.Cleaner) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/scim/protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import (
const MediaType = "application/scim+json"

func Send(w http.ResponseWriter, status int, obj any) error {
return shared.SendJSONAs(w, status, MediaType, obj)
return shared.JSON(w).ContentType(MediaType).Status(status).Send(obj)
}
11 changes: 2 additions & 9 deletions internal/api/scim/server.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package scim

import (
"context"
"net/http"

"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/api/scim/protocol"
"github.com/supabase/auth/internal/conf"
)

const BasePath = "/scim/v2"

type Server struct {
config *conf.GlobalConfiguration
}
Expand All @@ -19,13 +19,6 @@ func NewServer(config *conf.GlobalConfiguration) *Server {
}
}

func (srv *Server) Middleware(w http.ResponseWriter, r *http.Request) (context.Context, error) {
if !srv.config.Experimental.ScimEnabled {
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeFeatureDisabled, "SCIM server is disabled")
}
return r.Context(), nil
}

func (srv *Server) ServiceProviderConfig(w http.ResponseWriter, r *http.Request) error {
return srv.notImplemented(w, r)
}
Expand Down
4 changes: 4 additions & 0 deletions internal/api/scim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ func TestSCIM(t *testing.T) {
for _, path := range scimPaths {
r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()

api.handler.ServeHTTP(w, r)

require.Equal(t, http.StatusNotFound, w.Code)
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
require.JSONEq(t, `{"code":404,"error_code":"feature_disabled","msg":"SCIM server is disabled"}`, w.Body.String())
}
})

Expand All @@ -55,6 +58,7 @@ func TestSCIM(t *testing.T) {

r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()

api.handler.ServeHTTP(w, r)

require.Equal(t, w.Code, http.StatusNotImplemented)
Expand Down
20 changes: 1 addition & 19 deletions internal/api/shared/http.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
package shared

import (
"encoding/json"
"net/http"

"github.com/pkg/errors"
)

// SendJSON sends a JSON response with proper error handling
func SendJSON(w http.ResponseWriter, status int, obj any) error {
return SendJSONAs(w, status, "application/json", obj)
}

func SendJSONAs(w http.ResponseWriter, status int, contentType string, obj any) error {
var b []byte
if obj != nil {
var err error
b, err = json.Marshal(obj)
if err != nil {
return errors.Wrapf(err, "Error encoding json response: %v", obj)
}
}
w.Header().Set("Content-Type", contentType)
w.WriteHeader(status)
_, err := w.Write(b)
return err
return JSON(w).ContentType("application/json").Status(status).Send(obj)
}
10 changes: 5 additions & 5 deletions internal/api/shared/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@ import (
"github.com/stretchr/testify/require"
)

func TestSendJSONAs(t *testing.T) {
func TestSendJSON(t *testing.T) {
t.Run("with an empty body", func(t *testing.T) {
w := httptest.NewRecorder()

require.NoError(t, SendJSONAs(w, http.StatusTeapot, "application/example+json", nil))
require.NoError(t, SendJSON(w, http.StatusTeapot, nil))

assert.Equal(t, http.StatusTeapot, w.Code)
assert.Equal(t, "application/example+json", w.Header().Get("Content-Type"))
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, "", w.Body.String())
})

t.Run("with a JSON body", func(t *testing.T) {
w := httptest.NewRecorder()

require.NoError(t, SendJSONAs(w, http.StatusTeapot, "application/example+json", map[string]string{"key": "value"}))
require.NoError(t, SendJSON(w, http.StatusTeapot, map[string]string{"key": "value"}))

assert.Equal(t, http.StatusTeapot, w.Code)
assert.Equal(t, "application/example+json", w.Header().Get("Content-Type"))
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, `{"key":"value"}`, w.Body.String())
})
}
53 changes: 53 additions & 0 deletions internal/api/shared/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package shared

import (
"encoding/json"
"net/http"

"github.com/pkg/errors"
)

type JSONResponse struct {
w http.ResponseWriter
status int
}

func JSON(w http.ResponseWriter) *JSONResponse {
j := &JSONResponse{w: w}
return j.ContentType("application/json").Status(http.StatusOK)
}

func (j *JSONResponse) Header(key, value string) *JSONResponse {
j.w.Header().Set(key, value)
return j
}

func (j *JSONResponse) ContentType(contentType string) *JSONResponse {
return j.Header("Content-Type", contentType)
}

func (j *JSONResponse) Status(status int) *JSONResponse {
j.status = status
return j
}

func (j *JSONResponse) Write(b []byte) error {
j.w.WriteHeader(j.status)
if len(b) == 0 {
return nil
}
_, err := j.w.Write(b)
return err
}

func (j *JSONResponse) Send(obj any) error {
var b []byte
if obj != nil {
var err error
b, err = json.Marshal(obj)
if err != nil {
return errors.Wrapf(err, "Error encoding json response: %v", obj)
}
}
return j.Write(b)
}
Loading