zitadel/internal/api/authz/context.go
Livio Spring c347e75485
fix: ignore projectID and origin check for service accounts (#8704)
# Which Problems Are Solved

Calls with tokens issued through JWT Profile or Client Credentials
Grants were no longer possible and threw a "could not read projectid by
clientid (AUTH-GHpw2)" error.
ZITADEL checks the allowed origins of an application and load its
projectID into the context on any API call.
Tokens from service accounts did not contain any clientID and therefore
never did that check.
But due to a change in https://github.com/zitadel/zitadel/pull/8580,
were the service user id was set as client_id in the OIDC session to fix
the introspection response
(https://github.com/zitadel/zitadel/issues/8590).

# How the Problems Are Solved

- Check if the project and origin were retrieved and only then check the
origins

# Additional Changes

None.

# Additional Context

- closes https://github.com/zitadel/zitadel/issues/8676
- relates to https://github.com/zitadel/zitadel/pull/8580 (released on
2.62.0)
- relates to https://github.com/zitadel/zitadel/issues/8590
2024-10-01 16:38:28 +02:00

195 lines
5.9 KiB
Go

//go:generate enumer -type MemberType -trimprefix MemberType
package authz
import (
"context"
"errors"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/grpc"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type key int
const (
requestPermissionsKey key = 1
dataKey key = 2
allPermissionsKey key = 3
instanceKey key = 4
)
type CtxData struct {
UserID string
OrgID string
ProjectID string
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
}
func (ctxData CtxData) IsZero() bool {
return ctxData.UserID == "" || ctxData.OrgID == "" && ctxData.SystemMemberships == nil
}
type Grants []*Grant
type Grant struct {
OrgID string
Roles []string
}
type Memberships []*Membership
type Membership struct {
MemberType MemberType
AggregateID string
//ObjectID differs from aggregate id if object is sub of an aggregate
ObjectID string
Roles []string
}
type MemberType int32
const (
MemberTypeUnspecified MemberType = iota
MemberTypeOrganization
MemberTypeProject
MemberTypeProjectGrant
MemberTypeIAM
MemberTypeSystem
)
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) }()
tokenWOBearer, err := extractBearerToken(token)
if err != nil {
return CtxData{}, err
}
userID, clientID, agentID, prefLang, resourceOwner, err := t.VerifyAccessToken(ctx, tokenWOBearer)
var sysMemberships Memberships
if err != nil && !zerrors.IsUnauthenticated(err) {
return CtxData{}, err
}
if err != nil {
logging.WithFields("org_id", orgID, "org_domain", orgDomain).WithError(err).Warn("authz: verify access token")
var sysTokenErr error
sysMemberships, userID, sysTokenErr = t.VerifySystemToken(ctx, tokenWOBearer, orgID)
if sysTokenErr != nil || sysMemberships == nil {
return CtxData{}, zerrors.ThrowUnauthenticated(errors.Join(err, sysTokenErr), "AUTH-7fs1e", "Errors.Token.Invalid")
}
}
projectID, err := projectIDAndCheckOriginForClientID(ctx, clientID, t)
if err != nil {
return CtxData{}, err
}
if orgID == "" && orgDomain == "" {
orgID = resourceOwner
}
// System API calls don't have a resource owner
if orgID != "" {
orgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, zerrors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
}
}
return CtxData{
UserID: userID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
}, nil
}
func projectIDAndCheckOriginForClientID(ctx context.Context, clientID string, t APITokenVerifier) (string, error) {
if clientID == "" {
return "", nil
}
projectID, origins, err := t.ProjectIDAndOriginsByClientID(ctx, clientID)
logging.WithFields("clientID", clientID).OnError(err).Debug("could not check projectID and origin of clientID (might be service account)")
// 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.
// For the current time we will only check origins for tokens issued to users through apps (code / implicit flow).
if projectID == "" {
return "", nil
}
return projectID, checkOrigin(ctx, origins)
}
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {
return context.WithValue(ctx, dataKey, ctxData)
}
func GetCtxData(ctx context.Context) CtxData {
ctxData, _ := ctx.Value(dataKey).(CtxData)
return ctxData
}
func GetRequestPermissionsFromCtx(ctx context.Context) []string {
ctxPermission, _ := ctx.Value(requestPermissionsKey).([]string)
return ctxPermission
}
func checkOrigin(ctx context.Context, origins []string) error {
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
if origin == "" {
origin = http_util.OriginHeader(ctx)
if origin == "" {
return nil
}
}
if http_util.IsOriginAllowed(origins, origin) {
return nil
}
return zerrors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
}
func extractBearerToken(token string) (part string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", zerrors.ThrowUnauthenticated(nil, "AUTH-toLo1", "invalid auth header")
}
return parts[1], nil
}