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