mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:17:35 +00:00
feat: add SYSTEM_OWNER role (#6765)
* define roles and permissions * support system user memberships * don't limit system users * cleanup permissions * restrict memberships to aggregates * default to SYSTEM_OWNER * update unit tests * test: system user token test (#6778) * update unit tests * refactor: make authz testable * move session constants * cleanup * comment * comment * decode member type string to enum (#6780) * decode member type string to enum * handle all membership types * decode enums where necessary * decode member type in steps config * update system api docs * add technical advisory * tweak docs a bit * comment in comment * lint * extract token from Bearer header prefix * review changes * fix tests * fix: add fix for activityhandler * add isSystemUser * remove IsSystemUser from activity info * fix: add fix for activityhandler --------- Co-authored-by: Stefan Benz <stefan@caos.ch>
This commit is contained in:
@@ -389,11 +389,27 @@ EncryptionKeys:
|
||||
UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID
|
||||
|
||||
SystemAPIUsers:
|
||||
# Add keys for authentication of the systemAPI here:
|
||||
# you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT:
|
||||
# # Add keys for authentication of the systemAPI here:
|
||||
# # you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT:
|
||||
# - superuser:
|
||||
# Path: /path/to/superuser/key.pem # you can provide the key either by reference with the path
|
||||
# Path: /path/to/superuser/ey.pem # you can provide the key either by reference with the path
|
||||
# Memberships:
|
||||
# # MemberType System allows the user to access all APIs for all instances or organizations
|
||||
# - MemberType: System
|
||||
# Roles:
|
||||
# - "SYSTEM_OWNER"
|
||||
# # Actually, we don't recommend adding IAM_OWNER and ORG_OWNER to the System membership, as this basically enables god mode for the system user
|
||||
# - "IAM_OWNER"
|
||||
# - "ORG_OWNER"
|
||||
# # MemberType IAM and Organization let you restrict access to a specific instance or organization by specifying the AggregateID
|
||||
# - MemberType: IAM
|
||||
# Roles: "IAM_OWNER"
|
||||
# AggregateID: "123456789012345678"
|
||||
# - MemberType: Organization
|
||||
# Roles: "ORG_OWNER"
|
||||
# AggregateID: "123456789012345678"
|
||||
# - superuser2:
|
||||
# # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER"
|
||||
# KeyData: <base64 encoded key> # or you can directly embed it as base64 encoded value
|
||||
|
||||
#TODO: remove as soon as possible
|
||||
@@ -841,6 +857,29 @@ AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION
|
||||
|
||||
InternalAuthZ:
|
||||
RolePermissionMappings:
|
||||
- Role: "SYSTEM_OWNER"
|
||||
Permissions:
|
||||
- "system.instance.read"
|
||||
- "system.instance.write"
|
||||
- "system.instance.delete"
|
||||
- "system.domain.read"
|
||||
- "system.domain.write"
|
||||
- "system.domain.delete"
|
||||
- "system.debug.read"
|
||||
- "system.debug.write"
|
||||
- "system.debug.delete"
|
||||
- "system.feature.write"
|
||||
- "system.limits.write"
|
||||
- "system.limits.delete"
|
||||
- "system.quota.write"
|
||||
- "system.quota.delete"
|
||||
- "system.iam.member.read"
|
||||
- Role: "SYSTEM_OWNER_VIEWER"
|
||||
Permissions:
|
||||
- "system.instance.read"
|
||||
- "system.domain.read"
|
||||
- "system.debug.read"
|
||||
- "system.iam.member.read"
|
||||
- Role: "IAM_OWNER"
|
||||
Permissions:
|
||||
- "iam.read"
|
||||
|
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/config/hook"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -23,7 +25,8 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
hook.StringToFeatureHookFunc(),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read default config")
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
@@ -45,7 +46,8 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
database.DecodeHook,
|
||||
hook.StringToFeatureHookFunc(),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(authz.MemberTypeString),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read default config")
|
||||
@@ -101,7 +103,7 @@ func MustNewSteps(v *viper.Viper) *Steps {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
hook.StringToFeatureHookFunc(),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read steps")
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
@@ -92,7 +93,8 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
database.DecodeHook,
|
||||
actions.HTTPConfigDecodeHook,
|
||||
systemAPIUsersDecodeHook,
|
||||
hook.StringToFeatureHookFunc(),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read config")
|
||||
|
84
cmd/start/config_test.go
Normal file
84
cmd/start/config_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func TestMustNewConfig(t *testing.T) {
|
||||
type args struct {
|
||||
yaml string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Config
|
||||
}{{
|
||||
name: "features ok",
|
||||
args: args{yaml: `
|
||||
DefaultInstance:
|
||||
Features:
|
||||
- FeatureLoginDefaultOrg: true
|
||||
`},
|
||||
want: &Config{
|
||||
DefaultInstance: command.InstanceSetup{
|
||||
Features: map[domain.Feature]any{
|
||||
domain.FeatureLoginDefaultOrg: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "membership types ok",
|
||||
args: args{yaml: `
|
||||
SystemAPIUsers:
|
||||
- superuser:
|
||||
Memberships:
|
||||
- MemberType: System
|
||||
- MemberType: Organization
|
||||
- MemberType: IAM
|
||||
`},
|
||||
want: &Config{
|
||||
SystemAPIUsers: map[string]*authz.SystemAPIUser{
|
||||
"superuser": {
|
||||
Memberships: authz.Memberships{{
|
||||
MemberType: authz.MemberTypeSystem,
|
||||
}, {
|
||||
MemberType: authz.MemberTypeOrganization,
|
||||
}, {
|
||||
MemberType: authz.MemberTypeIAM,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
err := v.ReadConfig(strings.NewReader(`Log:
|
||||
Level: info
|
||||
Actions:
|
||||
HTTP:
|
||||
DenyList: []
|
||||
` + tt.args.yaml))
|
||||
require.NoError(t, err)
|
||||
tt.want.Log = &logging.Config{Level: "info"}
|
||||
tt.want.Actions = &actions.Config{HTTP: actions.HTTPConfig{DenyList: []actions.AddressChecker{}}}
|
||||
require.NoError(t, tt.want.Log.SetLogger())
|
||||
got := MustNewConfig(v)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MustNewConfig() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -320,8 +320,13 @@ func startAPIs(
|
||||
// always set the origin in the context if available in the http headers, no matter for what protocol
|
||||
router.Use(middleware.OriginHandler)
|
||||
// adds used HTTPPathPattern and RequestMethod to context
|
||||
router.Use(middleware.ActivityHandler(append(oidcPrefixes, saml.HandlerPrefix, admin.GatewayPathPrefix(), management.GatewayPathPrefix())))
|
||||
verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
|
||||
router.Use(middleware.ActivityHandler)
|
||||
systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessTokenVerifer := internal_authz.StartAccessTokenVerifierFromRepo(repo)
|
||||
verifier := internal_authz.StartAPITokenVerifier(repo, accessTokenVerifer, systemTokenVerifier)
|
||||
tlsConfig, err := config.TLS.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
|
Reference in New Issue
Block a user