fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! feat(permissions): Addeding system user support for permission check v2

This commit is contained in:
Iraq Jaber
2025-03-12 11:39:25 +00:00
parent 306ce97828
commit 255d77a6e9
12 changed files with 87 additions and 46 deletions

View File

@@ -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"]}]'

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -55,6 +55,9 @@ type Membership struct {
ObjectID string
Roles []string
// aggregate all the permissions for each role
Permissions []string
}
type MemberType int32

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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
}