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:
Elio Bischof 2023-10-25 17:10:45 +02:00 committed by GitHub
parent c8b9b0ac75
commit 4980cd6a0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 959 additions and 410 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,33 @@ SystemAPIUsers:
KeyData: <base64 encoded value of system-user-1.pub>
```
You can define memberships for the user as well:
```yaml
SystemAPIUsers:
- system-user-1:
Path: /system-user-1.pub
Memberships:
# MemberType System allows the user to access all APIs for all instances or organizations
- MemberType: System
Roles:
- "SYSTEM_OWNER"
- "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
```
If you don't specify any memberships, you are allowed to access the whole [ZITADEL System API](/apis/resources/system).
## Generate JWT
Similar to the OAuth 2.0 JWT Profile, we will create and sign a JWT. For this API, the JWT will not be used to authenticate against ZITADEL Authorization Server, but rather directly to the API itself.
@ -145,8 +172,6 @@ You should get a successful response with a `totalResult` number of 1 and the de
}
```
With this token you are allowed to access the whole [ZITADEL System API](/apis/resources/system).
## Summary
* Create an RSA keypair

View File

@ -0,0 +1,63 @@
---
title: Technical Advisory 10007
---
## Date and Version
Version: Upcoming
Date: Upcoming
## Affected Users
This advisory applies to self-hosted ZITADEL installations with custom roles to permissions mappings in the *InternalAuthZ.RolePermissionMappings* configuration section.
## Description
In upcoming ZITADEL versions, RBAC also applies to [system users defined in the ZITADEL runtime configuration](/guides/integrate/access-zitadel-system-api#runtime-configuration).
This enables fine grained access control to the system API as well as other APIs for system users.
ZITADEL defines the new default roles *SYSTEM_OWNER* and *SYSTEM_OWNER_VIEWER*.
System users without any memberships defined in the configuration will be assigned the *SYSTEM_OWNER* role.
**Self-hosting users who define their own custom mapping at the *InternalAuthZ.RolePermissionMappings* configuration section**, have to define the *SYSTEM_OWNER* role in their configuration too to be able to access the system API with the default system user membership.
## Statement
This change is tracked in the following PR: [feat: add SYSTEM_OWNER role](https://github.com/zitadel/zitadel/pull/6765).
As soon as the release version is published, we will include the version here.
## Mitigation
If you have a custom role mapping configured, make sure you configure the new role *SYSTEM_OWNER* before migrating to upcoming ZITADEL versions.
As a reference, these are the default mappings:
```yaml
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"
...
```
## Impact
If the system users don't have the correct memberships and roles which resolve to permissions, the system users lose access to the system API.

View File

@ -130,6 +130,18 @@ We understand that these advisories may include breaking changes, and we aim to
<td>2.39.0</td>
<td>Calendar week 41/42 2023</td>
</tr>
<tr>
<td>
<a href="./advisory/a10007">A-10006</a>
</td>
<td>Additional grant to cockroach database user</td>
<td>Breaking Behaviour Change</td>
<td>
Upcoming Versions require the SYSTEM_OWNER role to be available in the permission role mappings. Self-hosting ZITADEL users who define custom permission role mappings need to make sure their system users don't lose access to the system API.
</td>
<td>Upcoming</td>
<td>Upcoming</td>
</tr>
</table>
## Subscribe to our Mailing List

View File

@ -45,29 +45,47 @@ func (t TriggerMethod) String() string {
}
func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod) {
triggerLog(authz.GetInstance(ctx).InstanceID(), orgID, userID, http_utils.ComposedOrigin(ctx), trigger, info.ActivityInfoFromContext(ctx))
ai := info.ActivityInfoFromContext(ctx)
triggerLog(
authz.GetInstance(ctx).InstanceID(),
orgID,
userID,
http_utils.ComposedOrigin(ctx),
trigger,
ai.Method,
ai.Path,
ai.RequestMethod,
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func TriggerWithContext(ctx context.Context, trigger TriggerMethod) {
data := authz.GetCtxData(ctx)
ai := info.ActivityInfoFromContext(ctx)
// if GRPC call, path is prefilled with the grpc fullmethod and method is empty
if ai.Method == "" {
ai.Method = ai.Path
ai.Path = ""
}
triggerLog(authz.GetInstance(ctx).InstanceID(), data.OrgID, data.UserID, http_utils.ComposedOrigin(ctx), trigger, ai)
// GRPC call the method is contained in the HTTP request path
method := ai.Path
triggerLog(
authz.GetInstance(ctx).InstanceID(),
authz.GetCtxData(ctx).OrgID,
authz.GetCtxData(ctx).UserID,
http_utils.ComposedOrigin(ctx),
trigger,
method,
"",
ai.RequestMethod,
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, ai *info.ActivityInfo) {
func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, method, path, requestMethod string, isSystemUser bool) {
logging.WithFields(
"instance", instanceID,
"org", orgID,
"user", userID,
"domain", domain,
"trigger", trigger.String(),
"method", ai.Method,
"path", ai.Path,
"requestMethod", ai.RequestMethod,
"method", method,
"path", path,
"requestMethod", requestMethod,
"isSystemUser", isSystemUser,
).Info(Activity)
}

View File

@ -28,7 +28,7 @@ import (
type API struct {
port uint16
grpcServer *grpc.Server
verifier *internal_authz.TokenVerifier
verifier internal_authz.APITokenVerifier
health healthCheck
router *mux.Router
http1HostName string
@ -47,7 +47,7 @@ func New(
port uint16,
router *mux.Router,
queries *query.Queries,
verifier *internal_authz.TokenVerifier,
verifier internal_authz.APITokenVerifier,
authZ internal_authz.Config,
tlsConfig *tls.Config, http2HostName, http1HostName string,
accessInterceptor *http_mw.AccessInterceptor,

View File

@ -80,7 +80,7 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defa
http.Error(w, err.Error(), code)
}
func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
h := &Handler{
commands: commands,
errorHandler: DefaultErrorHandler,

View File

@ -0,0 +1,44 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
BearerPrefix = "Bearer "
)
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
}
type authZRepo interface {
MembershipsResolver
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}
var _ AccessTokenVerifier = (*AccessTokenVerifierFromRepo)(nil)
type AccessTokenVerifierFromRepo struct {
authZRepo authZRepo
}
func StartAccessTokenVerifierFromRepo(authZRepo authZRepo) *AccessTokenVerifierFromRepo {
return &AccessTokenVerifierFromRepo{authZRepo: authZRepo}
}
func (a *AccessTokenVerifierFromRepo) VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userID, agentID, clientID, prefLang, resourceOwner, err = a.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
return userID, clientID, agentID, prefLang, resourceOwner, err
}
type client struct {
name string
}

View File

@ -2,19 +2,17 @@ package authz
import (
"context"
"sync"
"testing"
"github.com/zitadel/zitadel/internal/errors"
)
func Test_VerifyAccessToken(t *testing.T) {
func Test_extractBearerToken(t *testing.T) {
type args struct {
ctx context.Context
token string
verifier *TokenVerifier
method string
verifier AccessTokenVerifier
}
tests := []struct {
name string
@ -42,23 +40,16 @@ func Test_VerifyAccessToken(t *testing.T) {
args: args{
ctx: context.Background(),
token: "Bearer AUTH",
verifier: &TokenVerifier{
authZRepo: &testVerifier{memberships: []*Membership{}},
clients: func() sync.Map {
m := sync.Map{}
m.Store("service", &client{name: "name"})
return m
}(),
authMethods: MethodMapping{"/service/method": Option{Permission: "authenticated"}},
},
method: "/service/method",
verifier: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, _, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier, tt.args.method)
_, err := extractBearerToken(tt.args.token)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}

View File

@ -0,0 +1,69 @@
package authz
import (
"context"
"sync"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
// TODO: Define interfaces where they are accepted
type APITokenVerifier interface {
AccessTokenVerifier
SystemTokenVerifier
RegisterServer(appName, methodPrefix string, mappings MethodMapping)
CheckAuthMethod(method string) (Option, bool)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error)
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error)
}
type ApiTokenVerifier struct {
AccessTokenVerifier
SystemTokenVerifier
authZRepo authZRepo
clients sync.Map
authMethods MethodMapping
}
func StartAPITokenVerifier(authZRepo authZRepo, accessTokenVerifier AccessTokenVerifier, systemTokenVerifier SystemTokenVerifier) *ApiTokenVerifier {
return &ApiTokenVerifier{
authZRepo: authZRepo,
SystemTokenVerifier: systemTokenVerifier,
AccessTokenVerifier: accessTokenVerifier,
}
}
func (v *ApiTokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
v.clients.Store(methodPrefix, &client{name: appName})
if v.authMethods == nil {
v.authMethods = make(map[string]Option)
}
for method, option := range mappings {
v.authMethods[method] = option
}
}
func (v *ApiTokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk)
}
func (v *ApiTokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}
func (v *ApiTokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *ApiTokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
}

View File

@ -19,11 +19,11 @@ 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 *TokenVerifier, 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, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, method)
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier)
if err != nil {
return nil, err
}

View File

@ -1,12 +1,15 @@
//go:generate enumer -type MemberType -trimprefix MemberType
package authz
import (
"context"
"errors"
"strings"
"github.com/zitadel/zitadel/internal/api/grpc"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/errors"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
@ -26,10 +29,11 @@ type CtxData struct {
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
}
func (ctxData CtxData) IsZero() bool {
return ctxData.UserID == "" || ctxData.OrgID == ""
return ctxData.UserID == "" || ctxData.OrgID == "" && ctxData.SystemMemberships == nil
}
type Grants []*Grant
@ -54,29 +58,68 @@ type MemberType int32
const (
MemberTypeUnspecified MemberType = iota
MemberTypeOrganisation
MemberTypeOrganization
MemberTypeProject
MemberTypeProjectGrant
MemberTypeIam
MemberTypeIAM
MemberTypeSystem
)
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t *TokenVerifier, method string) (_ CtxData, err error) {
type TokenVerifier interface {
ExistsOrg(ctx context.Context, id, domain string) (string, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
AccessTokenVerifier
SystemTokenVerifier
}
type AccessTokenVerifier interface {
VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLan, resourceOwner string, err error)
}
// AccessTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a AccessTokenVerifier.
type AccessTokenVerifierFunc func(context.Context, string) (string, string, string, string, string, error)
func (a AccessTokenVerifierFunc) VerifyAccessToken(ctx context.Context, token string) (string, string, string, string, string, error) {
return a(ctx, token)
}
type SystemTokenVerifier interface {
VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error)
}
// SystemTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a SystemTokenVerifier.
type SystemTokenVerifierFunc func(context.Context, string, string) (Memberships, string, error)
func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token string, orgID string) (Memberships, string, error) {
return s(ctx, token, orgID)
}
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userID, clientID, agentID, prefLang, resourceOwner, err := verifyAccessToken(ctx, token, t, method)
tokenWOBearer, err := extractBearerToken(token)
if err != nil {
return CtxData{}, err
}
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
return CtxData{UserID: userID}, nil
userID, clientID, agentID, prefLang, resourceOwner, err := t.VerifyAccessToken(ctx, tokenWOBearer)
var sysMemberships Memberships
if err != nil && !zitadel_errors.IsUnauthenticated(err) {
return CtxData{}, err
}
if err != nil {
var sysTokenErr error
sysMemberships, userID, sysTokenErr = t.VerifySystemToken(ctx, tokenWOBearer, orgID)
err = errors.Join(err, sysTokenErr)
if sysTokenErr != nil || sysMemberships == nil {
return CtxData{}, err
}
}
var projectID string
var origins []string
if clientID != "" {
projectID, origins, err = t.ProjectIDAndOriginsByClientID(ctx, clientID)
if err != nil {
return CtxData{}, errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid")
return CtxData{}, zitadel_errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid")
}
// We used to check origins for every token, but service users shouldn't be used publicly (native app / SPA).
// Therefore, mostly won't send an origin and aren't able to configure them anyway.
@ -88,21 +131,22 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st
if orgID == "" && orgDomain == "" {
orgID = resourceOwner
}
verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
// System API calls dont't have a resource owner
if orgID != "" {
orgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
}
}
return CtxData{
UserID: userID,
OrgID: verifiedOrgID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
}, nil
}
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {
@ -119,11 +163,6 @@ func GetRequestPermissionsFromCtx(ctx context.Context) []string {
return ctxPermission
}
func GetAllPermissionsFromCtx(ctx context.Context) []string {
ctxPermission, _ := ctx.Value(allPermissionsKey).([]string)
return ctxPermission
}
func checkOrigin(ctx context.Context, origins []string) error {
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
if origin == "" {
@ -135,5 +174,13 @@ func checkOrigin(ctx context.Context, origins []string) error {
if http_util.IsOriginAllowed(origins, origin) {
return nil
}
return errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
return zitadel_errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
}
func extractBearerToken(token string) (part string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", zitadel_errors.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return parts[1], nil
}

View File

@ -0,0 +1,94 @@
// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT.
package authz
import (
"fmt"
"strings"
)
const _MemberTypeName = "UnspecifiedOrganizationProjectProjectGrantIAMSystem"
var _MemberTypeIndex = [...]uint8{0, 11, 23, 30, 42, 45, 51}
const _MemberTypeLowerName = "unspecifiedorganizationprojectprojectgrantiamsystem"
func (i MemberType) String() string {
if i < 0 || i >= MemberType(len(_MemberTypeIndex)-1) {
return fmt.Sprintf("MemberType(%d)", i)
}
return _MemberTypeName[_MemberTypeIndex[i]:_MemberTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _MemberTypeNoOp() {
var x [1]struct{}
_ = x[MemberTypeUnspecified-(0)]
_ = x[MemberTypeOrganization-(1)]
_ = x[MemberTypeProject-(2)]
_ = x[MemberTypeProjectGrant-(3)]
_ = x[MemberTypeIAM-(4)]
_ = x[MemberTypeSystem-(5)]
}
var _MemberTypeValues = []MemberType{MemberTypeUnspecified, MemberTypeOrganization, MemberTypeProject, MemberTypeProjectGrant, MemberTypeIAM, MemberTypeSystem}
var _MemberTypeNameToValueMap = map[string]MemberType{
_MemberTypeName[0:11]: MemberTypeUnspecified,
_MemberTypeLowerName[0:11]: MemberTypeUnspecified,
_MemberTypeName[11:23]: MemberTypeOrganization,
_MemberTypeLowerName[11:23]: MemberTypeOrganization,
_MemberTypeName[23:30]: MemberTypeProject,
_MemberTypeLowerName[23:30]: MemberTypeProject,
_MemberTypeName[30:42]: MemberTypeProjectGrant,
_MemberTypeLowerName[30:42]: MemberTypeProjectGrant,
_MemberTypeName[42:45]: MemberTypeIAM,
_MemberTypeLowerName[42:45]: MemberTypeIAM,
_MemberTypeName[45:51]: MemberTypeSystem,
_MemberTypeLowerName[45:51]: MemberTypeSystem,
}
var _MemberTypeNames = []string{
_MemberTypeName[0:11],
_MemberTypeName[11:23],
_MemberTypeName[23:30],
_MemberTypeName[30:42],
_MemberTypeName[42:45],
_MemberTypeName[45:51],
}
// MemberTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func MemberTypeString(s string) (MemberType, error) {
if val, ok := _MemberTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _MemberTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to MemberType values", s)
}
// MemberTypeValues returns all values of the enum
func MemberTypeValues() []MemberType {
return _MemberTypeValues
}
// MemberTypeStrings returns a slice of all String values of the enum
func MemberTypeStrings() []string {
strs := make([]string, len(_MemberTypeNames))
copy(strs, _MemberTypeNames)
return strs
}
// IsAMemberType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i MemberType) IsAMemberType() bool {
for _, v := range _MemberTypeValues {
if i == v {
return true
}
}
return false
}

View File

@ -30,6 +30,11 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi
return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing")
}
if ctxData.SystemMemberships != nil {
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings)
return requestedPermissions, allPermissions, nil
}
ctx = context.WithValue(ctx, dataKey, ctxData)
memberships, err := resolver.SearchMyMemberships(ctx, orgID, false)
if err != nil {

View File

@ -7,33 +7,6 @@ import (
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
func getTestCtx(userID, orgID string) context.Context {
return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
}
type testVerifier struct {
memberships []*Membership
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "userID", "agentID", "clientID", "de", "orgID", nil
}
func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*Membership, error) {
return v.memberships, nil
}
func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "clientID", "projectID", nil
}
func equalStringArray(a, b []string) bool {
if len(a) != len(b) {
return false
@ -46,12 +19,18 @@ func equalStringArray(a, b []string) bool {
return true
}
type membershipsResolverFunc func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
func (m membershipsResolverFunc) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return m(ctx, orgID, shouldTriggerBulk)
}
func Test_GetUserPermissions(t *testing.T) {
type args struct {
ctxData CtxData
verifier *TokenVerifier
requiredPerm string
authConfig Config
ctxData CtxData
membershipsResolver MembershipsResolver
requiredPerm string
authConfig Config
}
tests := []struct {
name string
@ -64,11 +43,9 @@ func Test_GetUserPermissions(t *testing.T) {
name: "Empty Context",
args: args{
ctxData: CtxData{},
verifier: Start(&testVerifier{memberships: []*Membership{
{
Roles: []string{"ORG_OWNER"},
},
}}, "", nil),
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{{Roles: []string{"ORG_OWNER"}}}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@ -90,8 +67,10 @@ func Test_GetUserPermissions(t *testing.T) {
{
name: "No Grants",
args: args{
ctxData: CtxData{},
verifier: Start(&testVerifier{memberships: []*Membership{}}, "", nil),
ctxData: CtxData{},
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@ -112,14 +91,16 @@ func Test_GetUserPermissions(t *testing.T) {
name: "Get Permissions",
args: args{
ctxData: CtxData{UserID: "userID", OrgID: "orgID"},
verifier: Start(&testVerifier{memberships: []*Membership{
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIam,
Roles: []string{"IAM_OWNER"},
},
}}, "", nil),
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@ -139,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
_, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
@ -176,7 +157,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
@ -204,7 +185,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
@ -232,13 +213,13 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIam,
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
},
@ -266,7 +247,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "2",
ObjectID: "2",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
@ -327,7 +308,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{
@ -355,7 +336,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{

View File

@ -0,0 +1,32 @@
package authz
import (
"context"
"encoding/base64"
"fmt"
"github.com/zitadel/zitadel/internal/crypto"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return err
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return zitadel_errors.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
}
return nil
}
}

View File

@ -0,0 +1,117 @@
package authz
import (
"context"
"crypto/rsa"
"errors"
"os"
"sync"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
)
var _ SystemTokenVerifier = (*SystemTokenVerifierFromConfig)(nil)
type SystemTokenVerifierFromConfig struct {
systemJWTProfile *op.JWTProfileVerifier
systemUsers map[string]Memberships
}
func StartSystemTokenVerifierFromConfig(issuer string, keys map[string]*SystemAPIUser) (*SystemTokenVerifierFromConfig, error) {
systemUsers := make(map[string]Memberships, len(keys))
for userID, key := range keys {
if len(key.Memberships) == 0 {
systemUsers[userID] = Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}}
continue
}
for _, membership := range key.Memberships {
switch membership.MemberType {
case MemberTypeSystem, MemberTypeIAM, MemberTypeOrganization:
systemUsers[userID] = key.Memberships
case MemberTypeUnspecified, MemberTypeProject, MemberTypeProjectGrant:
return nil, errors.New("for system users, only the membership types System, IAM and Organization are supported")
default:
return nil, errors.New("unknown membership type")
}
}
}
return &SystemTokenVerifierFromConfig{
systemJWTProfile: op.NewJWTProfileVerifier(
&systemJWTStorage{
keys: keys,
cachedKeys: make(map[string]*rsa.PublicKey),
},
issuer,
1*time.Hour,
time.Second,
),
systemUsers: systemUsers,
}, nil
}
func (s *SystemTokenVerifierFromConfig) VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error) {
jwtReq, err := op.VerifyJWTAssertion(ctx, token, s.systemJWTProfile)
if err != nil {
return nil, "", err
}
systemUserMemberships, ok := s.systemUsers[jwtReq.Subject]
if !ok {
return nil, "", zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
matchingMemberships = make(Memberships, 0, len(systemUserMemberships))
for _, membership := range systemUserMemberships {
if membership.MemberType == MemberTypeSystem ||
membership.MemberType == MemberTypeIAM && GetInstance(ctx).InstanceID() == membership.AggregateID ||
membership.MemberType == MemberTypeOrganization && orgID == membership.AggregateID {
matchingMemberships = append(matchingMemberships, membership)
}
}
return matchingMemberships, jwtReq.Subject, nil
}
type systemJWTStorage struct {
keys map[string]*SystemAPIUser
mutex sync.Mutex
cachedKeys map[string]*rsa.PublicKey
}
type SystemAPIUser struct {
Path string // if a path is specified, the key will be read from that path
KeyData []byte // else you can also specify the data directly in the KeyData
Memberships Memberships
}
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
if s.Path != "" {
var err error
s.KeyData, err = os.ReadFile(s.Path)
if err != nil {
return nil, zitadel_errors.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
}
}
return crypto.BytesToPublicKey(s.KeyData)
}
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
cachedKey, ok := s.cachedKeys[userID]
if ok {
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
}
key, ok := s.keys[userID]
if !ok {
return nil, zitadel_errors.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
}
s.mutex.Lock()
defer s.mutex.Unlock()
publicKey, err := key.readKey()
if err != nil {
return nil, err
}
s.cachedKeys[userID] = publicKey
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
}

View File

@ -1,188 +0,0 @@
package authz
import (
"context"
"crypto/rsa"
"encoding/base64"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
BearerPrefix = "Bearer "
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
type TokenVerifier struct {
authZRepo authZRepo
clients sync.Map
authMethods MethodMapping
systemJWTProfile *op.JWTProfileVerifier
}
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
}
type authZRepo interface {
MembershipsResolver
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}
func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
return &TokenVerifier{
authZRepo: authZRepo,
systemJWTProfile: op.NewJWTProfileVerifier(
&systemJWTStorage{
keys: keys,
cachedKeys: make(map[string]*rsa.PublicKey),
},
issuer,
1*time.Hour,
time.Second,
),
}
}
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
userID, err := v.verifySystemToken(ctx, token)
if err != nil {
return "", "", "", "", "", err
}
return userID, "", "", "", "", nil
}
userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
return userID, clientID, agentID, prefLang, resourceOwner, err
}
func (v *TokenVerifier) verifySystemToken(ctx context.Context, token string) (string, error) {
jwtReq, err := op.VerifyJWTAssertion(ctx, token, v.systemJWTProfile)
if err != nil {
return "", err
}
return jwtReq.Subject, nil
}
type systemJWTStorage struct {
keys map[string]*SystemAPIUser
mutex sync.Mutex
cachedKeys map[string]*rsa.PublicKey
}
type SystemAPIUser struct {
Path string //if a path is specified, the key will be read from that path
KeyData []byte //else you can also specify the data directly in the KeyData
}
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
if s.Path != "" {
var err error
s.KeyData, err = os.ReadFile(s.Path)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
}
}
return crypto.BytesToPublicKey(s.KeyData)
}
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
cachedKey, ok := s.cachedKeys[userID]
if ok {
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
}
key, ok := s.keys[userID]
if !ok {
return nil, caos_errs.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
}
defer s.mutex.Unlock()
s.mutex.Lock()
publicKey, err := key.readKey()
if err != nil {
return nil, err
}
s.cachedKeys[userID] = publicKey
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
}
type client struct {
id string
projectID string
name string
}
func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
v.clients.Store(methodPrefix, &client{name: appName})
if v.authMethods == nil {
v.authMethods = make(map[string]Option)
}
for method, option := range mappings {
v.authMethods[method] = option
}
}
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk)
}
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}
func (v *TokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
}
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID, prefLan, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return t.VerifyAccessToken(ctx, parts[1], method)
}
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return err
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
var token string
token, err = algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return caos_errs.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
}
return nil
}
}

View File

@ -77,5 +77,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
}
func (s *Server) GatewayPathPrefix() string {
return GatewayPathPrefix()
}
func GatewayPathPrefix() string {
return "/auth/v1"
}

View File

@ -13,13 +13,13 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, 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)
}
}
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier *authz.TokenVerifier, authConfig authz.Config) (_ interface{}, err error) {
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) {
authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod)
if !needsToken {
return handler(ctx, req)

View File

@ -2,6 +2,7 @@ package middleware
import (
"context"
"errors"
"reflect"
"testing"
@ -9,44 +10,54 @@ import (
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/api/authz"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
)
var (
mockMethods = authz.MethodMapping{
"need.authentication": authz.Option{
Permission: "authenticated",
},
}
)
const anAPIRole = "AN_API_ROLE"
type verifierMock struct{}
type authzRepoMock struct{}
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}
func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return nil, nil
func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return authz.Memberships{{
MemberType: authz.MemberTypeOrganization,
AggregateID: orgID,
Roles: []string{anAPIRole},
}}, nil
}
func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
}
var (
accessTokenOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) {
return "user1", "", "", "", "org1", nil
})
accessTokenNOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) {
return "", "", "", "", "", zitadel_errors.ThrowUnauthenticated(nil, "TEST-fQHDI", "unauthenticaded")
})
systemTokenNOK = authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return nil, "", errors.New("system token error")
})
)
func Test_authorize(t *testing.T) {
type args struct {
ctx context.Context
req interface{}
info *grpc.UnaryServerInfo
handler grpc.UnaryHandler
verifier *authz.TokenVerifier
authConfig authz.Config
authMethods authz.MethodMapping
ctx context.Context
req interface{}
info *grpc.UnaryServerInfo
handler grpc.UnaryHandler
verifier func() authz.APITokenVerifier
authConfig authz.Config
}
type res struct {
want interface{}
@ -64,12 +75,11 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/no/token/needed"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{})
return verifier
}(),
authMethods: mockMethods,
},
},
res{
&mockReq{},
@ -83,13 +93,12 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
nil,
@ -103,13 +112,12 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
nil,
@ -123,13 +131,118 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
&mockReq{},
false,
},
},
{
"permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: anAPIRole,
Permissions: []string{"to.do.something.else"},
}},
},
},
res{
nil,
true,
},
},
{
"permission ok",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: anAPIRole,
Permissions: []string{"to.do.something"},
}},
},
},
res{
&mockReq{},
false,
},
},
{
"system token permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return authz.Memberships{{
MemberType: authz.MemberTypeSystem,
Roles: []string{"A_SYSTEM_ROLE"},
}}, "systemuser", nil
}))
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: "A_SYSTEM_ROLE",
Permissions: []string{"to.do.something.else"},
}},
},
},
res{
nil,
true,
},
},
{
"system token permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return authz.Memberships{{
MemberType: authz.MemberTypeSystem,
Roles: []string{"A_SYSTEM_ROLE"},
}}, "systemuser", nil
}))
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: "A_SYSTEM_ROLE",
Permissions: []string{"to.do.something"},
}},
},
},
res{
&mockReq{},
@ -139,7 +252,7 @@ func Test_authorize(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, tt.args.authConfig)
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig)
if (err != nil) != tt.res.wantErr {
t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr)
return

View File

@ -28,7 +28,9 @@ func QuotaExhaustedInterceptor(svc *logstore.Service[*record.AccessLog], ignoreS
// The auth interceptor will ensure that only authorized or public requests are allowed.
// So if there's no authorization context, we don't need to check for limitation
if authz.GetCtxData(ctx).IsZero() {
// Also, we don't limit calls with system user tokens
ctxData := authz.GetCtxData(ctx)
if ctxData.IsZero() || ctxData.SystemMemberships != nil {
return handler(ctx, req)
}

View File

@ -35,7 +35,7 @@ type WithGatewayPrefix interface {
}
func CreateServer(
verifier *authz.TokenVerifier,
verifier authz.APITokenVerifier,
authConfig authz.Config,
queries *query.Queries,
hostHeaderName string,

View File

@ -2,31 +2,13 @@ package middleware
import (
"net/http"
"strings"
"github.com/zitadel/zitadel/internal/api/info"
)
func ActivityHandler(handlerPrefixes []string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
activityInfo := info.ActivityInfoFromContext(ctx)
hasPrefix := false
// only add path to context if handler is called
for _, prefix := range handlerPrefixes {
if strings.HasPrefix(r.URL.Path, prefix) {
activityInfo.SetPath(r.URL.Path)
hasPrefix = true
break
}
}
// last call is with grpc method as path
if !hasPrefix {
activityInfo.SetMethod(r.URL.Path)
}
ctx = activityInfo.SetRequestMethod(r.Method).IntoContext(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func ActivityHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := info.ActivityInfoFromContext(r.Context()).SetPath(r.URL.Path).SetRequestMethod(r.Method).IntoContext(r.Context())
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@ -11,11 +11,11 @@ import (
)
type AuthInterceptor struct {
verifier *authz.TokenVerifier
verifier authz.APITokenVerifier
authConfig authz.Config
}
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) *AuthInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor {
return &AuthInterceptor{
verifier: verifier,
authConfig: authConfig,
@ -48,7 +48,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
type httpReq struct{}
func authorize(r *http.Request, verifier *authz.TokenVerifier, authConfig authz.Config) (_ context.Context, err error) {
func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) {
ctx := r.Context()
authOpt, needsToken := verifier.CheckAuthMethod(r.Method + ":" + r.RequestURI)
if !needsToken {

View File

@ -51,7 +51,7 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID
func userMembershipToMembership(membership *query.Membership) *authz.Membership {
if membership.IAM != nil {
return &authz.Membership{
MemberType: authz.MemberTypeIam,
MemberType: authz.MemberTypeIAM,
AggregateID: membership.IAM.IAMID,
ObjectID: membership.IAM.IAMID,
Roles: membership.Roles,
@ -59,7 +59,7 @@ func userMembershipToMembership(membership *query.Membership) *authz.Membership
}
if membership.Org != nil {
return &authz.Membership{
MemberType: authz.MemberTypeOrganisation,
MemberType: authz.MemberTypeOrganization,
AggregateID: membership.Org.OrgID,
ObjectID: membership.Org.OrgID,
Roles: membership.Roles,

View File

@ -4,11 +4,10 @@ import (
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/zitadel/zitadel/internal/domain"
"golang.org/x/exp/constraints"
)
func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType {
func EnumHookFunc[T constraints.Integer](resolve func(string) (T, error)) mapstructure.DecodeHookFuncType {
return func(
f reflect.Type,
t reflect.Type,
@ -17,11 +16,9 @@ func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(domain.FeatureUnspecified) {
if t != reflect.TypeOf(T(0)) {
return data, nil
}
return domain.FeatureString(data.(string))
return resolve(data.(string))
}
}

View File

@ -293,6 +293,10 @@ service AdminService {
post: "/domains/_search";
};
option (zitadel.v1.auth_option) = {
permission: "iam.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Instance";
summary: "List Instance Domains";

View File

@ -115,7 +115,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.read";
};
}
@ -126,7 +126,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.read";
};
}
@ -140,7 +140,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.write";
};
}
@ -152,7 +152,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.write";
};
}
@ -165,7 +165,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.write";
};
}
@ -177,12 +177,13 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.instance.delete";
};
}
//Returns all instance members matching the request
// all queries need to match (ANDed)
// Deprecated: Use the Admin APIs ListIAMMembers instead
rpc ListIAMMembers(ListIAMMembersRequest) returns (ListIAMMembersResponse) {
option (google.api.http) = {
post: "/instances/{instance_id}/members/_search";
@ -190,11 +191,11 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.iam.member.read";
};
}
// Checks if a domain exists
//Checks if a domain exists
rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) {
option (google.api.http) = {
post: "/domains/{domain}/_exists";
@ -202,11 +203,13 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.domain.read";
};
}
// Returns the custom domains of an instance
//Checks if a domain exists
// Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead
rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) {
option (google.api.http) = {
post: "/instances/{instance_id}/domains/_search";
@ -214,7 +217,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.domain.read";
};
}
@ -226,7 +229,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.domain.write";
};
}
@ -237,7 +240,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.domain.delete";
};
}
@ -249,7 +252,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.domain.write";
};
}
@ -263,7 +266,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.debug.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@ -287,7 +290,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.debug.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@ -311,7 +314,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.debug.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@ -336,9 +339,8 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.debug.delete";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "failed events";
responses: {
@ -375,7 +377,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.quota.write";
};
}
@ -392,7 +394,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.quota.write";
};
}
@ -407,7 +409,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.quota.delete";
};
}
@ -419,7 +421,7 @@ service SystemService {
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
permission: "system.feature.write";
};
}