feat: option to disallow public org registration (#6917)

* feat: return 404 or 409 if org reg disallowed

* fix: system limit permissions

* feat: add iam limits api

* feat: disallow public org registrations on default instance

* add integration test

* test: integration

* fix test

* docs: describe public org registrations

* avoid updating docs deps

* fix system limits integration test

* silence integration tests

* fix linting

* ignore strange linter complaints

* review

* improve reset properties naming

* redefine the api

* use restrictions aggregate

* test query

* simplify and test projection

* test commands

* fix unit tests

* move integration test

* support restrictions on default instance

* also test GetRestrictions

* self review

* lint

* abstract away resource owner

* fix tests

* lint
This commit is contained in:
Elio Bischof
2023-11-22 10:29:38 +01:00
committed by GitHub
parent 5fa596a871
commit 76fe032b5f
45 changed files with 1280 additions and 123 deletions

View File

@@ -4,32 +4,17 @@ package admin_test
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
var (
Tester *integration.Tester
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
return m.Run()
}())
}
func TestServer_Healthz(t *testing.T) {
_, err := Tester.Client.Admin.Healthz(context.TODO(), &admin.HealthzRequest{})
ctx, cancel := context.WithTimeout(AdminCTX, time.Minute)
defer cancel()
_, err := Tester.Client.Admin.Healthz(ctx, &admin.HealthzRequest{})
require.NoError(t, err)
}

View File

@@ -0,0 +1,30 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) {
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration})
if err != nil {
return nil, err
}
return &admin.SetRestrictionsResponse{
Details: object.ChangeToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
}, nil
}
func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRequest) (*admin.GetRestrictionsResponse, error) {
restrictions, err := s.query.GetInstanceRestrictions(ctx)
if err != nil {
return nil, err
}
return &admin.GetRestrictionsResponse{
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
}, nil
}

View File

@@ -0,0 +1,106 @@
//go:build integration
package admin_test
import (
"bytes"
"context"
"github.com/muhlemmer/gu"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX)
regOrgUrl, err := url.Parse("http://" + domain + ":8080/ui/login/register/org")
require.NoError(t, err)
// The CSRF cookie must be sent with every request.
// We can simulate a browser session using a cookie jar.
jar, err := cookiejar.New(nil)
require.NoError(t, err)
browserSession := &http.Client{Jar: jar}
// Default should be allowed
csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
require.NoError(t, err)
awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
require.NoError(t, err)
awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
}
// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
require.False(t, restrictions.DisallowPublicOrgRegistration)
return csrfToken
}
// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
require.True(t, restrictions.DisallowPublicOrgRegistration)
}
// awaitGetResponse cuts the CSRF token from the response body if it exists
func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
var csrfToken []byte
await(t, ctx, func() bool {
resp, err := client.Get(parsedURL.String())
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
searchField := `<input type="hidden" name="gorilla.csrf.Token" value="`
_, after, hasCsrfToken := bytes.Cut(body, []byte(searchField))
if hasCsrfToken {
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
}
return resp.StatusCode == expectCode
})
return string(csrfToken)
}
// awaitPostFormResponse needs a valid CSRF token to make it to the actual endpoint implementation and get the expected status code
func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int, csrfToken string) {
await(t, ctx, func() bool {
resp, err := client.PostForm(parsedURL.String(), url.Values{
"gorilla.csrf.Token": {csrfToken},
})
require.NoError(t, err)
return resp.StatusCode == expectCode
})
}
func await(t *testing.T, ctx context.Context, cb func() bool) {
deadline, ok := ctx.Deadline()
require.True(t, ok, "context must have deadline")
require.Eventuallyf(
t,
func() bool {
defer func() {
require.Nil(t, recover(), "panic in await callback")
}()
return cb()
},
time.Until(deadline),
100*time.Millisecond,
"awaiting successful callback failed",
)
}

View File

@@ -0,0 +1,32 @@
//go:build integration
package admin_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
)
var (
AdminCTX, SystemCTX context.Context
Tester *integration.Tester
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(3 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
return m.Run()
}())
}