mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:17:32 +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:
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
|
||||
}
|
Reference in New Issue
Block a user