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
34 changed files with 959 additions and 410 deletions

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