mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:37:34 +00:00
feat: system api requires authenticated requests (#3570)
* begin auth * feat: system api requires authenticated requests * fix tests
This commit is contained in:
@@ -14,7 +14,6 @@ import (
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/authz/repository"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -37,23 +36,20 @@ type health interface {
|
||||
func New(
|
||||
port uint16,
|
||||
router *mux.Router,
|
||||
repo *struct {
|
||||
repository.Repository
|
||||
*query.Queries
|
||||
},
|
||||
queries *query.Queries,
|
||||
verifier *internal_authz.TokenVerifier,
|
||||
authZ internal_authz.Config,
|
||||
externalSecure bool,
|
||||
http2HostName string,
|
||||
) *API {
|
||||
verifier := internal_authz.Start(repo)
|
||||
api := &API{
|
||||
port: port,
|
||||
verifier: verifier,
|
||||
health: repo,
|
||||
health: queries,
|
||||
router: router,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
api.grpcServer = server.CreateServer(api.verifier, authZ, repo.Queries, http2HostName)
|
||||
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName)
|
||||
api.routeGRPC()
|
||||
|
||||
api.RegisterHandler("/debug", api.healthHandler())
|
||||
@@ -68,9 +64,7 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.Server) erro
|
||||
return err
|
||||
}
|
||||
a.RegisterHandler(prefix, handler)
|
||||
if a.verifier != nil {
|
||||
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
|
||||
}
|
||||
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
@@ -67,6 +68,9 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *To
|
||||
if err != nil {
|
||||
return CtxData{}, err
|
||||
}
|
||||
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
|
||||
return CtxData{UserID: userID}, nil
|
||||
}
|
||||
var projectID string
|
||||
var origins []string
|
||||
if clientID != "" {
|
||||
|
@@ -68,7 +68,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
{
|
||||
Roles: []string{"ORG_OWNER"},
|
||||
},
|
||||
}}),
|
||||
}}, "", nil),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
@@ -91,7 +91,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
name: "No Grants",
|
||||
args: args{
|
||||
ctxData: CtxData{},
|
||||
verifier: Start(&testVerifier{memberships: []*Membership{}}),
|
||||
verifier: Start(&testVerifier{memberships: []*Membership{}}, "", nil),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
@@ -119,7 +119,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
MemberType: MemberTypeIam,
|
||||
Roles: []string{"IAM_OWNER"},
|
||||
},
|
||||
}}),
|
||||
}}, "", nil),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
|
@@ -2,9 +2,16 @@ package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/op"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
@@ -14,9 +21,10 @@ const (
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
authZRepo authZRepo
|
||||
clients sync.Map
|
||||
authMethods MethodMapping
|
||||
authZRepo authZRepo
|
||||
clients sync.Map
|
||||
authMethods MethodMapping
|
||||
systemJWTProfile op.JWTProfileVerifier
|
||||
}
|
||||
|
||||
type authZRepo interface {
|
||||
@@ -27,15 +35,74 @@ type authZRepo interface {
|
||||
ExistsOrg(ctx context.Context, orgID string) error
|
||||
}
|
||||
|
||||
func Start(authZRepo authZRepo) (v *TokenVerifier) {
|
||||
return &TokenVerifier{authZRepo: authZRepo}
|
||||
func Start(authZRepo authZRepo, systemAPI string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
|
||||
return &TokenVerifier{
|
||||
authZRepo: authZRepo,
|
||||
systemJWTProfile: op.NewJWTProfileVerifier(
|
||||
&systemJWTStorage{
|
||||
keys: keys,
|
||||
cachedKeys: make(map[string]*rsa.PublicKey),
|
||||
},
|
||||
systemAPI,
|
||||
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
|
||||
}
|
||||
|
||||
func (s *systemJWTStorage) GetKeyByIDAndUserID(_ 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()
|
||||
keyData, err := os.ReadFile(key.Path)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
|
||||
}
|
||||
publicKey, err := crypto.BytesToPublicKey(keyData)
|
||||
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
|
||||
|
@@ -15,10 +15,6 @@ import (
|
||||
|
||||
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
//TODO: Change as soon as we know how to authenticate system api
|
||||
if verifier == nil {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
return authorize(ctx, req, info, handler, verifier, authConfig)
|
||||
}
|
||||
}
|
||||
|
@@ -65,7 +65,7 @@ func Test_authorize(t *testing.T) {
|
||||
info: mockInfo("/no/token/needed"),
|
||||
handler: emptyMockHandler,
|
||||
verifier: func() *authz.TokenVerifier {
|
||||
verifier := authz.Start(&verifierMock{})
|
||||
verifier := authz.Start(&verifierMock{}, "", nil)
|
||||
verifier.RegisterServer("need", "need", authz.MethodMapping{})
|
||||
return verifier
|
||||
}(),
|
||||
@@ -84,7 +84,7 @@ func Test_authorize(t *testing.T) {
|
||||
info: mockInfo("/need/authentication"),
|
||||
handler: emptyMockHandler,
|
||||
verifier: func() *authz.TokenVerifier {
|
||||
verifier := authz.Start(&verifierMock{})
|
||||
verifier := authz.Start(&verifierMock{}, "", nil)
|
||||
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
|
||||
return verifier
|
||||
}(),
|
||||
@@ -104,7 +104,7 @@ func Test_authorize(t *testing.T) {
|
||||
info: mockInfo("/need/authentication"),
|
||||
handler: emptyMockHandler,
|
||||
verifier: func() *authz.TokenVerifier {
|
||||
verifier := authz.Start(&verifierMock{})
|
||||
verifier := authz.Start(&verifierMock{}, "", nil)
|
||||
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
|
||||
return verifier
|
||||
}(),
|
||||
@@ -124,7 +124,7 @@ func Test_authorize(t *testing.T) {
|
||||
info: mockInfo("/need/authentication"),
|
||||
handler: emptyMockHandler,
|
||||
verifier: func() *authz.TokenVerifier {
|
||||
verifier := authz.Start(&verifierMock{})
|
||||
verifier := authz.Start(&verifierMock{}, "", nil)
|
||||
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
|
||||
return verifier
|
||||
}(),
|
||||
|
@@ -28,6 +28,9 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf
|
||||
interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
for _, service := range ignoredServices {
|
||||
if !strings.HasPrefix(service, "/") {
|
||||
service = "/" + service
|
||||
}
|
||||
if strings.HasPrefix(info.FullMethod, service) {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
@@ -29,8 +30,7 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, querie
|
||||
middleware.SentryHandler(),
|
||||
middleware.NoCacheInterceptor(),
|
||||
middleware.ErrorHandler(),
|
||||
//TODO: Handle Ignored Services
|
||||
middleware.InstanceInterceptor(queries, hostHeaderName, "/zitadel.system.v1.SystemService"),
|
||||
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_MethodPrefix),
|
||||
middleware.AuthorizationInterceptor(verifier, authConfig),
|
||||
middleware.TranslationHandler(),
|
||||
middleware.ValidationHandler(),
|
||||
|
Reference in New Issue
Block a user