Skip to content
Open
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
12 changes: 12 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/supabase/auth/internal/api/apitask"
"github.com/supabase/auth/internal/api/oauthserver"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/api/scim"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/hooks/hookshttp"
"github.com/supabase/auth/internal/hooks/hookspgfunc"
Expand Down Expand Up @@ -46,6 +47,7 @@ type API struct {
hooksMgr *v0hooks.Manager
hibpClient *hibp.PwnedClient
oauthServer *oauthserver.Server
scim *scim.Server
tokenService *tokens.Service
mailer mailer.Mailer
oidcCache *provider.OIDCProviderCache
Expand Down Expand Up @@ -211,6 +213,16 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.Post("/", api.ExternalProviderCallback)
})

if globalConfig.Experimental.ScimEnabled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a middleware could be better to prevent non-JSON 404 responses, relevant PR: #2457

props to @fadymak

api.scim = scim.NewServer()

r.Route("/scim/v2", func(r *router) {
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
32 changes: 32 additions & 0 deletions internal/api/scim/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package scim

import (
"net/http"
)

const mediaType = "application/scim+json"

type Server struct {
}

func NewServer() *Server {
return &Server{}
}

func (srv *Server) ServiceProviderConfig(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", mediaType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably better to have a centralized placed for these?

w.WriteHeader(http.StatusNotImplemented)
return nil
}

func (srv *Server) ResourceTypes(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(http.StatusNotImplemented)
return nil
}

func (srv *Server) Schemas(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(http.StatusNotImplemented)
return nil
}
34 changes: 34 additions & 0 deletions internal/api/scim/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package scim

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestServer(t *testing.T) {
t.Run("NewServer", func(t *testing.T) {
require.NotNil(t, NewServer())
})

srv := NewServer()
for _, tc := range []struct {
path string
handler func(http.ResponseWriter, *http.Request) error
}{
{"ServiceProviderConfig", srv.ServiceProviderConfig},
{"ResourceTypes", srv.ResourceTypes},
{"Schemas", srv.Schemas},
} {
t.Run(tc.path, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/scim/v2/"+tc.path, nil)
w := httptest.NewRecorder()

require.NoError(t, tc.handler(w, r))
require.Equal(t, w.Code, http.StatusNotImplemented)
require.Equal(t, "application/scim+json", w.Header().Get("Content-Type"))
})
}
}
64 changes: 64 additions & 0 deletions internal/api/scim_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package api

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/storage"
)

var scimPaths = []string{
"/scim/v2/ServiceProviderConfig",
"/scim/v2/ResourceTypes",
"/scim/v2/Schemas",
}

func TestSCIM(t *testing.T) {
t.Run("Disabled by default", func(t *testing.T) {
api, _, err := setupAPIForTest()
require.NoError(t, err)

require.False(t, api.config.Experimental.ScimEnabled)
require.Nil(t, api.scim)

for _, path := range scimPaths {
r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
api.handler.ServeHTTP(w, r)

require.Equal(t, w.Code, http.StatusNotFound)
}
})

t.Run("Can be enabled", func(t *testing.T) {
api, _, err := setupAPIForTestWithCallback(func(config *conf.GlobalConfiguration, conn *storage.Connection) {
if config != nil {
config.Experimental.ScimEnabled = true
}
})
require.NoError(t, err)

require.True(t, api.config.Experimental.ScimEnabled)
require.NotNil(t, api.scim)
})

for _, path := range scimPaths {
t.Run(path, func(t *testing.T) {
api, _, err := setupAPIForTestWithCallback(func(config *conf.GlobalConfiguration, conn *storage.Connection) {
if config != nil {
config.Experimental.ScimEnabled = true
}
})
require.NoError(t, err)

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

require.Equal(t, w.Code, http.StatusNotImplemented)
})
}
}
5 changes: 5 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ type ExperimentalConfiguration struct {
// provided, GET /admin/users serves fast, count-free cursor pages.
// Env: GOTRUE_EXPERIMENTAL_CURSOR_PAGINATION_ENABLED=true
CursorPaginationEnabled bool `split_words:"true" default:"false"`

// ScimEnabled gates the /scim/v2 router. Ships dark: no per-provider
// enablement yet, just a kill switch for internal verification.
// Env: GOTRUE_EXPERIMENTAL_SCIM_ENABLED=true
ScimEnabled bool `split_words:"true" default:"false"`
}

// ReloadingConfiguration holds the configuration values for runtime
Expand Down
28 changes: 28 additions & 0 deletions internal/conf/confload/confload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,34 @@ func TestExperimentalCursorPaginationEnabled(t *testing.T) {
}
}

func TestExperimentalScimEndpoints(t *testing.T) {
baseEnv := func() {
os.Clearenv()
os.Setenv("GOTRUE_SITE_URL", "http://localhost:8080")
os.Setenv("GOTRUE_DB_DRIVER", "postgres")
os.Setenv("GOTRUE_DB_DATABASE_URL", "fake")
os.Setenv("GOTRUE_JWT_SECRET", "secret")
os.Setenv("API_EXTERNAL_URL", "http://localhost:9999")
}

{
baseEnv()
cfg, err := LoadGlobalFromEnv()
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Equal(t, false, cfg.Experimental.ScimEnabled)
}

{
baseEnv()
os.Setenv("GOTRUE_EXPERIMENTAL_SCIM_ENABLED", "true")
cfg, err := LoadGlobalFromEnv()
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Equal(t, true, cfg.Experimental.ScimEnabled)
}
}

func TestLoading(t *testing.T) {
os.Clearenv()

Expand Down
Loading