Skip to content

Commit

Permalink
feat(api): new session service (#5801)
Browse files Browse the repository at this point in the history
* backup new protoc plugin

* backup

* session

* backup

* initial implementation

* change to specific events

* implement tests

* cleanup

* refactor: use new protoc plugin for api v2

* change package

* simplify code

* cleanup

* cleanup

* fix merge

* start queries

* fix tests

* improve returned values

* add token to projection

* tests

* test db map

* update query

* permission checks

* fix tests and linting

* rework token creation

* i18n

* refactor token check and fix tests

* session to PB test

* request to query tests

* cleanup proto

* test user check

* add comment

* simplify database map type

* Update docs/docs/guides/integrate/access-zitadel-system-api.md

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>

* fix test

* cleanup

* docs

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
  • Loading branch information
livio-a and muhlemmer committed May 5, 2023
1 parent 74377c2 commit c2cb84c
Show file tree
Hide file tree
Showing 55 changed files with 3,909 additions and 104 deletions.
1 change: 1 addition & 0 deletions cmd/setup/03.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
nil,
nil,
nil,
nil,
)

if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/setup/config_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
nil,
nil,
nil,
nil,
)

if err != nil {
Expand Down
43 changes: 39 additions & 4 deletions cmd/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
"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"
Expand Down Expand Up @@ -129,7 +130,21 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
return fmt.Errorf("cannot start eventstore for queries: %w", err)
}

queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, keys.SAML, config.InternalAuthZ.RolePermissionMappings)
sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)

queries, err := query.StartQueries(
ctx,
eventstoreClient,
dbClient,
config.Projections,
config.SystemDefaults,
keys.IDPConfig,
keys.OTP,
keys.OIDC,
keys.SAML,
config.InternalAuthZ.RolePermissionMappings,
sessionTokenVerifier,
)
if err != nil {
return fmt.Errorf("cannot start queries: %w", err)
}
Expand All @@ -138,6 +153,9 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
if err != nil {
return fmt.Errorf("error starting authz repo: %w", err)
}
permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
}

storage, err := config.AssetStorage.NewStorage(dbClient.DB)
if err != nil {
Expand Down Expand Up @@ -165,7 +183,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
keys.OIDC,
keys.SAML,
&http.Client{},
authZRepo,
permissionCheck,
sessionTokenVerifier,
)
if err != nil {
return fmt.Errorf("cannot start commands: %w", err)
Expand Down Expand Up @@ -195,7 +214,22 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
if err != nil {
return err
}
err = startAPIs(ctx, clock, router, commands, queries, eventstoreClient, dbClient, config, storage, authZRepo, keys, queries, usageReporter)
err = startAPIs(
ctx,
clock,
router,
commands,
queries,
eventstoreClient,
dbClient,
config,
storage,
authZRepo,
keys,
queries,
usageReporter,
permissionCheck,
)
if err != nil {
return err
}
Expand Down Expand Up @@ -239,6 +273,7 @@ func startAPIs(
keys *encryptionKeys,
quotaQuerier logstore.QuotaQuerier,
usageReporter logstore.UsageReporter,
permissionCheck domain.PermissionCheck,
) error {
repo := struct {
authz_repo.Repository
Expand Down Expand Up @@ -294,7 +329,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil {
return err
}
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries, permissionCheck)); err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guides/integrate/access-zitadel-system-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ If your system is exposed without TLS or on a dedicated port, be sure to provide
If you want to manually create a JWT for a test, you can also use our [ZITADEL Tools](https://github.com/zitadel/zitadel-tools). Download the latest release and run:

```bash
./key2jwt -audience=https://custom-domain.com -key=system-user-1.pem -issuer=system-user-1
zitadel-tools key2jwt --audience=https://custom-domain.com --key=system-user-1.pem --issuer=system-user-1
```

## Call the System API
Expand Down
7 changes: 7 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ module.exports = {
sidebarOptions: {
groupPathsBy: "tag",
},
},
session: {
specPath: ".artifacts/openapi/zitadel/session/v2alpha/session_service.swagger.json",
outputDir: "docs/apis/session_service",
sidebarOptions: {
groupPathsBy: "tag",
},
}
}
},
Expand Down
14 changes: 14 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ module.exports = {
},
items: require("./docs/apis/user_service/sidebar.js"),
},
{
type: "category",
label: "Session Lifecycle (Alpha)",
link: {
type: "generated-index",
title: "Session Service API (Alpha)",
slug: "/apis/session_service",
description:
"This API is intended to manage sessions in a ZITADEL instance.\n"+
"\n"+
"This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.",
},
items: require("./docs/apis/session_service/sidebar.js"),
},
{
type: "category",
label: "Assets",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/sys v0.7.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
9 changes: 7 additions & 2 deletions internal/api/authz/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ const (
authenticated = "authenticated"
)

func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
// CheckUserAuthorization verifies that:
// - the token is active,
// - 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) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()

ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, method)
if err != nil {
return nil, err
}
Expand Down
11 changes: 6 additions & 5 deletions internal/api/authz/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const (
MemberTypeIam
)

func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *TokenVerifier, method string) (_ CtxData, err error) {
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t *TokenVerifier, method string) (_ CtxData, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

Expand All @@ -82,14 +82,15 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *To
if err := checkOrigin(ctx, origins); err != nil {
return CtxData{}, err
}
if orgID == "" {
if orgID == "" && orgDomain == "" {
orgID = resourceOwner
}

err = t.ExistsOrg(ctx, orgID)
verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
err = retry(func() error {
return t.ExistsOrg(ctx, orgID)
verifiedOrgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
return err
})
if err != nil {
return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
Expand All @@ -98,7 +99,7 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *To

return CtxData{
UserID: userID,
OrgID: orgID,
OrgID: verifiedOrgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
Expand Down
8 changes: 2 additions & 6 deletions internal/api/authz/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)

func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
ctxData := GetCtxData(ctx)
if allowSelf && ctxData.UserID == resourceID {
return nil
}
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/api/authz/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, client
return "", nil, nil
}

func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
return 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) {
Expand Down
28 changes: 24 additions & 4 deletions internal/api/authz/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package authz
import (
"context"
"crypto/rsa"
"encoding/base64"
"fmt"
"os"
"strings"
"sync"
Expand All @@ -17,7 +19,8 @@ import (
)

const (
BearerPrefix = "Bearer "
BearerPrefix = "Bearer "
SessionTokenFormat = "sess_%s:%s"
)

type TokenVerifier struct {
Expand All @@ -36,7 +39,7 @@ type authZRepo interface {
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, orgID string) error
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}

func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
Expand Down Expand Up @@ -144,10 +147,10 @@ func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clien
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}

func (v *TokenVerifier) ExistsOrg(ctx context.Context, orgID string) (err error) {
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, orgID)
return v.authZRepo.ExistsOrg(ctx, id, domain)
}

func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
Expand All @@ -165,3 +168,20 @@ func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, meth
}
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
}
}
19 changes: 19 additions & 0 deletions internal/api/grpc/object/v2/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
)

Expand All @@ -17,3 +18,21 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
}
return details
}

func ToListDetails(response query.SearchResponse) *object.ListDetails {
details := &object.ListDetails{
TotalResult: response.Count,
ProcessedSequence: response.Sequence,
}
if !response.Timestamp.IsZero() {
details.Timestamp = timestamppb.New(response.Timestamp)
}

return details
}
func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) {
if query == nil {
return 0, 0, false
}
return query.Offset, uint64(query.Limit), query.Asc
}
4 changes: 3 additions & 1 deletion internal/api/grpc/server/middleware/auth_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
return nil, status.Error(codes.Unauthenticated, "auth header missing")
}

var orgDomain string
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
if o, ok := req.(OrganisationFromRequest); ok {
orgID = o.OrganisationFromRequest().GetOrgId()
orgDomain = o.OrganisationFromRequest().GetOrgDomain()
}

ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/api/grpc/server/middleware/auth_interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([
func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error {
return nil
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
Expand Down
14 changes: 9 additions & 5 deletions internal/api/grpc/session/v2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
)

var _ session.SessionServiceServer = (*Server)(nil)

type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}

type Config struct{}

func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
checkPermission: checkPermission,
}
}

Expand Down

1 comment on commit c2cb84c

@vercel
Copy link

@vercel vercel bot commented on c2cb84c May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./

zitadel-docs.vercel.app
docs-git-main-zitadel.vercel.app
docs-zitadel.vercel.app

Please sign in to comment.