diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 55263bbe2c..9653446cc3 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1172,6 +1172,34 @@ DefaultInstance: # If an audit log retention is set using an instance limit, it will overwrite the system default. AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION +SystemAuthZ: + # Configure the RolePermissionMappings by environment variable using JSON notation: + # ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]' + # Beware that if you configure the RolePermissionMappings by environment variable, all the default RolePermissionMappings are lost. + # + # Warning: RolePermissionMappings are synhronized to the database. + # Changes here will only be applied after running `zitadel setup` or `zitadel start-from-setup`. + 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.read" + - "system.feature.write" + - "system.feature.delete" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + InternalAuthZ: # Configure the RolePermissionMappings by environment variable using JSON notation: # ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]' diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index c15747e74a..e347a7b9f6 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -147,7 +147,7 @@ func projections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, nil, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, @@ -184,7 +184,7 @@ func projections( keys.Target, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, nil, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) }, sessionTokenVerifier, config.OIDC.DefaultAccessTokenLifetime, diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 24f6dd732e..86c46b77d3 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -410,7 +410,7 @@ func startCommandsQueries( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, nil, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, // not needed for projections @@ -435,7 +435,7 @@ func startCommandsQueries( authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure) logging.OnError(err).Fatal("unable to start authz repo") permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, nil, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } commands, err := command.StartCommands(ctx, diff --git a/cmd/start/config.go b/cmd/start/config.go index 910759b653..380cc91029 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -1,6 +1,7 @@ package start import ( + "fmt" "time" "github.com/mitchellh/mapstructure" @@ -11,7 +12,7 @@ import ( "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/saml" @@ -65,12 +66,13 @@ type Config struct { Login login.Config Console console.Config AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config SystemDefaults systemdefaults.SystemDefaults EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser CustomerPortal string Machine *id.Config Actions *actions.Config @@ -94,12 +96,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, database.DecodeHook, actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hooks.MapTypeStringDecode[domain.Feature, any], hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), @@ -129,6 +131,7 @@ func MustNewConfig(v *viper.Viper) *Config { // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings + fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> config.SystemAuthZ = %+v\n", config.SystemAuthZ) return config } diff --git a/cmd/start/start.go b/cmd/start/start.go index b12dde2182..7bca8b46d1 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -174,6 +174,12 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("unable to start caches: %w", err) } + systemPermissions := make([]string, 0) + // for _, roleMapping := range config.SystemAuthZ.RolePermissionMappings { + // systemPermissions = append(systemPermissions, roleMapping.Permissions...) + // } + // config.InternalAuthZ.SystemUserPermissions = systemPermissions + queries, err := query.StartQueries( ctx, eventstoreClient, @@ -192,7 +198,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, systemPermissions, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, config.AuditLogRetention, @@ -208,7 +214,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("error starting authz repo: %w", err) } permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, systemPermissions, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } storage, err := config.AssetStorage.NewStorage(dbClient.DB) @@ -407,7 +413,8 @@ func startAPIs( http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) + + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.SystemAuthZ, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -600,7 +607,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls go func() { logging.Infof("server is listening on %s", lis.Addr().String()) if tlsConfig != nil { - //we don't need to pass the files here, because we already initialized the TLS config on the server + // we don't need to pass the files here, because we already initialized the TLS config on the server errCh <- http1Server.ServeTLS(lis, "", "") } else { errCh <- http1Server.Serve(lis) diff --git a/internal/api/api.go b/internal/api/api.go index 15d6c5b996..62d3e14b35 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,7 +15,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" http_util "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -29,7 +29,7 @@ import ( type API struct { port uint16 grpcServer *grpc.Server - verifier internal_authz.APITokenVerifier + verifier authz.APITokenVerifier health healthCheck router *mux.Router hostHeaders []string @@ -72,8 +72,9 @@ func New( port uint16, router *mux.Router, queries *query.Queries, - verifier internal_authz.APITokenVerifier, - authZ internal_authz.Config, + verifier authz.APITokenVerifier, + systemAuthz authz.Config, + authZ authz.Config, tlsConfig *tls.Config, externalDomain string, hostHeaders []string, @@ -89,7 +90,7 @@ func New( hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 5455ad97db..d1a13e3116 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -20,7 +20,7 @@ const ( // - the organisation (**either** provided by ID or verified domain) exists // - the user is permitted to call the requested endpoint (permission option in proto) // it will pass the [CtxData] and permission of the user into the ctx [context.Context] -func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { +func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, SystemAuthConfig Config, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,12 +31,12 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, if requiredAuthOption.Permission == authenticated { return func(parent context.Context) context.Context { - parent = addGetSystemUserRolesFuncToCtx(parent, ctxData) + parent = addGetSystemUserRolesFuncToCtx(parent, SystemAuthConfig.RolePermissionMappings, ctxData) return context.WithValue(parent, dataKey, ctxData) }, nil } - requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) + requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, SystemAuthConfig.RolePermissionMappings, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, parent = context.WithValue(parent, dataKey, ctxData) parent = context.WithValue(parent, allPermissionsKey, allPermissions) parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions) - parent = addGetSystemUserRolesFuncToCtx(parent, ctxData) + parent = addGetSystemUserRolesFuncToCtx(parent, SystemAuthConfig.RolePermissionMappings, ctxData) return parent }, nil } @@ -129,17 +129,17 @@ func GetAllPermissionCtxIDs(perms []string) []string { return ctxIDs } -func addGetSystemUserRolesFuncToCtx(ctx context.Context, ctxData CtxData) context.Context { - if len(ctxData.SystemMemberships) != 0 { +func addGetSystemUserRolesFuncToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context { + if len(ctxData.SystemMemberships) != 0 && ctxData.SystemMemberships[0].MemberType == MemberTypeSystem { ctx = context.WithValue(ctx, systemUserRolesFuncKey, func() func(ctx context.Context) ([]string, error) { - var roles []string + var permissions []string return func(ctx context.Context) ([]string, error) { - if roles != nil { - return roles, nil + if permissions != nil { + return permissions, nil } var err error - roles, err = getSystemUserRoles(ctx) - return roles, err + permissions, err = getSystemUserPermissions(ctx, systemUserRoleMap) + return permissions, err } }()) } @@ -158,17 +158,15 @@ func GetSystemUserRoles(ctx context.Context) ([]string, error) { return getSystemUserRolesFunc(ctx) } -func getSystemUserRoles(ctx context.Context) ([]string, error) { +func getSystemUserPermissions(ctx context.Context, systemUserRoleMap []RoleMapping) ([]string, error) { ctxData, ok := ctx.Value(dataKey).(CtxData) if !ok { return nil, errors.New("unable to obtain ctxData") } - var roles []string - if ctxData.SystemMemberships != nil { - for _, member := range ctxData.SystemMemberships { - roles = append(roles, member.Roles...) - } + var permissions []string + for _, member := range ctxData.SystemMemberships { + permissions = append(permissions, member.Roles...) } - return roles, nil + return permissions, nil } diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index 8432f72ebd..d473f00834 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -55,6 +55,9 @@ type Membership struct { ObjectID string Roles []string + + // aggregate all the permissions for each role + Permissions []string } type MemberType int32 diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index e96a7b256b..e14afde393 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { +func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemPermissions []string, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID) if err != nil { return err @@ -22,7 +22,7 @@ func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMapp // getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level), // and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately. -func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { +func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, SystemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi } if ctxData.SystemMemberships != nil { - requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, SystemUserRoleMappings) return requestedPermissions, allPermissions, nil } diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 6eb326a59a..3a359e9aa9 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -13,13 +13,13 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return authorize(ctx, req, info, handler, verifier, authConfig) + return authorize(ctx, req, info, handler, verifier, systemUserPermissions, authConfig) } } -func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) { +func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ interface{}, err error) { authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod) if !needsToken { return handler(ctx, req) @@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, } orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) - ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions, authConfig, authOpt, info.FullMethod) if err != nil { return nil, err } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 27b921b7d5..b686d3add9 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -36,6 +36,7 @@ type WithGatewayPrefix interface { func CreateServer( verifier authz.APITokenVerifier, + systemAuthz authz.Config, authConfig authz.Config, queries *query.Queries, externalDomain string, @@ -53,7 +54,7 @@ func CreateServer( middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), - middleware.AuthorizationInterceptor(verifier, authConfig), + middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig), middleware.TranslationHandler(), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), middleware.ExecutionHandler(queries), diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 1581d401b4..0adfd39430 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -71,7 +71,7 @@ func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig auth return nil, zerrors.ThrowUnauthenticated(nil, "AUT-1179", "auth header missing") } - ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authConfig, authOpt, r.RequestURI) if err != nil { return nil, err }