mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-04 07:55:23 +00:00

# Which Problems Are Solved Currently ZITADEL defines organization and instance member roles and permissions in defaults.yaml. The permission check is done on API call level. For example: "is this user allowed to make this call on this org". This makes sense on the V1 API where the API is permission-level shaped. For example, a search for users always happens in the context of the organization. (Either the organization the calling user belongs to, or through member ship and the x-zitadel-orgid header. However, for resource based APIs we must be able to resolve permissions by object. For example, an IAM_OWNER listing users should be able to get all users in an instance based on the query filters. Alternatively a user may have user.read permissions on one or more orgs. They should be able to read just those users. # How the Problems Are Solved ## Role permission mapping The role permission mappings defined from `defaults.yaml` or local config override are synchronized to the database on every run of `zitadel setup`: - A single query per **aggregate** builds a list of `add` and `remove` actions needed to reach the desired state or role permission mappings from the config. - The required events based on the actions are pushed to the event store. - Events define search fields so that permission checking can use the indices and is strongly consistent for both query and command sides. The migration is split in the following aggregates: - System aggregate for for roles prefixed with `SYSTEM` - Each instance for roles not prefixed with `SYSTEM`. This is in anticipation of instance level management over the API. ## Membership Current instance / org / project membership events now have field table definitions. Like the role permissions this ensures strong consistency while still being able to use the indices of the fields table. A migration is provided to fill the membership fields. ## Permission check I aimed keeping the mental overhead to the developer to a minimal. The provided implementation only provides a permission check for list queries for org level resources, for example users. In the `query` package there is a simple helper function `wherePermittedOrgs` which makes sure the underlying database function is called as part of the `SELECT` query and the permitted organizations are part of the `WHERE` clause. This makes sure results from non-permitted organizations are omitted. Under the hood: - A Pg/PlSQL function searches for a list of organization IDs the passed user has the passed permission. - When the user has the permission on instance level, it returns early with all organizations. - The functions uses a number of views. The views help mapping the fields entries into relational data and simplify the code use for the function. The views provide some pre-filters which allow proper index usage once the final `WHERE` clauses are set by the function. # Additional Changes # Additional Context Closes #9032 Closes https://github.com/zitadel/zitadel/issues/9014 https://github.com/zitadel/zitadel/issues/9188 defines follow-ups for the new permission framework based on this concept.
225 lines
9.2 KiB
Go
225 lines
9.2 KiB
Go
package feature
|
|
|
|
import (
|
|
"net/url"
|
|
|
|
"github.com/muhlemmer/gu"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/feature"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
|
)
|
|
|
|
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command.SystemFeatures, error) {
|
|
loginV2, err := loginV2ToDomain(req.GetLoginV2())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &command.SystemFeatures{
|
|
LoginDefaultOrg: req.LoginDefaultOrg,
|
|
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
|
LegacyIntrospection: req.OidcLegacyIntrospection,
|
|
UserSchema: req.UserSchema,
|
|
Actions: req.Actions,
|
|
TokenExchange: req.OidcTokenExchange,
|
|
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
|
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
|
|
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
|
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
|
LoginV2: loginV2,
|
|
PermissionCheckV2: req.PermissionCheckV2,
|
|
}, nil
|
|
}
|
|
|
|
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
|
|
return &feature_pb.GetSystemFeaturesResponse{
|
|
Details: object.DomainToDetailsPb(f.Details),
|
|
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
|
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
|
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
|
UserSchema: featureSourceToFlagPb(&f.UserSchema),
|
|
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
|
Actions: featureSourceToFlagPb(&f.Actions),
|
|
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
|
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
|
|
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
|
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
|
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
|
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
|
|
}
|
|
}
|
|
|
|
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*command.InstanceFeatures, error) {
|
|
loginV2, err := loginV2ToDomain(req.GetLoginV2())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &command.InstanceFeatures{
|
|
LoginDefaultOrg: req.LoginDefaultOrg,
|
|
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
|
LegacyIntrospection: req.OidcLegacyIntrospection,
|
|
UserSchema: req.UserSchema,
|
|
TokenExchange: req.OidcTokenExchange,
|
|
Actions: req.Actions,
|
|
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
|
WebKey: req.WebKey,
|
|
DebugOIDCParentError: req.DebugOidcParentError,
|
|
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
|
|
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
|
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
|
LoginV2: loginV2,
|
|
PermissionCheckV2: req.PermissionCheckV2,
|
|
}, nil
|
|
}
|
|
|
|
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
|
|
return &feature_pb.GetInstanceFeaturesResponse{
|
|
Details: object.DomainToDetailsPb(f.Details),
|
|
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
|
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
|
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
|
UserSchema: featureSourceToFlagPb(&f.UserSchema),
|
|
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
|
Actions: featureSourceToFlagPb(&f.Actions),
|
|
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
|
WebKey: featureSourceToFlagPb(&f.WebKey),
|
|
DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError),
|
|
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
|
|
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
|
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
|
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
|
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
|
|
}
|
|
}
|
|
|
|
func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.ImprovedPerformanceType]) *feature_pb.ImprovedPerformanceFeatureFlag {
|
|
return &feature_pb.ImprovedPerformanceFeatureFlag{
|
|
ExecutionPaths: improvedPerformanceTypesToPb(fs.Value),
|
|
Source: featureLevelToSourcePb(fs.Level),
|
|
}
|
|
}
|
|
|
|
func loginV2ToDomain(loginV2 *feature_pb.LoginV2) (_ *feature.LoginV2, err error) {
|
|
if loginV2 == nil {
|
|
return nil, nil
|
|
}
|
|
var baseURI *url.URL
|
|
if loginV2.GetBaseUri() != "" {
|
|
baseURI, err = url.Parse(loginV2.GetBaseUri())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &feature.LoginV2{
|
|
Required: loginV2.GetRequired(),
|
|
BaseURI: baseURI,
|
|
}, nil
|
|
}
|
|
|
|
func loginV2ToLoginV2FlagPb(f query.FeatureSource[*feature.LoginV2]) *feature_pb.LoginV2FeatureFlag {
|
|
var required bool
|
|
var baseURI *string
|
|
if f.Value != nil {
|
|
required = f.Value.Required
|
|
if f.Value.BaseURI != nil && f.Value.BaseURI.String() != "" {
|
|
baseURI = gu.Ptr(f.Value.BaseURI.String())
|
|
}
|
|
}
|
|
return &feature_pb.LoginV2FeatureFlag{
|
|
Required: required,
|
|
BaseUri: baseURI,
|
|
Source: featureLevelToSourcePb(f.Level),
|
|
}
|
|
}
|
|
|
|
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
|
|
return &feature_pb.FeatureFlag{
|
|
Enabled: fs.Value,
|
|
Source: featureLevelToSourcePb(fs.Level),
|
|
}
|
|
}
|
|
|
|
func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
|
|
switch level {
|
|
case feature.LevelUnspecified:
|
|
return feature_pb.Source_SOURCE_UNSPECIFIED
|
|
case feature.LevelSystem:
|
|
return feature_pb.Source_SOURCE_SYSTEM
|
|
case feature.LevelInstance:
|
|
return feature_pb.Source_SOURCE_INSTANCE
|
|
case feature.LevelOrg:
|
|
return feature_pb.Source_SOURCE_ORGANIZATION
|
|
case feature.LevelProject:
|
|
return feature_pb.Source_SOURCE_PROJECT
|
|
case feature.LevelApp:
|
|
return feature_pb.Source_SOURCE_APP
|
|
case feature.LevelUser:
|
|
return feature_pb.Source_SOURCE_USER
|
|
default:
|
|
return feature_pb.Source(level)
|
|
}
|
|
}
|
|
|
|
func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []feature_pb.ImprovedPerformance {
|
|
res := make([]feature_pb.ImprovedPerformance, len(types))
|
|
|
|
for i, typ := range types {
|
|
res[i] = improvedPerformanceTypeToPb(typ)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
|
|
switch typ {
|
|
case feature.ImprovedPerformanceTypeUnknown:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
|
|
case feature.ImprovedPerformanceTypeOrgByID:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
|
|
case feature.ImprovedPerformanceTypeProjectGrant:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT
|
|
case feature.ImprovedPerformanceTypeProject:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
|
|
case feature.ImprovedPerformanceTypeUserGrant:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT
|
|
case feature.ImprovedPerformanceTypeOrgDomainVerified:
|
|
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED
|
|
default:
|
|
return feature_pb.ImprovedPerformance(typ)
|
|
}
|
|
}
|
|
|
|
func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []feature.ImprovedPerformanceType {
|
|
if list == nil {
|
|
return nil
|
|
}
|
|
res := make([]feature.ImprovedPerformanceType, len(list))
|
|
|
|
for i, typ := range list {
|
|
res[i] = improvedPerformanceToDomain(typ)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
|
|
switch typ {
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
|
|
return feature.ImprovedPerformanceTypeUnknown
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
|
|
return feature.ImprovedPerformanceTypeOrgByID
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
|
|
return feature.ImprovedPerformanceTypeProjectGrant
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
|
|
return feature.ImprovedPerformanceTypeProject
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT:
|
|
return feature.ImprovedPerformanceTypeUserGrant
|
|
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED:
|
|
return feature.ImprovedPerformanceTypeOrgDomainVerified
|
|
default:
|
|
return feature.ImprovedPerformanceTypeUnknown
|
|
}
|
|
}
|