mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
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:
parent
5fa596a871
commit
76fe032b5f
2
Makefile
2
Makefile
@ -100,7 +100,7 @@ core_integration_setup:
|
||||
|
||||
.PHONY: core_integration_test
|
||||
core_integration_test: core_integration_setup
|
||||
go test -tags=integration -race -p 1 -v -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... ./internal/notification/handlers/... ./internal/api/oidc/...
|
||||
go test -tags=integration -race -p 1 -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... ./internal/notification/handlers/... ./internal/api/oidc/...
|
||||
|
||||
.PHONY: console_lint
|
||||
console_lint:
|
||||
|
@ -823,6 +823,10 @@ DefaultInstance:
|
||||
# A value of "0s" means that all events are available.
|
||||
# If this value is set, it overwrites the system default unless it is not reset via the admin API.
|
||||
AuditLogRetention: # ZITADEL_DEFAULTINSTANCE_LIMITS_AUDITLOGRETENTION
|
||||
Restrictions:
|
||||
# DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org
|
||||
# If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.
|
||||
DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION
|
||||
Quotas:
|
||||
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
||||
# The following unit types are supported
|
||||
@ -907,6 +911,8 @@ InternalAuthZ:
|
||||
- "iam.flow.write"
|
||||
- "iam.flow.delete"
|
||||
- "iam.feature.write"
|
||||
- "iam.restrictions.read"
|
||||
- "iam.restrictions.write"
|
||||
- "org.read"
|
||||
- "org.global.read"
|
||||
- "org.create"
|
||||
@ -967,6 +973,7 @@ InternalAuthZ:
|
||||
- "iam.idp.read"
|
||||
- "iam.action.read"
|
||||
- "iam.flow.read"
|
||||
- "iam.restrictions.read"
|
||||
- "org.read"
|
||||
- "org.member.read"
|
||||
- "org.idp.read"
|
||||
|
14
docs/docs/guides/manage/customize/restrictions.md
Normal file
14
docs/docs/guides/manage/customize/restrictions.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Feature Restrictions
|
||||
---
|
||||
|
||||
New self-hosted and [ZITADEL Cloud instances](https://zitadel.com/signin) are unrestricted by default.
|
||||
Self-hosters can change this default using the DefaultInstance.Restrictions configuration section.
|
||||
Users with the role IAM_OWNER can change the restrictions of their instance using the [Feature Restrictions Admin API](/category/apis/resources/admin/feature-restrictions).
|
||||
Currently, the following restrictions are available:
|
||||
|
||||
- *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests.
|
||||
- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages*
|
||||
|
||||
Feature restrictions for an instance are intended to be configured by a user that is managed within that instance.
|
||||
However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users.
|
@ -94,6 +94,7 @@ module.exports = {
|
||||
"guides/manage/customize/branding",
|
||||
"guides/manage/customize/texts",
|
||||
"guides/manage/customize/behavior",
|
||||
"guides/manage/customize/restrictions",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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)
|
||||
}
|
||||
|
30
internal/api/grpc/admin/restrictions.go
Normal file
30
internal/api/grpc/admin/restrictions.go
Normal 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
|
||||
}
|
106
internal/api/grpc/admin/restrictions_integration_test.go
Normal file
106
internal/api/grpc/admin/restrictions_integration_test.go
Normal 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",
|
||||
)
|
||||
}
|
32
internal/api/grpc/admin/server_integration_test.go
Normal file
32
internal/api/grpc/admin/server_integration_test.go
Normal 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()
|
||||
}())
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@ -38,6 +39,11 @@ type registerOrgData struct {
|
||||
}
|
||||
|
||||
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
||||
if disallowed || err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
data := new(registerOrgFormData)
|
||||
authRequest, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
@ -48,6 +54,11 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
|
||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
||||
if disallowed || err != nil {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
data := new(registerOrgFormData)
|
||||
authRequest, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
@ -119,6 +130,11 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) {
|
||||
restrictions, err := l.query.GetInstanceRestrictions(ctx)
|
||||
return restrictions.DisallowPublicOrgRegistration, err
|
||||
}
|
||||
|
||||
func (d registerOrgFormData) toUserDomain() *domain.Human {
|
||||
if d.Username == "" {
|
||||
d.Username = string(d.Email)
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
@ -152,6 +153,7 @@ func StartCommands(
|
||||
action.RegisterEventMappers(repo.eventstore)
|
||||
quota.RegisterEventMappers(repo.eventstore)
|
||||
limits.RegisterEventMappers(repo.eventstore)
|
||||
restrictions.RegisterEventMappers(repo.eventstore)
|
||||
session.RegisterEventMappers(repo.eventstore)
|
||||
idpintent.RegisterEventMappers(repo.eventstore)
|
||||
authrequest.RegisterEventMappers(repo.eventstore)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
@ -115,10 +116,9 @@ type InstanceSetup struct {
|
||||
Quotas *struct {
|
||||
Items []*SetQuota
|
||||
}
|
||||
Features map[domain.Feature]any
|
||||
Limits *struct {
|
||||
AuditLogRetention *time.Duration
|
||||
}
|
||||
Features map[domain.Feature]any
|
||||
Limits *SetLimits
|
||||
Restrictions *SetRestrictions
|
||||
}
|
||||
|
||||
type SecretGenerators struct {
|
||||
@ -135,12 +135,13 @@ type SecretGenerators struct {
|
||||
}
|
||||
|
||||
type ZitadelConfig struct {
|
||||
projectID string
|
||||
mgmtAppID string
|
||||
adminAppID string
|
||||
authAppID string
|
||||
consoleAppID string
|
||||
limitsID string
|
||||
projectID string
|
||||
mgmtAppID string
|
||||
adminAppID string
|
||||
authAppID string
|
||||
consoleAppID string
|
||||
limitsID string
|
||||
restrictionsID string
|
||||
}
|
||||
|
||||
func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
|
||||
@ -169,6 +170,10 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
|
||||
return err
|
||||
}
|
||||
s.zitadel.limitsID, err = idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.zitadel.restrictionsID, err = idGenerator.Next()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -200,6 +205,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
userAgg := user.NewAggregate(userID, orgID)
|
||||
projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID)
|
||||
limitsAgg := limits.NewAggregate(setup.zitadel.limitsID, instanceID, instanceID)
|
||||
restrictionsAgg := restrictions.NewAggregate(setup.zitadel.restrictionsID, instanceID, instanceID)
|
||||
|
||||
validations := []preparation.Validation{
|
||||
prepareAddInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage),
|
||||
@ -453,9 +459,11 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
}
|
||||
|
||||
if setup.Limits != nil {
|
||||
validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, &SetLimits{
|
||||
AuditLogRetention: setup.Limits.AuditLogRetention,
|
||||
}))
|
||||
validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setup.Limits))
|
||||
}
|
||||
|
||||
if setup.Restrictions != nil {
|
||||
validations = append(validations, c.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setup.Restrictions))
|
||||
}
|
||||
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type SetLimits struct {
|
||||
AuditLogRetention *time.Duration `json:"AuditLogRetention,omitempty"`
|
||||
AuditLogRetention *time.Duration
|
||||
}
|
||||
|
||||
// SetLimits creates new limits or updates existing limits.
|
||||
@ -34,14 +34,14 @@ func (c *Commands) SetLimits(
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createCmds, err := c.SetLimitsCommand(limits.NewAggregate(aggregateId, instanceId, resourceOwner), wm, setLimits)()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := createCmds(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) > 0 {
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||
quota_repo "github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
@ -60,6 +61,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
||||
oidcsession.RegisterEventMappers(es)
|
||||
quota_repo.RegisterEventMappers(es)
|
||||
limits.RegisterEventMappers(es)
|
||||
restrictions.RegisterEventMappers(es)
|
||||
feature.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
|
81
internal/command/restrictions.go
Normal file
81
internal/command/restrictions.go
Normal file
@ -0,0 +1,81 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
)
|
||||
|
||||
type SetRestrictions struct {
|
||||
DisallowPublicOrgRegistration *bool
|
||||
}
|
||||
|
||||
// SetRestrictions creates new restrictions or updates existing restrictions.
|
||||
func (c *Commands) SetInstanceRestrictions(
|
||||
ctx context.Context,
|
||||
setRestrictions *SetRestrictions,
|
||||
) (*domain.ObjectDetails, error) {
|
||||
instanceId := authz.GetInstance(ctx).InstanceID()
|
||||
wm, err := c.getRestrictionsWriteModel(ctx, instanceId, instanceId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregateId := wm.AggregateID
|
||||
if aggregateId == "" {
|
||||
aggregateId, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setCmd, err := c.SetRestrictionsCommand(restrictions.NewAggregate(aggregateId, instanceId, instanceId), wm, setRestrictions)()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := setCmd(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) > 0 {
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(wm, events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getRestrictionsWriteModel(ctx context.Context, instanceId, resourceOwner string) (*restrictionsWriteModel, error) {
|
||||
wm := newRestrictionsWriteModel(instanceId, resourceOwner)
|
||||
return wm, c.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
}
|
||||
|
||||
func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if setRestrictions == nil || setRestrictions.DisallowPublicOrgRegistration == nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified")
|
||||
}
|
||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
changes := wm.NewChanges(setRestrictions)
|
||||
if len(changes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []eventstore.Command{restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
&a.Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
changes...,
|
||||
)}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
55
internal/command/restrictions_model.go
Normal file
55
internal/command/restrictions_model.go
Normal file
@ -0,0 +1,55 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
)
|
||||
|
||||
type restrictionsWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
disallowPublicOrgRegistrations bool
|
||||
}
|
||||
|
||||
// newRestrictionsWriteModel aggregateId is filled by reducing unit matching events
|
||||
func newRestrictionsWriteModel(instanceId, resourceOwner string) *restrictionsWriteModel {
|
||||
return &restrictionsWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
InstanceID: instanceId,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *restrictionsWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
InstanceID(wm.InstanceID).
|
||||
AddQuery().
|
||||
AggregateTypes(restrictions.AggregateType).
|
||||
EventTypes(restrictions.SetEventType)
|
||||
|
||||
return query.Builder()
|
||||
}
|
||||
|
||||
func (wm *restrictionsWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
wm.ChangeDate = event.CreatedAt()
|
||||
if e, ok := event.(*restrictions.SetEvent); ok && e.DisallowPublicOrgRegistrations != nil {
|
||||
wm.disallowPublicOrgRegistrations = *e.DisallowPublicOrgRegistrations
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
// NewChanges returns all changes that need to be applied to the aggregate.
|
||||
// nil properties in setRestrictions are ignored
|
||||
func (wm *restrictionsWriteModel) NewChanges(setRestrictions *SetRestrictions) (changes []restrictions.RestrictionsChange) {
|
||||
if setRestrictions == nil {
|
||||
return nil
|
||||
}
|
||||
changes = make([]restrictions.RestrictionsChange, 0, 1)
|
||||
if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistrations != *setRestrictions.DisallowPublicOrgRegistration) {
|
||||
changes = append(changes, restrictions.ChangePublicOrgRegistrations(*setRestrictions.DisallowPublicOrgRegistration))
|
||||
}
|
||||
return changes
|
||||
}
|
189
internal/command/restrictions_test.go
Normal file
189
internal/command/restrictions_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
)
|
||||
|
||||
func TestSetRestrictions(t *testing.T) {
|
||||
type fields func(*testing.T) (*eventstore.Eventstore, id.Generator)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
setRestrictions *SetRestrictions
|
||||
}
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "set new restrictions",
|
||||
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||
return eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
restrictions.ChangePublicOrgRegistrations(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
id_mock.NewIDGeneratorExpectIDs(t, "restrictions1")
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
setRestrictions: &SetRestrictions{
|
||||
DisallowPublicOrgRegistration: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change restrictions",
|
||||
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||
return eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
restrictions.ChangePublicOrgRegistrations(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
restrictions.ChangePublicOrgRegistrations(false),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
nil
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
setRestrictions: &SetRestrictions{
|
||||
DisallowPublicOrgRegistration: gu.Ptr(false),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set restrictions idempotency",
|
||||
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||
return eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
restrictions.ChangePublicOrgRegistrations(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
nil
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
setRestrictions: &SetRestrictions{
|
||||
DisallowPublicOrgRegistration: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no restrictions defined",
|
||||
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
|
||||
return eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
restrictions.NewSetEvent(
|
||||
eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
|
||||
restrictions.SetEventType,
|
||||
),
|
||||
restrictions.ChangePublicOrgRegistrations(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
), nil
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
setRestrictions: &SetRestrictions{},
|
||||
},
|
||||
res: res{
|
||||
err: zitadel_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := new(Commands)
|
||||
r.eventstore, r.idGenerator = tt.fields(t)
|
||||
got, err := r.SetInstanceRestrictions(tt.args.ctx, tt.args.setRestrictions)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.res.err != nil && !tt.res.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
if tt.res.err == nil {
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -119,7 +119,7 @@ func NewUpsertStatement(event eventstore.Event, conflictCols []Column, values []
|
||||
config.err = ErrNoValues
|
||||
}
|
||||
|
||||
updateCols, updateVals := getUpdateCols(cols, conflictTarget)
|
||||
updateCols, updateVals := getUpdateCols(values, conflictTarget)
|
||||
if len(updateCols) == 0 || len(updateVals) == 0 {
|
||||
config.err = ErrNoValues
|
||||
}
|
||||
@ -141,17 +141,38 @@ func NewUpsertStatement(event eventstore.Event, conflictCols []Column, values []
|
||||
return NewStatement(event, exec(config, q, opts))
|
||||
}
|
||||
|
||||
func getUpdateCols(cols, conflictTarget []string) (updateCols, updateVals []string) {
|
||||
var _ ValueContainer = (*onlySetValueOnInsert)(nil)
|
||||
|
||||
type onlySetValueOnInsert struct {
|
||||
Table string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (c *onlySetValueOnInsert) GetValue() interface{} {
|
||||
return c.Value
|
||||
}
|
||||
|
||||
func OnlySetValueOnInsert(table string, value interface{}) *onlySetValueOnInsert {
|
||||
return &onlySetValueOnInsert{
|
||||
Table: table,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func getUpdateCols(cols []Column, conflictTarget []string) (updateCols, updateVals []string) {
|
||||
updateCols = make([]string, len(cols))
|
||||
updateVals = make([]string, len(cols))
|
||||
|
||||
copy(updateCols, cols)
|
||||
|
||||
for i := len(updateCols) - 1; i >= 0; i-- {
|
||||
updateVals[i] = "EXCLUDED." + updateCols[i]
|
||||
|
||||
for i := len(cols) - 1; i >= 0; i-- {
|
||||
col := cols[i]
|
||||
table := "EXCLUDED"
|
||||
if onlyOnInsert, ok := col.Value.(*onlySetValueOnInsert); ok {
|
||||
table = onlyOnInsert.Table
|
||||
}
|
||||
updateCols[i] = col.Name
|
||||
updateVals[i] = table + "." + col.Name
|
||||
for _, conflict := range conflictTarget {
|
||||
if conflict == updateCols[i] {
|
||||
if conflict == col.Name {
|
||||
copy(updateCols[i:], updateCols[i+1:])
|
||||
updateCols[len(updateCols)-1] = ""
|
||||
updateCols = updateCols[:len(updateCols)-1]
|
||||
@ -383,6 +404,10 @@ func NewCopyStatement(event eventstore.Event, conflictCols, from, to []Column, n
|
||||
return NewStatement(event, exec(config, q, opts))
|
||||
}
|
||||
|
||||
type ValueContainer interface {
|
||||
GetValue() interface{}
|
||||
}
|
||||
|
||||
func columnsToQuery(cols []Column) (names []string, parameters []string, values []interface{}) {
|
||||
names = make([]string, len(cols))
|
||||
values = make([]interface{}, len(cols))
|
||||
@ -390,10 +415,13 @@ func columnsToQuery(cols []Column) (names []string, parameters []string, values
|
||||
var parameterIndex int
|
||||
for i, col := range cols {
|
||||
names[i] = col.Name
|
||||
if c, ok := col.Value.(Column); ok {
|
||||
switch c := col.Value.(type) {
|
||||
case Column:
|
||||
parameters[i] = c.Name
|
||||
continue
|
||||
} else {
|
||||
case ValueContainer:
|
||||
values[parameterIndex] = c.GetValue()
|
||||
default:
|
||||
values[parameterIndex] = col.Value
|
||||
}
|
||||
parameters[i] = "$" + strconv.Itoa(parameterIndex+1)
|
||||
|
@ -358,7 +358,7 @@ func TestNewUpsertStatement(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "correct UPDATE single col",
|
||||
name: "correct *onlySetValueOnInsert",
|
||||
args: args{
|
||||
table: "my_table",
|
||||
event: &testEvent{
|
||||
@ -372,11 +372,18 @@ func TestNewUpsertStatement(t *testing.T) {
|
||||
values: []Column{
|
||||
{
|
||||
Name: "col1",
|
||||
Value: "val",
|
||||
Value: "val1",
|
||||
},
|
||||
{
|
||||
Name: "col2",
|
||||
Value: "val",
|
||||
Value: "val2",
|
||||
},
|
||||
{
|
||||
Name: "col3",
|
||||
Value: &onlySetValueOnInsert{
|
||||
Table: "some.table",
|
||||
Value: "val3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -388,8 +395,53 @@ func TestNewUpsertStatement(t *testing.T) {
|
||||
executer: &wantExecuter{
|
||||
params: []params{
|
||||
{
|
||||
query: "INSERT INTO my_table (col1, col2) VALUES ($1, $2) ON CONFLICT (col1) DO UPDATE SET col2 = EXCLUDED.col2",
|
||||
args: []interface{}{"val", "val"},
|
||||
query: "INSERT INTO my_table (col1, col2, col3) VALUES ($1, $2, $3) ON CONFLICT (col1) DO UPDATE SET (col2, col3) = (EXCLUDED.col2, some.table.col3)",
|
||||
args: []interface{}{"val1", "val2", "val3"},
|
||||
},
|
||||
},
|
||||
shouldExecute: true,
|
||||
},
|
||||
isErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "correct all *onlySetValueOnInsert",
|
||||
args: args{
|
||||
table: "my_table",
|
||||
event: &testEvent{
|
||||
aggregateType: "agg",
|
||||
sequence: 1,
|
||||
previousSequence: 0,
|
||||
},
|
||||
conflictCols: []Column{
|
||||
NewCol("col1", nil),
|
||||
},
|
||||
values: []Column{
|
||||
{
|
||||
Name: "col1",
|
||||
Value: "val1",
|
||||
},
|
||||
{
|
||||
Name: "col2",
|
||||
Value: &onlySetValueOnInsert{
|
||||
Table: "some.table",
|
||||
Value: "val2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
table: "my_table",
|
||||
aggregateType: "agg",
|
||||
sequence: 1,
|
||||
previousSequence: 1,
|
||||
executer: &wantExecuter{
|
||||
params: []params{
|
||||
{
|
||||
query: "INSERT INTO my_table (col1, col2) VALUES ($1, $2) ON CONFLICT (col1) DO UPDATE SET col2 = some.table.col2",
|
||||
args: []interface{}{"val1", "val2"},
|
||||
},
|
||||
},
|
||||
shouldExecute: true,
|
||||
|
@ -59,7 +59,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
}
|
||||
|
||||
func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
|
||||
primaryDomain = randString(5) + ".integration"
|
||||
primaryDomain = randString(5) + ".integration.localhost"
|
||||
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
|
||||
InstanceName: "testinstance",
|
||||
CustomDomain: primaryDomain,
|
||||
@ -74,7 +74,7 @@ func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (pr
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.createClientConn(iamOwnerCtx, grpc.WithAuthority(primaryDomain))
|
||||
t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port))
|
||||
instanceId = instance.GetInstanceId()
|
||||
t.Users.Set(instanceId, IAMOwner, &User{
|
||||
Token: instance.GetPat(),
|
||||
|
@ -127,12 +127,11 @@ func (s *Tester) Host() string {
|
||||
return fmt.Sprintf("%s:%d", s.Config.ExternalDomain, s.Config.Port)
|
||||
}
|
||||
|
||||
func (s *Tester) createClientConn(ctx context.Context, opts ...grpc.DialOption) {
|
||||
target := s.Host()
|
||||
cc, err := grpc.DialContext(ctx, target, append(opts,
|
||||
func (s *Tester) createClientConn(ctx context.Context, target string) {
|
||||
cc, err := grpc.DialContext(ctx, target,
|
||||
grpc.WithBlock(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)...)
|
||||
)
|
||||
if err != nil {
|
||||
s.Shutdown <- os.Interrupt
|
||||
s.wg.Wait()
|
||||
@ -346,9 +345,10 @@ func NewTester(ctx context.Context) *Tester {
|
||||
case <-ctx.Done():
|
||||
logging.OnError(ctx.Err()).Fatal("waiting for integration tester server")
|
||||
}
|
||||
tester.createClientConn(ctx)
|
||||
host := tester.Host()
|
||||
tester.createClientConn(ctx, host)
|
||||
tester.createLoginClient(ctx)
|
||||
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(tester.Host(), tester.Config.ExternalSecure))
|
||||
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(host, tester.Config.ExternalSecure))
|
||||
tester.createMachineUserOrgOwner(ctx)
|
||||
tester.createMachineUserInstanceOwner(ctx)
|
||||
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host())
|
||||
|
@ -1,61 +0,0 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/limits"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||
quota_repo "github.com/zitadel/zitadel/internal/repository/quota"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
)
|
||||
|
||||
type expect func(mockRepository *mock.MockRepository)
|
||||
|
||||
func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
||||
m := mock.NewRepo(t)
|
||||
for _, e := range expects {
|
||||
e(m)
|
||||
}
|
||||
es := eventstore.NewEventstore(
|
||||
&eventstore.Config{
|
||||
Querier: m.MockQuerier,
|
||||
Pusher: m.MockPusher,
|
||||
},
|
||||
)
|
||||
iam_repo.RegisterEventMappers(es)
|
||||
org.RegisterEventMappers(es)
|
||||
usr_repo.RegisterEventMappers(es)
|
||||
proj_repo.RegisterEventMappers(es)
|
||||
quota_repo.RegisterEventMappers(es)
|
||||
limits.RegisterEventMappers(es)
|
||||
usergrant.RegisterEventMappers(es)
|
||||
key_repo.RegisterEventMappers(es)
|
||||
action_repo.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
|
||||
func eventFromEventPusher(event eventstore.Command) *repository.Event {
|
||||
data, _ := eventstore.EventData(event)
|
||||
return &repository.Event{
|
||||
ID: "",
|
||||
Seq: 0,
|
||||
CreationDate: time.Time{},
|
||||
Typ: event.Type(),
|
||||
Data: data,
|
||||
EditorUser: event.Creator(),
|
||||
Version: event.Aggregate().Version,
|
||||
AggregateID: event.Aggregate().ID,
|
||||
AggregateType: event.Aggregate().Type,
|
||||
ResourceOwner: sql.NullString{String: event.Aggregate().ResourceOwner, Valid: event.Aggregate().ResourceOwner != ""},
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ var (
|
||||
MilestoneProjection *handler.Handler
|
||||
QuotaProjection *quotaProjection
|
||||
LimitsProjection *handler.Handler
|
||||
RestrictionsProjection *handler.Handler
|
||||
)
|
||||
|
||||
type projection interface {
|
||||
@ -143,6 +144,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
||||
MilestoneProjection = newMilestoneProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["milestones"]), systemUsers)
|
||||
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
|
||||
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
|
||||
RestrictionsProjection = newRestrictionsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["restrictions"]))
|
||||
newProjectionsList()
|
||||
return nil
|
||||
}
|
||||
@ -247,5 +249,6 @@ func newProjectionsList() {
|
||||
MilestoneProjection,
|
||||
QuotaProjection.handler,
|
||||
LimitsProjection,
|
||||
RestrictionsProjection,
|
||||
}
|
||||
}
|
||||
|
96
internal/query/projection/restrictions.go
Normal file
96
internal/query/projection/restrictions.go
Normal file
@ -0,0 +1,96 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
)
|
||||
|
||||
const (
|
||||
RestrictionsProjectionTable = "projections.restrictions"
|
||||
|
||||
RestrictionsColumnAggregateID = "aggregate_id"
|
||||
RestrictionsColumnCreationDate = "creation_date"
|
||||
RestrictionsColumnChangeDate = "change_date"
|
||||
RestrictionsColumnResourceOwner = "resource_owner"
|
||||
RestrictionsColumnInstanceID = "instance_id"
|
||||
RestrictionsColumnSequence = "sequence"
|
||||
|
||||
RestrictionsColumnDisallowPublicOrgRegistration = "disallow_public_org_registration"
|
||||
)
|
||||
|
||||
type restrictionsProjection struct{}
|
||||
|
||||
func newRestrictionsProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, &restrictionsProjection{})
|
||||
}
|
||||
|
||||
func (*restrictionsProjection) Name() string {
|
||||
return RestrictionsProjectionTable
|
||||
}
|
||||
|
||||
func (*restrictionsProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(
|
||||
handler.NewTable([]*handler.InitColumn{
|
||||
handler.NewColumn(RestrictionsColumnAggregateID, handler.ColumnTypeText),
|
||||
handler.NewColumn(RestrictionsColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(RestrictionsColumnChangeDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(RestrictionsColumnResourceOwner, handler.ColumnTypeText),
|
||||
handler.NewColumn(RestrictionsColumnInstanceID, handler.ColumnTypeText),
|
||||
handler.NewColumn(RestrictionsColumnSequence, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(RestrictionsColumnDisallowPublicOrgRegistration, handler.ColumnTypeBool),
|
||||
},
|
||||
handler.NewPrimaryKey(RestrictionsColumnInstanceID, RestrictionsColumnResourceOwner),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *restrictionsProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: restrictions.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: restrictions.SetEventType,
|
||||
Reduce: p.reduceRestrictionsSet,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: instance.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(RestrictionsColumnInstanceID),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *restrictionsProjection) reduceRestrictionsSet(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, err := assertEvent[*restrictions.SetEvent](event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conflictCols := []handler.Column{
|
||||
handler.NewCol(RestrictionsColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCol(RestrictionsColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||
}
|
||||
updateCols := []handler.Column{
|
||||
handler.NewCol(RestrictionsColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCol(RestrictionsColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||
handler.NewCol(RestrictionsColumnCreationDate, handler.OnlySetValueOnInsert(RestrictionsProjectionTable, e.CreationDate())),
|
||||
handler.NewCol(RestrictionsColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(RestrictionsColumnSequence, e.Sequence()),
|
||||
handler.NewCol(RestrictionsColumnAggregateID, e.Aggregate().ID),
|
||||
}
|
||||
if e.DisallowPublicOrgRegistrations != nil {
|
||||
updateCols = append(updateCols, handler.NewCol(RestrictionsColumnDisallowPublicOrgRegistration, *e.DisallowPublicOrgRegistrations))
|
||||
}
|
||||
return handler.NewUpsertStatement(e, conflictCols, updateCols), nil
|
||||
}
|
96
internal/query/projection/restrictions_test.go
Normal file
96
internal/query/projection/restrictions_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
)
|
||||
|
||||
func TestRestrictionsProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceRestrictionsSet should update defined",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
restrictions.SetEventType,
|
||||
restrictions.AggregateType,
|
||||
[]byte(`{ "disallowPublicOrgRegistrations": true }`),
|
||||
), restrictions.SetEventMapper),
|
||||
},
|
||||
reduce: (&restrictionsProjection{}).reduceRestrictionsSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("restrictions"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.restrictions (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) = (projections.restrictions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.disallow_public_org_registration)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRestrictionsSet shouldn't update undefined",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
restrictions.SetEventType,
|
||||
restrictions.AggregateType,
|
||||
[]byte(`{}`),
|
||||
), restrictions.SetEventMapper),
|
||||
},
|
||||
reduce: (&restrictionsProjection{}).reduceRestrictionsSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("restrictions"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.restrictions (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id) = (projections.restrictions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if !errors.IsErrorInvalidArgument(err) {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, RestrictionsProjectionTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/repository/restrictions"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
@ -113,6 +114,7 @@ func StartQueries(
|
||||
oidcsession.RegisterEventMappers(repo.eventstore)
|
||||
quota.RegisterEventMappers(repo.eventstore)
|
||||
limits.RegisterEventMappers(repo.eventstore)
|
||||
restrictions.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.checkPermission = permissionCheck(repo)
|
||||
|
||||
|
108
internal/query/restrictions.go
Normal file
108
internal/query/restrictions.go
Normal file
@ -0,0 +1,108 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
zitade_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
var (
|
||||
restrictionsTable = table{
|
||||
name: projection.RestrictionsProjectionTable,
|
||||
instanceIDCol: projection.RestrictionsColumnInstanceID,
|
||||
}
|
||||
RestrictionsColumnAggregateID = Column{
|
||||
name: projection.RestrictionsColumnAggregateID,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnCreationDate = Column{
|
||||
name: projection.RestrictionsColumnCreationDate,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnChangeDate = Column{
|
||||
name: projection.RestrictionsColumnChangeDate,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnResourceOwner = Column{
|
||||
name: projection.RestrictionsColumnResourceOwner,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnInstanceID = Column{
|
||||
name: projection.RestrictionsColumnInstanceID,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnSequence = Column{
|
||||
name: projection.RestrictionsColumnSequence,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
RestrictionsColumnDisallowPublicOrgRegistrations = Column{
|
||||
name: projection.RestrictionsColumnDisallowPublicOrgRegistration,
|
||||
table: restrictionsTable,
|
||||
}
|
||||
)
|
||||
|
||||
type Restrictions struct {
|
||||
AggregateID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
|
||||
DisallowPublicOrgRegistration bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Restrictions, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareRestrictionsQuery(ctx, q.client)
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
RestrictionsColumnInstanceID.identifier(): instanceID,
|
||||
RestrictionsColumnResourceOwner.identifier(): instanceID,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatment")
|
||||
}
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
restrictions, err = scan(row)
|
||||
return err
|
||||
}, query, args...)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// not found is not an error
|
||||
err = nil
|
||||
}
|
||||
return restrictions, err
|
||||
}
|
||||
|
||||
func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (Restrictions, error)) {
|
||||
return sq.Select(
|
||||
RestrictionsColumnAggregateID.identifier(),
|
||||
RestrictionsColumnCreationDate.identifier(),
|
||||
RestrictionsColumnChangeDate.identifier(),
|
||||
RestrictionsColumnResourceOwner.identifier(),
|
||||
RestrictionsColumnSequence.identifier(),
|
||||
RestrictionsColumnDisallowPublicOrgRegistrations.identifier(),
|
||||
).
|
||||
From(restrictionsTable.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (restrictions Restrictions, err error) {
|
||||
return restrictions, row.Scan(
|
||||
&restrictions.AggregateID,
|
||||
&restrictions.CreationDate,
|
||||
&restrictions.ChangeDate,
|
||||
&restrictions.ResourceOwner,
|
||||
&restrictions.Sequence,
|
||||
&restrictions.DisallowPublicOrgRegistration,
|
||||
)
|
||||
}
|
||||
}
|
111
internal/query/restrictions_test.go
Normal file
111
internal/query/restrictions_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
expectedRestrictionsQuery = regexp.QuoteMeta("SELECT projections.restrictions.aggregate_id," +
|
||||
" projections.restrictions.creation_date," +
|
||||
" projections.restrictions.change_date," +
|
||||
" projections.restrictions.resource_owner," +
|
||||
" projections.restrictions.sequence," +
|
||||
" projections.restrictions.disallow_public_org_registration" +
|
||||
" FROM projections.restrictions" +
|
||||
" AS OF SYSTEM TIME '-1 ms'",
|
||||
)
|
||||
|
||||
restrictionsCols = []string{
|
||||
"aggregate_id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"resource_owner",
|
||||
"sequence",
|
||||
"disallow_public_org_registration",
|
||||
}
|
||||
)
|
||||
|
||||
func Test_RestrictionsPrepare(t *testing.T) {
|
||||
type want struct {
|
||||
sqlExpectations sqlExpectation
|
||||
err checkErr
|
||||
object interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare interface{}
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "prepareRestrictionsQuery no result",
|
||||
prepare: prepareRestrictionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryScanErr(
|
||||
expectedRestrictionsQuery,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("err should be sql.ErrNoRows got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
object: Restrictions{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareRestrictionsQuery",
|
||||
prepare: prepareRestrictionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQuery(
|
||||
expectedRestrictionsQuery,
|
||||
restrictionsCols,
|
||||
[]driver.Value{
|
||||
"restrictions1",
|
||||
testNow,
|
||||
testNow,
|
||||
"instance1",
|
||||
0,
|
||||
true,
|
||||
},
|
||||
),
|
||||
object: Restrictions{
|
||||
AggregateID: "restrictions1",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
ResourceOwner: "instance1",
|
||||
Sequence: 0,
|
||||
DisallowPublicOrgRegistration: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareRestrictionsQuery sql err",
|
||||
prepare: prepareRestrictionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
expectedRestrictionsQuery,
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
object: (*Restrictions)(nil),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertPrepare(t, tt.prepare, tt.want.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
|
||||
})
|
||||
}
|
||||
}
|
26
internal/repository/restrictions/aggregate.go
Normal file
26
internal/repository/restrictions/aggregate.go
Normal file
@ -0,0 +1,26 @@
|
||||
package restrictions
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "restrictions"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, instanceId, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
InstanceID: instanceId,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
53
internal/repository/restrictions/events.go
Normal file
53
internal/repository/restrictions/events.go
Normal file
@ -0,0 +1,53 @@
|
||||
package restrictions
|
||||
|
||||
import (
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
eventTypePrefix = eventstore.EventType("restrictions.")
|
||||
SetEventType = eventTypePrefix + "set"
|
||||
)
|
||||
|
||||
// SetEvent describes that restrictions are added or modified and contains only changed properties
|
||||
type SetEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
DisallowPublicOrgRegistrations *bool `json:"disallowPublicOrgRegistrations,omitempty"`
|
||||
}
|
||||
|
||||
func (e *SetEvent) Payload() any {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SetEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = b
|
||||
}
|
||||
|
||||
func NewSetEvent(
|
||||
base *eventstore.BaseEvent,
|
||||
changes ...RestrictionsChange,
|
||||
) *SetEvent {
|
||||
changedEvent := &SetEvent{
|
||||
BaseEvent: base,
|
||||
}
|
||||
for _, change := range changes {
|
||||
change(changedEvent)
|
||||
}
|
||||
return changedEvent
|
||||
}
|
||||
|
||||
type RestrictionsChange func(*SetEvent)
|
||||
|
||||
func ChangePublicOrgRegistrations(disallow bool) RestrictionsChange {
|
||||
return func(e *SetEvent) {
|
||||
e.DisallowPublicOrgRegistrations = gu.Ptr(disallow)
|
||||
}
|
||||
}
|
||||
|
||||
var SetEventMapper = eventstore.GenericEventMapper[SetEvent]
|
9
internal/repository/restrictions/eventstore.go
Normal file
9
internal/repository/restrictions/eventstore.go
Normal file
@ -0,0 +1,9 @@
|
||||
package restrictions
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
es.RegisterFilterEventMapper(AggregateType, SetEventType, SetEventMapper)
|
||||
}
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Лимитът не е намерен
|
||||
NoneSpecified: Не са посочени лимити
|
||||
Restrictions:
|
||||
NoneSpecified: Не са посочени ограничения
|
||||
Language:
|
||||
NotParsed: Езикът не можа да бъде анализиран синтактично
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limity nebyly nalezeny
|
||||
NoneSpecified: Nebyly určeny žádné limity
|
||||
Restrictions:
|
||||
NoneSpecified: Nebyla určena žádná omezení
|
||||
Language:
|
||||
NotParsed: Jazyk nelze určit
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limits konnten nicht gefunden werden
|
||||
NoneSpecified: Keine Limits angegeben
|
||||
Restrictions:
|
||||
NoneSpecified: Keine Restriktionen angegeben
|
||||
Language:
|
||||
NotParsed: Sprache konnte nicht gemapped werden
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limits not found
|
||||
NoneSpecified: No limits specified
|
||||
Restrictions:
|
||||
NoneSpecified: No restrictions specified
|
||||
Language:
|
||||
NotParsed: Could not parse language
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Límite no encontrado
|
||||
NoneSpecified: No se especificaron límites
|
||||
Restrictions:
|
||||
NoneSpecified: No se especificaron restricciones
|
||||
Language:
|
||||
NotParsed: No pude analizar el idioma
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limites non trouvée
|
||||
NoneSpecified: Aucune limite spécifiée
|
||||
Restrictions:
|
||||
NoneSpecified: Aucune restriction spécifiée
|
||||
Language:
|
||||
NotParsed: Impossible d'analyser la langue
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limite non trovato
|
||||
NoneSpecified: Nessun limite specificato
|
||||
Restrictions:
|
||||
NoneSpecified: Nessuna restrizione specificata
|
||||
Language:
|
||||
NotParsed: Impossibile analizzare la lingua
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: 制限が見つかりません
|
||||
NoneSpecified: 制限が指定されていません
|
||||
Restrictions:
|
||||
NoneSpecified: 制限が指定されていません
|
||||
Language:
|
||||
NotParsed: 言語のパースに失敗しました
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Лимитот не е пронајден
|
||||
NoneSpecified: Не се наведени лимити
|
||||
Restrictions:
|
||||
NoneSpecified: Не се наведени ограничувања
|
||||
Language:
|
||||
NotParsed: Јазикот не може да се парсира
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limit nie znaleziony
|
||||
NoneSpecified: Nie określono limitów
|
||||
Restrictions:
|
||||
NoneSpecified: Nie określono ograniczeń
|
||||
Language:
|
||||
NotParsed: Nie można przeanalizować języka
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: Limite não encontrado
|
||||
NoneSpecified: Nenhum limite especificado
|
||||
Restrictions:
|
||||
NoneSpecified: Nenhuma restrição especificada
|
||||
Language:
|
||||
NotParsed: Não foi possível analisar o idioma
|
||||
OIDCSettings:
|
||||
|
@ -28,6 +28,11 @@ Errors:
|
||||
RemoveFailed: Объект не удалось удалить
|
||||
Limit:
|
||||
ExceedsDefault: Лимит превышает лимит по умолчанию
|
||||
Limits:
|
||||
NotFound: Лимиты не найдены
|
||||
NoneSpecified: Не указаны лимиты
|
||||
Restrictions:
|
||||
NoneSpecified: Не указаны ограничения
|
||||
Language:
|
||||
NotParsed: Не удалось разобрать язык
|
||||
OIDCSettings:
|
||||
|
@ -31,6 +31,8 @@ Errors:
|
||||
Limits:
|
||||
NotFound: 未找到限制
|
||||
NoneSpecified: 未指定限制
|
||||
Restrictions:
|
||||
NoneSpecified: 未指定限制
|
||||
Language:
|
||||
NotParsed: 无法解析语言
|
||||
OIDCSettings:
|
||||
|
@ -3795,6 +3795,59 @@ service AdminService {
|
||||
description: "Returns a list of reached instance usage milestones."
|
||||
};
|
||||
}
|
||||
|
||||
// Sets restrictions
|
||||
rpc SetRestrictions(SetRestrictionsRequest) returns (SetRestrictionsResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/restrictions"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.restrictions.write";
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: ["Feature Restrictions"];
|
||||
summary: "Restrict the instances features";
|
||||
description: "Undefined values don't change the current restriction. Zero values remove the current restriction.";
|
||||
responses: {
|
||||
key: "200";
|
||||
value: {
|
||||
description: "Restrictions set.";
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
key: "400";
|
||||
value: {
|
||||
description: "No restriction is defined.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Gets restrictions
|
||||
rpc GetRestrictions(GetRestrictionsRequest) returns (GetRestrictionsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/restrictions"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "iam.restrictions.read";
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: ["Feature Restrictions"];
|
||||
summary: "Get the current feature restrictions for the instance";
|
||||
description: "Undefined values mean that the feature is not restricted. If restrictions were never set, the instances features are not restricted, all properties are undefined and the details object is empty.";
|
||||
responses: {
|
||||
key: "200";
|
||||
value: {
|
||||
description: "The status 200 is also returned if no restrictions were ever set. In this case, all feature restrictions have zero values.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7934,3 +7987,27 @@ message ListMilestonesResponse {
|
||||
zitadel.v1.ListDetails details = 1;
|
||||
repeated zitadel.milestone.v1.Milestone result = 2;
|
||||
}
|
||||
|
||||
message SetRestrictionsRequest {
|
||||
optional bool disallow_public_org_registration = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetRestrictionsResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message GetRestrictionsRequest {}
|
||||
|
||||
message GetRestrictionsResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
bool disallow_public_org_registration = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -433,7 +433,7 @@ service SystemService {
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated";
|
||||
permission: "system.limits.write";
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
@ -465,7 +465,7 @@ service SystemService {
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated";
|
||||
permission: "system.limits.delete";
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
@ -766,7 +766,7 @@ message SetLimitsRequest {
|
||||
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
google.protobuf.Duration audit_log_retention = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "AuditLogRetention limits the number of events that can be queried via the events API by their age. A value of '0s' means that all events are available. If this value is set, it overwrites the system default.";
|
||||
description: "auditLogRetention limits the number of events that can be queried via the events API by their age. A value of '0s' means that all events are available. If this value is set, it overwrites the system default.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user