feat: project roles (#843)

* fix logging

* token verification

* feat: assert roles

* feat: add project role assertion on project and token type on app

* id and access token role assertion

* add project role check

* user grant required step in login

* update library

* fix merge

* fix merge

* fix merge

* update oidc library

* fix tests

* add tests for GrantRequiredStep

* add missing field ProjectRoleCheck on project view model

* fix project create

* fix project create
This commit is contained in:
Livio Amstutz 2020-10-16 07:49:38 +02:00 committed by GitHub
parent f5a7a0a09f
commit a321d850ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 10894 additions and 18297 deletions

3
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/aws/aws-sdk-go v1.34.24 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
github.com/caos/logging v0.0.2
github.com/caos/oidc v0.11.1
github.com/caos/oidc v0.12.0
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cockroachdb/cockroach-go/v2 v2.0.7
github.com/envoyproxy/protoc-gen-validate v0.4.1
@ -37,7 +37,6 @@ require (
github.com/kevinburke/go.uuid v1.2.0 // indirect
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
github.com/kevinburke/twilio-go v0.0.0-20200810163702-320748330fac
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/lib/pq v1.8.0
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect

19
go.sum
View File

@ -71,12 +71,8 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo=
github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
github.com/caos/oidc v0.10.0 h1:/GKyQgKHvkc2jNCXzmdJs9hCXDYdArCMm3d5FXaiNWA=
github.com/caos/oidc v0.10.0/go.mod h1:RREtWSRzH/mXQXJkxB63mFDZ/RUNyzoU6czd6UJfvJI=
github.com/caos/oidc v0.11.0 h1:larwR0ur4hcHXkMtlXbHApv4DUr5yu/zbxBeGjcpTXk=
github.com/caos/oidc v0.11.0/go.mod h1:R9UKITZmSo5vNhSLUYcTDH8pAaV2xwPASXg8wpEs2xQ=
github.com/caos/oidc v0.11.1 h1:7Nkup+fiU/zZVN61BfGOTzuloD7aOdqA3V9eNrJv5xc=
github.com/caos/oidc v0.11.1/go.mod h1:R9UKITZmSo5vNhSLUYcTDH8pAaV2xwPASXg8wpEs2xQ=
github.com/caos/oidc v0.12.0 h1:BcEwgeq8fpum2hdc47ZDOTwnc4KdHpos1NRbE1JcHN8=
github.com/caos/oidc v0.12.0/go.mod h1:R9UKITZmSo5vNhSLUYcTDH8pAaV2xwPASXg8wpEs2xQ=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -280,8 +276,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -349,8 +343,6 @@ github.com/shopspring/decimal v0.0.0-20200419222939-1884f454f8ea/go.mod h1:DKyhr
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
@ -409,8 +401,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -484,8 +474,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -546,9 +534,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zr
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a h1:bhXnJ7fn2SiL+C8iOWPfNBJKDTjUByftpPW7b9CX94U=
golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -52,8 +52,11 @@ func (m *IamMember) processIamMember(event *models.Event) (err error) {
member := new(iam_model.IAMMemberView)
switch event.Type {
case model.IAMMemberAdded:
member.AppendEvent(event)
m.fillData(member)
err = member.AppendEvent(event)
if err != nil {
return err
}
err = m.fillData(member)
case model.IAMMemberChanged:
err := member.SetData(event)
if err != nil {
@ -63,7 +66,7 @@ func (m *IamMember) processIamMember(event *models.Event) (err error) {
if err != nil {
return err
}
member.AppendEvent(event)
err = member.AppendEvent(event)
case model.IAMMemberRemoved:
err := member.SetData(event)
if err != nil {

View File

@ -35,7 +35,10 @@ func (o *Org) Reduce(event *es_models.Event) error {
switch event.Type {
case model.OrgAdded:
org.AppendEvent(event)
err := org.AppendEvent(event)
if err != nil {
return err
}
case model.OrgChanged:
err := org.SetData(event)
if err != nil {

View File

@ -56,6 +56,9 @@ func oidcConfigFromModel(config *proj_model.OIDCConfig) *management.OIDCConfig {
NoneCompliant: config.Compliance.NoneCompliant,
ComplianceProblems: complianceProblemsToLocalizedMessages(config.Compliance.Problems),
DevMode: config.DevMode,
AccessTokenType: oidcTokenTypeFromModel(config.AccessTokenType),
AccessTokenRoleAssertion: config.AccessTokenRoleAssertion,
IdTokenRoleAssertion: config.IDTokenRoleAssertion,
}
}
@ -72,6 +75,9 @@ func oidcConfigFromApplicationViewModel(app *proj_model.ApplicationView) *manage
NoneCompliant: app.NoneCompliant,
ComplianceProblems: complianceProblemsToLocalizedMessages(app.ComplianceProblems),
DevMode: app.DevMode,
AccessTokenType: oidcTokenTypeFromModel(app.AccessTokenType),
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
IdTokenRoleAssertion: app.IDTokenRoleAssertion,
}
}
@ -100,6 +106,9 @@ func oidcAppCreateToModel(app *management.OIDCApplicationCreate) *proj_model.App
AuthMethodType: oidcAuthMethodTypeToModel(app.AuthMethodType),
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
DevMode: app.DevMode,
AccessTokenType: oidcTokenTypeToModel(app.AccessTokenType),
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
},
}
}
@ -127,6 +136,9 @@ func oidcConfigUpdateToModel(app *management.OIDCConfigUpdate) *proj_model.OIDCC
AuthMethodType: oidcAuthMethodTypeToModel(app.AuthMethodType),
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
DevMode: app.DevMode,
AccessTokenType: oidcTokenTypeToModel(app.AccessTokenType),
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
}
}
@ -351,6 +363,28 @@ func oidcAuthMethodTypeFromModel(authType proj_model.OIDCAuthMethodType) managem
}
}
func oidcTokenTypeToModel(tokenType management.OIDCTokenType) proj_model.OIDCTokenType {
switch tokenType {
case management.OIDCTokenType_OIDCTokenType_Bearer:
return proj_model.OIDCTokenTypeBearer
case management.OIDCTokenType_OIDCTokenType_JWT:
return proj_model.OIDCTokenTypeJWT
default:
return proj_model.OIDCTokenTypeBearer
}
}
func oidcTokenTypeFromModel(tokenType proj_model.OIDCTokenType) management.OIDCTokenType {
switch tokenType {
case proj_model.OIDCTokenTypeBearer:
return management.OIDCTokenType_OIDCTokenType_Bearer
case proj_model.OIDCTokenTypeJWT:
return management.OIDCTokenType_OIDCTokenType_JWT
default:
return management.OIDCTokenType_OIDCTokenType_Bearer
}
}
func oidcVersionFromModel(version proj_model.OIDCVersion) management.OIDCVersion {
switch version {
case proj_model.OIDCVersionV1:

View File

@ -12,7 +12,7 @@ import (
)
func (s *Server) CreateProject(ctx context.Context, in *management.ProjectCreateRequest) (*management.Project, error) {
project, err := s.project.CreateProject(ctx, in.Name)
project, err := s.project.CreateProject(ctx, projectCreateToModel(in))
if err != nil {
return nil, err
}

View File

@ -28,6 +28,8 @@ func projectFromModel(project *proj_model.Project) *management.Project {
ChangeDate: changeDate,
Name: project.Name,
Sequence: project.Sequence,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
}
}
@ -67,6 +69,8 @@ func projectViewFromModel(project *proj_model.ProjectView) *management.ProjectVi
Name: project.Name,
Sequence: project.Sequence,
ResourceOwner: project.ResourceOwner,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
}
}
@ -117,12 +121,22 @@ func projectStateFromModel(state proj_model.ProjectState) management.ProjectStat
}
}
func projectCreateToModel(project *management.ProjectCreateRequest) *proj_model.Project {
return &proj_model.Project{
Name: project.Name,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
}
}
func projectUpdateToModel(project *management.ProjectUpdateRequest) *proj_model.Project {
return &proj_model.Project{
ObjectRoot: models.ObjectRoot{
AggregateID: project.Id,
},
Name: project.Name,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
}
}

View File

@ -135,7 +135,7 @@ func userGrantViewFromModel(grant *grant_model.UserGrantView) *management.UserGr
Email: grant.Email,
ProjectName: grant.ProjectName,
OrgName: grant.OrgName,
OrgDomain: grant.OrgDomain,
OrgDomain: grant.OrgPrimaryDomain,
RoleKeys: grant.RoleKeys,
UserId: grant.UserID,
ProjectId: grant.ProjectID,

View File

@ -3,6 +3,7 @@ package oidc
import (
"context"
"fmt"
"strings"
"time"
"github.com/caos/oidc/pkg/oidc"
@ -11,6 +12,7 @@ import (
"github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/errors"
proj_model "github.com/caos/zitadel/internal/project/model"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
)
@ -19,6 +21,14 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest
if !ok {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id")
}
app, err := o.repo.ApplicationByClientID(ctx, req.ClientID)
if err != nil {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-AEG4d", "Errors.Internal")
}
req.Scopes, err = o.assertProjectRoleScopes(app, req.Scopes)
if err != nil {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-Gqrfg", "Errors.Internal")
}
authRequest := CreateAuthRequestToBusiness(ctx, req, userAgentID, userID)
resp, err := o.repo.CreateAuthRequest(ctx, authRequest)
if err != nil {
@ -102,3 +112,22 @@ func (o *OPStorage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error)
func (o *OPStorage) SaveNewKeyPair(ctx context.Context) error {
return o.repo.GenerateSigningKeyPair(ctx, o.signingKeyAlgorithm)
}
func (o *OPStorage) assertProjectRoleScopes(app *proj_model.ApplicationView, scopes []string) ([]string, error) {
if !app.ProjectRoleAssertion {
return scopes, nil
}
for _, scope := range scopes {
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
return scopes, nil
}
}
roles, err := o.repo.ProjectRolesByProjectID(app.ProjectID)
if err != nil {
return nil, err
}
for _, role := range roles {
scopes = append(scopes, ScopeProjectRolePrefix+role.Key)
}
return scopes, nil
}

View File

@ -2,6 +2,7 @@ package oidc
import (
"context"
"strings"
"golang.org/x/text/language"
"gopkg.in/square/go-jose.v2"
@ -15,6 +16,7 @@ import (
"github.com/caos/zitadel/internal/errors"
proj_model "github.com/caos/zitadel/internal/project/model"
user_model "github.com/caos/zitadel/internal/user/model"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
)
const (
@ -24,6 +26,9 @@ const (
scopePhone = "phone"
scopeAddress = "address"
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
oidcCtx = "oidc"
)
@ -35,7 +40,15 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (op.Clie
if client.State != proj_model.AppStateActive {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active")
}
return ClientFromBusiness(client, o.defaultLoginURL, o.defaultAccessTokenLifetime, o.defaultIdTokenLifetime)
projectRoles, err := o.repo.ProjectRolesByProjectID(client.ProjectID)
if err != nil {
return nil, err
}
allowedScopes := make([]string, len(projectRoles))
for i, role := range projectRoles {
allowedScopes[i] = ScopeProjectRolePrefix + role.Key
}
return ClientFromBusiness(client, o.defaultLoginURL, o.defaultAccessTokenLifetime, o.defaultIdTokenLifetime, allowedScopes)
}
func (o *OPStorage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) {
@ -65,10 +78,10 @@ func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secr
return o.repo.AuthorizeOIDCApplication(ctx, id, secret)
}
func (o *OPStorage) GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (*oidc.Userinfo, error) {
token, err := o.repo.TokenByID(ctx, tokenID, subject)
func (o *OPStorage) GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) {
token, err := o.repo.TokenByID(ctx, subject, tokenID)
if err != nil {
return nil, err
return nil, errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
}
if token.ApplicationID != "" {
app, err := o.repo.ApplicationByClientID(ctx, token.ApplicationID)
@ -79,63 +92,124 @@ func (o *OPStorage) GetUserinfoFromToken(ctx context.Context, tokenID, subject,
return nil, errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
}
}
return o.GetUserinfoFromScopes(ctx, token.UserID, token.Scopes)
return o.GetUserinfoFromScopes(ctx, token.UserID, token.ApplicationID, token.Scopes)
}
func (o *OPStorage) GetUserinfoFromScopes(ctx context.Context, userID string, scopes []string) (*oidc.Userinfo, error) {
func (o *OPStorage) GetUserinfoFromScopes(ctx context.Context, userID, applicationID string, scopes []string) (oidc.UserInfo, error) {
user, err := o.repo.UserByID(ctx, userID)
if err != nil {
return nil, err
}
userInfo := new(oidc.Userinfo)
userInfo := oidc.NewUserInfo()
roles := make([]string, 0)
for _, scope := range scopes {
switch scope {
case scopeOpenID:
userInfo.Subject = user.ID
case scopeEmail:
case oidc.ScopeOpenID:
userInfo.SetSubject(user.ID)
case oidc.ScopeEmail:
if user.HumanView == nil {
continue
}
userInfo.Email = user.Email
userInfo.EmailVerified = user.IsEmailVerified
case scopeProfile:
userInfo.PreferredUsername = user.PreferredLoginName
userInfo.UpdatedAt = user.ChangeDate
userInfo.SetEmail(user.Email, user.IsEmailVerified)
case oidc.ScopeProfile:
userInfo.SetPreferredUsername(user.PreferredLoginName)
userInfo.SetUpdatedAt(user.ChangeDate)
if user.HumanView != nil {
userInfo.Name = user.DisplayName
userInfo.FamilyName = user.LastName
userInfo.GivenName = user.FirstName
userInfo.Nickname = user.NickName
userInfo.Gender = oidc.Gender(getGender(user.Gender))
userInfo.Locale, err = language.Parse(user.PreferredLanguage)
userInfo.SetName(user.DisplayName)
userInfo.SetFamilyName(user.LastName)
userInfo.SetGivenName(user.FirstName)
userInfo.SetNickname(user.NickName)
userInfo.SetGender(oidc.Gender(getGender(user.Gender)))
locale, _ := language.Parse(user.PreferredLanguage)
userInfo.SetLocale(locale)
} else {
userInfo.Name = user.MachineView.Name
userInfo.SetName(user.MachineView.Name)
}
case scopePhone:
case oidc.ScopePhone:
if user.HumanView == nil {
continue
}
userInfo.PhoneNumber = user.Phone
userInfo.PhoneNumberVerified = user.IsPhoneVerified
case scopeAddress:
userInfo.SetPhone(user.Phone, user.IsPhoneVerified)
case oidc.ScopeAddress:
if user.HumanView == nil {
continue
}
if user.StreetAddress == "" && user.Locality == "" && user.Region == "" && user.PostalCode == "" && user.Country == "" {
continue
}
userInfo.Address = &oidc.UserinfoAddress{
StreetAddress: user.StreetAddress,
Locality: user.Locality,
Region: user.Region,
PostalCode: user.PostalCode,
Country: user.Country,
}
userInfo.SetAddress(oidc.NewUserInfoAddress(user.StreetAddress, user.Locality, user.Region, user.PostalCode, user.Country, ""))
default:
userInfo.Authorizations = append(userInfo.Authorizations, scope)
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
}
}
}
if len(roles) == 0 || applicationID == "" {
return userInfo, nil
}
projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles)
if err != nil {
return nil, err
}
if len(projectRoles) > 0 {
userInfo.AppendClaims(ClaimProjectRoles, projectRoles)
}
return userInfo, nil
}
func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, applicationID string, scopes []string) (claims map[string]interface{}, err error) {
roles := make([]string, 0)
for _, scope := range scopes {
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
}
}
if len(roles) == 0 || applicationID == "" {
return nil, nil
}
projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles)
if err != nil {
return nil, err
}
if len(projectRoles) > 0 {
claims = map[string]interface{}{ClaimProjectRoles: projectRoles}
}
return claims, err
}
func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles []string) (map[string]map[string]string, error) {
app, err := o.repo.ApplicationByClientID(ctx, applicationID)
if err != nil {
return nil, err
}
grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, userID)
if err != nil {
return nil, err
}
projectRoles := make(map[string]map[string]string)
for _, requestedRole := range requestedRoles {
for _, grant := range grants {
checkGrantedRoles(projectRoles, grant, requestedRole)
}
}
return projectRoles, nil
}
func checkGrantedRoles(roles map[string]map[string]string, grant *grant_model.UserGrantView, requestedRole string) {
for _, grantedRole := range grant.RoleKeys {
if requestedRole == grantedRole {
appendRole(roles, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain)
}
}
}
func appendRole(roles map[string]map[string]string, role, orgID, orgPrimaryDomain string) {
if roles[role] == nil {
roles[role] = make(map[string]string, 0)
}
roles[role][orgID] = orgPrimaryDomain
}
func getGender(gender user_model.Gender) string {

View File

@ -1,9 +1,9 @@
package oidc
import (
"github.com/caos/oidc/pkg/oidc"
"time"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op"
"github.com/caos/zitadel/internal/errors"
@ -15,13 +15,20 @@ type Client struct {
defaultLoginURL string
defaultAccessTokenLifetime time.Duration
defaultIdTokenLifetime time.Duration
allowedScopes []string
}
func ClientFromBusiness(app *model.ApplicationView, defaultLoginURL string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration) (op.Client, error) {
func ClientFromBusiness(app *model.ApplicationView, defaultLoginURL string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) {
if !app.IsOIDC {
return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application")
}
return &Client{ApplicationView: app, defaultLoginURL: defaultLoginURL, defaultAccessTokenLifetime: defaultAccessTokenLifetime, defaultIdTokenLifetime: defaultIdTokenLifetime}, nil
return &Client{
ApplicationView: app,
defaultLoginURL: defaultLoginURL,
defaultAccessTokenLifetime: defaultAccessTokenLifetime,
defaultIdTokenLifetime: defaultIdTokenLifetime,
allowedScopes: allowedScopes},
nil
}
func (c *Client) ApplicationType() op.ApplicationType {
@ -56,6 +63,18 @@ func (c *Client) DevMode() bool {
return c.ApplicationView.DevMode
}
func (c *Client) AllowedScopes() []string {
return c.allowedScopes
}
func (c *Client) AssertAdditionalIdTokenScopes() bool {
return c.IDTokenRoleAssertion
}
func (c *Client) AssertAdditionalAccessTokenScopes() bool {
return c.AccessTokenRoleAssertion
}
func (c *Client) AccessTokenLifetime() time.Duration {
return c.defaultAccessTokenLifetime //PLANNED: impl from real client
}
@ -65,7 +84,18 @@ func (c *Client) IDTokenLifetime() time.Duration {
}
func (c *Client) AccessTokenType() op.AccessTokenType {
return op.AccessTokenTypeBearer //PLANNED: impl from real client
return accessTokenTypeToOIDC(c.ApplicationView.AccessTokenType)
}
func accessTokenTypeToOIDC(tokenType model.OIDCTokenType) op.AccessTokenType {
switch tokenType {
case model.OIDCTokenTypeBearer:
return op.AccessTokenTypeBearer
case model.OIDCTokenTypeJWT:
return op.AccessTokenTypeJWT
default:
return op.AccessTokenTypeBearer
}
}
func authMethodToOIDC(authType model.OIDCAuthMethodType) op.AuthMethod {

View File

@ -2,28 +2,30 @@ package eventstore
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/eventstore/sdk"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
org_event "github.com/caos/zitadel/internal/org/repository/eventsourcing"
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/auth_request/model"
cache "github.com/caos/zitadel/internal/auth_request/repository"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/sdk"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/id"
org_model "github.com/caos/zitadel/internal/org/model"
org_event "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_view_model "github.com/caos/zitadel/internal/org/repository/view/model"
project_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
user_model "github.com/caos/zitadel/internal/user/model"
user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
user_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
grant_view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
)
type AuthRequestRepo struct {
@ -38,6 +40,7 @@ type AuthRequestRepo struct {
OrgViewProvider orgViewProvider
LoginPolicyViewProvider loginPolicyViewProvider
IDPProviderViewProvider idpProviderViewProvider
UserGrantProvider userGrantProvider
IdGenerator id.Generator
@ -76,6 +79,11 @@ type orgViewProvider interface {
OrgByPrimaryDomain(string) (*org_view_model.OrgView, error)
}
type userGrantProvider interface {
ApplicationByClientID(context.Context, string) (*project_view_model.ApplicationView, error)
UserGrantsByProjectAndUserID(string, string) ([]*grant_view_model.UserGrantView, error)
}
func (repo *AuthRequestRepo) Health(ctx context.Context) error {
if err := repo.UserEvents.Health(ctx); err != nil {
return err
@ -89,14 +97,18 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod
return nil, err
}
request.ID = reqID
ids, err := repo.View.AppIDsFromProjectByClientID(ctx, request.ApplicationID)
app, err := repo.View.ApplicationByClientID(ctx, request.ApplicationID)
if err != nil {
return nil, err
}
request.Audience = ids
appIDs, err := repo.View.AppIDsFromProjectID(ctx, app.ProjectID)
if err != nil {
return nil, err
}
request.Audience = appIDs
if request.LoginHint != "" {
err = repo.checkLoginName(ctx, request, request.LoginHint)
logging.LogWithFields("EVENT-aG311", "login name", request.LoginHint, "id", request.ID, "applicationID", request.ApplicationID).Debug("login hint invalid")
logging.LogWithFields("EVENT-aG311", "login name", request.LoginHint, "id", request.ID, "applicationID", request.ApplicationID).OnError(err).Debug("login hint invalid")
}
err = repo.AuthRequests.SaveAuthRequest(ctx, request)
if err != nil {
@ -541,6 +553,15 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
}
//PLANNED: consent step
missing, err := userGrantRequired(ctx, request, user, repo.UserGrantProvider)
if err != nil {
return nil, err
}
if missing {
return append(steps, &model.GrantRequiredStep{}), nil
}
return append(steps, &model.RedirectToCallbackStep{}), nil
}
@ -780,3 +801,23 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*model.ExternalUser, i
}
return true
}
func userGrantRequired(ctx context.Context, request *model.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
var app *project_view_model.ApplicationView
switch request.Request.Type() {
case model.AuthRequestTypeOIDC:
app, err = userGrantProvider.ApplicationByClientID(ctx, request.ApplicationID)
if err != nil {
return false, err
}
default:
return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported")
}
if !app.ProjectRoleCheck {
return false, nil
}
grants, err := userGrantProvider.UserGrantsByProjectAndUserID(app.ProjectID, user.ID)
if err != nil {
return false, err
}
return len(grants) == 0, nil
}

View File

@ -15,10 +15,12 @@ import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
org_model "github.com/caos/zitadel/internal/org/model"
org_view_model "github.com/caos/zitadel/internal/org/repository/view/model"
proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
user_model "github.com/caos/zitadel/internal/user/model"
user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
user_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
user_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
grant_view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
)
type mockViewNoUserSession struct{}
@ -157,6 +159,23 @@ func (m *mockViewErrOrg) OrgByPrimaryDomain(string) (*org_view_model.OrgView, er
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
type mockUserGrants struct {
roleCheck bool
userGrants int
}
func (m *mockUserGrants) ApplicationByClientID(ctx context.Context, s string) (*proj_view_model.ApplicationView, error) {
return &proj_view_model.ApplicationView{ProjectRoleCheck: m.roleCheck}, nil
}
func (m *mockUserGrants) UserGrantsByProjectAndUserID(s string, s2 string) ([]*grant_view_model.UserGrantView, error) {
var grants []*grant_view_model.UserGrantView
if m.userGrants > 0 {
grants = make([]*grant_view_model.UserGrantView, m.userGrants)
}
return grants, nil
}
func TestAuthRequestRepo_nextSteps(t *testing.T) {
type fields struct {
UserEvents *user_event.UserEventstore
@ -166,6 +185,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider userViewProvider
userEventProvider userEventProvider
orgViewProvider orgViewProvider
userGrantProvider userGrantProvider
PasswordCheckLifeTime time.Duration
ExternalLoginCheckLifeTime time.Duration
MfaInitSkippedLifeTime time.Duration
@ -424,10 +444,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
ExternalLoginCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID", Request: &model.AuthRequestOIDC{}}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
@ -460,10 +481,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
MfaSoftwareCheckLifeTime: 18 * time.Hour,
ExternalLoginCheckLifeTime: 10 * 24 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID", Request: &model.AuthRequestOIDC{}}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
@ -590,10 +612,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID"}, false},
args{&model.AuthRequest{UserID: "UserID", Request: &model.AuthRequestOIDC{}}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
@ -611,10 +634,61 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", Prompt: model.PromptNone}, true},
args{&model.AuthRequest{UserID: "UserID", Prompt: model.PromptNone, Request: &model.AuthRequestOIDC{}}, true},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
{
"prompt none, checkLoggedIn true, authenticated and required user grants missing, grant required step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{
roleCheck: true,
userGrants: 0,
},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", Prompt: model.PromptNone, Request: &model.AuthRequestOIDC{}}, true},
[]model.NextStep{&model.GrantRequiredStep{}},
nil,
},
{
"prompt none, checkLoggedIn true, authenticated and required user grants exist, redirect to callback step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{
roleCheck: true,
userGrants: 2,
},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", Prompt: model.PromptNone, Request: &model.AuthRequestOIDC{}}, true},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
@ -679,6 +753,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserViewProvider: tt.fields.userViewProvider,
UserEventProvider: tt.fields.userEventProvider,
OrgViewProvider: tt.fields.orgViewProvider,
UserGrantProvider: tt.fields.userGrantProvider,
PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime,
ExternalLoginCheckLifeTime: tt.fields.ExternalLoginCheckLifeTime,
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime,

View File

@ -0,0 +1,21 @@
package eventstore
import (
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/project/model"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
)
type ProjectRepo struct {
View *view.View
ProjectEvents *proj_event.ProjectEventstore
}
func (a *ApplicationRepo) ProjectRolesByProjectID(projectID string) ([]*model.ProjectRoleView, error) {
roles, err := a.View.ProjectRolesByProjectID(projectID)
if err != nil {
return nil, err
}
return proj_view_model.ProjectRolesToModel(roles), nil
}

View File

@ -52,11 +52,27 @@ func (p *Application) Reduce(event *models.Event) (err error) {
}
err = app.AppendEvent(event)
case es_model.ApplicationRemoved:
err := app.SetData(event)
err = app.SetData(event)
if err != nil {
return err
}
return p.view.DeleteApplication(app.ID, event.Sequence)
case es_model.ProjectChanged:
apps, err := p.view.ApplicationsByProjectID(event.AggregateID)
if err != nil {
return err
}
if len(apps) == 0 {
return p.view.ProcessedApplicationSequence(event.Sequence)
}
for _, app := range apps {
if err := app.AppendEvent(event); err != nil {
return err
}
}
return p.view.PutApplications(apps, event.Sequence)
case es_model.ProjectRemoved:
return p.view.DeleteApplicationsByProjectID(event.AggregateID)
default:
return p.view.ProcessedApplicationSequence(event.Sequence)
}

View File

@ -60,6 +60,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
&ExternalIDP{handler: handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount}, systemDefaults: systemDefaults, orgEvents: repos.OrgEvents, iamEvents: repos.IamEvents},
&PasswordComplexityPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("PasswordComplexityPolicy"), errorCount}},
&OrgIAMPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("OrgIAMPolicy"), errorCount}},
&ProjectRole{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount}, projectEvents: repos.ProjectEvents},
}
}

View File

@ -0,0 +1,70 @@
package handler
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
)
type ProjectRole struct {
handler
projectEvents *proj_event.ProjectEventstore
}
const (
projectRoleTable = "auth.project_roles"
)
func (p *ProjectRole) ViewModel() string {
return projectRoleTable
}
func (p *ProjectRole) EventQuery() (*models.SearchQuery, error) {
sequence, err := p.view.GetLatestProjectRoleSequence()
if err != nil {
return nil, err
}
return eventsourcing.ProjectQuery(sequence.CurrentSequence), nil
}
func (p *ProjectRole) Reduce(event *models.Event) (err error) {
role := new(view_model.ProjectRoleView)
switch event.Type {
case es_model.ProjectRoleAdded:
err = role.AppendEvent(event)
case es_model.ProjectRoleChanged:
err = role.SetData(event)
if err != nil {
return err
}
role, err = p.view.ProjectRoleByIDs(event.AggregateID, event.ResourceOwner, role.Key)
if err != nil {
return err
}
err = role.AppendEvent(event)
case es_model.ProjectRoleRemoved:
err = role.SetData(event)
if err != nil {
return err
}
return p.view.DeleteProjectRole(event.AggregateID, event.ResourceOwner, role.Key, event.Sequence)
case es_model.ProjectRemoved:
return p.view.DeleteProjectRolesByProjectID(event.AggregateID)
default:
return p.view.ProcessedProjectRoleSequence(event.Sequence)
}
if err != nil {
return err
}
return p.view.PutProjectRole(role)
}
func (p *ProjectRole) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-lso9w", "id", event.AggregateID).WithError(err).Warn("something went wrong in project role handler")
return spooler.HandleError(event, err, p.view.GetLatestProjectRoleFailedEvent, p.view.ProcessedProjectRoleFailedEvent, p.view.ProcessedProjectRoleSequence, p.errorCountUntilSkip)
}

View File

@ -354,6 +354,12 @@ func (u *UserGrant) fillProjectData(grant *view_model.UserGrantView, project *pr
func (u *UserGrant) fillOrgData(grant *view_model.UserGrantView, org *org_model.Org) {
grant.OrgName = org.Name
for _, domain := range org.Domains {
if domain.Primary {
grant.OrgPrimaryDomain = domain.Domain
break
}
}
}
func (u *UserGrant) OnError(event *models.Event, err error) error {

View File

@ -107,7 +107,7 @@ func (m *UserMembership) processOrg(event *models.Event) (err error) {
case org_es_model.OrgMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeOrganisation, event.Sequence)
case org_es_model.OrgChanged:
err = m.updateOrgName(event)
return m.updateOrgName(event)
default:
return m.view.ProcessedUserMembershipSequence(event.Sequence)
}
@ -178,7 +178,7 @@ func (m *UserMembership) processProject(event *models.Event) (err error) {
case proj_es_model.ProjectGrantMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, member.ObjectID, usr_model.MemberTypeProjectGrant, event.Sequence)
case proj_es_model.ProjectChanged:
err = m.updateProjectDisplayName(event)
return m.updateProjectDisplayName(event)
case proj_es_model.ProjectRemoved:
return m.view.DeleteUserMembershipsByAggregateID(event.AggregateID, event.Sequence)
case proj_es_model.ProjectGrantRemoved:
@ -227,6 +227,6 @@ func (m *UserMembership) processUser(event *models.Event) (err error) {
}
func (m *UserMembership) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-Ms3fj", "id", event.AggregateID).WithError(err).Warn("something went wrong in orgmember handler")
logging.LogWithFields("SPOOL-Ms3fj", "id", event.AggregateID).WithError(err).Warn("something went wrong in user membership handler")
return spooler.HandleError(event, err, m.view.GetLatestUserMembershipFailedEvent, m.view.ProcessedUserMembershipFailedEvent, m.view.ProcessedUserMembershipSequence, m.errorCountUntilSkip)
}

View File

@ -135,6 +135,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
OrgViewProvider: view,
IDPProviderViewProvider: view,
LoginPolicyViewProvider: view,
UserGrantProvider: view,
IdGenerator: idGenerator,
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
@ -156,6 +157,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
View: view,
ProjectEvents: project,
},
eventstore.UserSessionRepo{
View: view,
},

View File

@ -18,16 +18,28 @@ func (v *View) ApplicationByID(projectID, appID string) (*model.ApplicationView,
return view.ApplicationByID(v.Db, applicationTable, projectID, appID)
}
func (v *View) ApplicationsByProjectID(projectID string) ([]*model.ApplicationView, error) {
return view.ApplicationsByProjectID(v.Db, applicationTable, projectID)
}
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) {
return view.SearchApplications(v.Db, applicationTable, request)
}
func (v *View) PutApplication(project *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, project)
func (v *View) PutApplication(app *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, app)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(project.Sequence)
return v.ProcessedApplicationSequence(app.Sequence)
}
func (v *View) PutApplications(apps []*model.ApplicationView, sequence uint64) error {
err := view.PutApplications(v.Db, applicationTable, apps...)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(sequence)
}
func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
@ -38,6 +50,10 @@ func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
return v.ProcessedApplicationSequence(eventSequence)
}
func (v *View) DeleteApplicationsByProjectID(projectID string) error {
return view.DeleteApplicationsByProjectID(v.Db, applicationTable, projectID)
}
func (v *View) GetLatestApplicationSequence() (*repository.CurrentSequence, error) {
return v.latestSequence(applicationTable)
}
@ -55,24 +71,7 @@ func (v *View) ProcessedApplicationFailedEvent(failedEvent *repository.FailedEve
}
func (v *View) ApplicationByClientID(_ context.Context, clientID string) (*model.ApplicationView, error) {
req := &proj_model.ApplicationSearchRequest{
Limit: 1,
Queries: []*proj_model.ApplicationSearchQuery{
{
Key: proj_model.AppSearchKeyOIDCClientID,
Method: global_model.SearchMethodEquals,
Value: clientID,
},
},
}
apps, count, err := view.SearchApplications(v.Db, applicationTable, req)
if err != nil {
return nil, errors.ThrowPreconditionFailed(err, "VIEW-sd6JQ", "cannot find client")
}
if count != 1 {
return nil, errors.ThrowPreconditionFailed(nil, "VIEW-dfw3as", "cannot find client")
}
return apps[0], nil
return view.ApplicationByOIDCClientID(v.Db, applicationTable, clientID)
}
func (v *View) AppIDsFromProjectByClientID(ctx context.Context, clientID string) ([]string, error) {
@ -102,3 +101,27 @@ func (v *View) AppIDsFromProjectByClientID(ctx context.Context, clientID string)
}
return ids, nil
}
func (v *View) AppIDsFromProjectID(ctx context.Context, projectID string) ([]string, error) {
req := &proj_model.ApplicationSearchRequest{
Queries: []*proj_model.ApplicationSearchQuery{
{
Key: proj_model.AppSearchKeyProjectID,
Method: global_model.SearchMethodEquals,
Value: projectID,
},
},
}
apps, _, err := view.SearchApplications(v.Db, applicationTable, req)
if err != nil {
return nil, errors.ThrowPreconditionFailed(err, "VIEW-Gd24q", "cannot find applications")
}
ids := make([]string, 0, len(apps))
for _, app := range apps {
if !app.IsOIDC {
continue
}
ids = append(ids, app.OIDCClientID)
}
return ids, nil
}

View File

@ -0,0 +1,68 @@
package view
import (
proj_model "github.com/caos/zitadel/internal/project/model"
"github.com/caos/zitadel/internal/project/repository/view"
"github.com/caos/zitadel/internal/project/repository/view/model"
"github.com/caos/zitadel/internal/view/repository"
)
const (
projectRoleTable = "auth.project_roles"
)
func (v *View) ProjectRoleByIDs(projectID, orgID, key string) (*model.ProjectRoleView, error) {
return view.ProjectRoleByIDs(v.Db, projectRoleTable, projectID, orgID, key)
}
func (v *View) ProjectRolesByProjectID(projectID string) ([]*model.ProjectRoleView, error) {
return view.ProjectRolesByProjectID(v.Db, projectRoleTable, projectID)
}
func (v *View) ResourceOwnerProjectRolesByKey(projectID, resourceowner, key string) ([]*model.ProjectRoleView, error) {
return view.ResourceOwnerProjectRolesByKey(v.Db, projectRoleTable, projectID, resourceowner, key)
}
func (v *View) ResourceOwnerProjectRoles(projectID, resourceowner string) ([]*model.ProjectRoleView, error) {
return view.ResourceOwnerProjectRoles(v.Db, projectRoleTable, projectID, resourceowner)
}
func (v *View) SearchProjectRoles(request *proj_model.ProjectRoleSearchRequest) ([]*model.ProjectRoleView, uint64, error) {
return view.SearchProjectRoles(v.Db, projectRoleTable, request)
}
func (v *View) PutProjectRole(project *model.ProjectRoleView) error {
err := view.PutProjectRole(v.Db, projectRoleTable, project)
if err != nil {
return err
}
return v.ProcessedProjectRoleSequence(project.Sequence)
}
func (v *View) DeleteProjectRole(projectID, orgID, key string, eventSequence uint64) error {
err := view.DeleteProjectRole(v.Db, projectRoleTable, projectID, orgID, key)
if err != nil {
return nil
}
return v.ProcessedProjectRoleSequence(eventSequence)
}
func (v *View) DeleteProjectRolesByProjectID(projectID string) error {
return view.DeleteProjectRolesByProjectID(v.Db, projectRoleTable, projectID)
}
func (v *View) GetLatestProjectRoleSequence() (*repository.CurrentSequence, error) {
return v.latestSequence(projectRoleTable)
}
func (v *View) ProcessedProjectRoleSequence(eventSequence uint64) error {
return v.saveCurrentSequence(projectRoleTable, eventSequence)
}
func (v *View) GetLatestProjectRoleFailedEvent(sequence uint64) (*repository.FailedEvent, error) {
return v.latestFailedEvent(projectRoleTable, sequence)
}
func (v *View) ProcessedProjectRoleFailedEvent(failedEvent *repository.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -36,7 +36,7 @@ func (v *View) PutUserMembership(membership *model.UserMembershipView, sequence
}
func (v *View) BulkPutUserMemberships(memberships []*model.UserMembershipView, sequence uint64) error {
err := view.PutUserMemberships(v.Db, userTable, memberships...)
err := view.PutUserMemberships(v.Db, userMembershipTable, memberships...)
if err != nil {
return err
}

View File

@ -0,0 +1,9 @@
package repository
import (
"github.com/caos/zitadel/internal/project/model"
)
type ProjectRepository interface {
ProjectRolesByProjectID(projectID string) ([]*model.ProjectRoleView, error)
}

View File

@ -10,6 +10,7 @@ type Repository interface {
AuthRequestRepository
TokenRepository
ApplicationRepository
ProjectRepository
KeyRepository
UserSessionRepository
UserGrantRepository

View File

@ -22,6 +22,7 @@ const (
NextStepLinkUsers
NextStepExternalNotFoundOption
NextStepExternalLogin
NextStepGrantRequired
)
type UserSessionState int32
@ -127,6 +128,12 @@ func (s *LinkUsersStep) Type() NextStepType {
return NextStepLinkUsers
}
type GrantRequiredStep struct{}
func (s *GrantRequiredStep) Type() NextStepType {
return NextStepGrantRequired
}
type RedirectToCallbackStep struct{}
func (s *RedirectToCallbackStep) Type() NextStepType {

View File

@ -68,13 +68,12 @@ func (repo *ProjectRepo) ProjectByID(ctx context.Context, id string) (*proj_mode
return model.ProjectToModel(project), nil
}
func (repo *ProjectRepo) CreateProject(ctx context.Context, name string) (*proj_model.Project, error) {
func (repo *ProjectRepo) CreateProject(ctx context.Context, project *proj_model.Project) (*proj_model.Project, error) {
ctxData := authz.GetCtxData(ctx)
iam, err := repo.IAMEvents.IAMByID(ctx, repo.IAMID)
if err != nil {
return nil, err
}
project := &proj_model.Project{Name: name}
return repo.ProjectEvents.CreateProject(ctx, project, iam.GlobalOrgID == ctxData.OrgID)
}

View File

@ -57,6 +57,20 @@ func (p *Application) Reduce(event *models.Event) (err error) {
return err
}
return p.view.DeleteApplication(app.ID, event.Sequence)
case es_model.ProjectChanged:
apps, err := p.view.ApplicationsByProjectID(event.AggregateID)
if err != nil {
return err
}
if len(apps) == 0 {
return p.view.ProcessedApplicationSequence(event.Sequence)
}
for _, app := range apps {
if err := app.AppendEvent(event); err != nil {
return err
}
}
return p.view.PutApplications(apps, event.Sequence)
case es_model.ProjectRemoved:
return p.view.DeleteApplicationsByProjectID(event.AggregateID)
default:

View File

@ -180,6 +180,12 @@ func (u *UserGrant) fillProjectData(grant *view_model.UserGrantView, project *pr
func (u *UserGrant) fillOrgData(grant *view_model.UserGrantView, org *org_model.Org) {
grant.OrgName = org.Name
for _, domain := range org.Domains {
if domain.Primary {
grant.OrgPrimaryDomain = domain.Domain
break
}
}
}
func (u *UserGrant) OnError(event *models.Event, err error) error {

View File

@ -105,7 +105,7 @@ func (m *UserMembership) processOrg(event *models.Event) (err error) {
case org_es_model.OrgMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeOrganisation, event.Sequence)
case org_es_model.OrgChanged:
err = m.updateOrgDisplayName(event)
return m.updateOrgDisplayName(event)
default:
return m.view.ProcessedUserMembershipSequence(event.Sequence)
}
@ -166,7 +166,7 @@ func (m *UserMembership) processProject(event *models.Event) (err error) {
case proj_es_model.ProjectGrantMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, member.ObjectID, usr_model.MemberTypeProjectGrant, event.Sequence)
case proj_es_model.ProjectChanged:
err = m.updateProjectDisplayName(event)
return m.updateProjectDisplayName(event)
case proj_es_model.ProjectRemoved:
return m.view.DeleteUserMembershipsByAggregateID(event.AggregateID, event.Sequence)
case proj_es_model.ProjectGrantRemoved:
@ -215,6 +215,6 @@ func (m *UserMembership) processUser(event *models.Event) (err error) {
}
func (m *UserMembership) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-Ms3fj", "id", event.AggregateID).WithError(err).Warn("something went wrong in orgmember handler")
logging.LogWithFields("SPOOL-Fwer2", "id", event.AggregateID).WithError(err).Warn("something went wrong in user membership handler")
return spooler.HandleError(event, err, m.view.GetLatestUserMembershipFailedEvent, m.view.ProcessedUserMembershipFailedEvent, m.view.ProcessedUserMembershipSequence, m.errorCountUntilSkip)
}

View File

@ -15,20 +15,28 @@ func (v *View) ApplicationByID(projectID, appID string) (*model.ApplicationView,
return view.ApplicationByID(v.Db, applicationTable, projectID, appID)
}
func (v *View) ApplicationsByProjectID(ProjectID string) ([]*model.ApplicationView, error) {
return view.ApplicationsByProjectID(v.Db, applicationTable, ProjectID)
func (v *View) ApplicationsByProjectID(projectID string) ([]*model.ApplicationView, error) {
return view.ApplicationsByProjectID(v.Db, applicationTable, projectID)
}
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) {
return view.SearchApplications(v.Db, applicationTable, request)
}
func (v *View) PutApplication(project *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, project)
func (v *View) PutApplication(app *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, app)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(project.Sequence)
return v.ProcessedApplicationSequence(app.Sequence)
}
func (v *View) PutApplications(apps []*model.ApplicationView, sequence uint64) error {
err := view.PutApplications(v.Db, applicationTable, apps...)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(sequence)
}
func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
@ -39,8 +47,8 @@ func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
return v.ProcessedApplicationSequence(eventSequence)
}
func (v *View) DeleteApplicationsByProjectID(ProjectID string) error {
return view.DeleteApplicationsByProjectID(v.Db, applicationTable, ProjectID)
func (v *View) DeleteApplicationsByProjectID(projectID string) error {
return view.DeleteApplicationsByProjectID(v.Db, applicationTable, projectID)
}
func (v *View) GetLatestApplicationSequence() (*repository.CurrentSequence, error) {

View File

@ -32,7 +32,7 @@ func (v *View) PutUserMembership(membership *model.UserMembershipView, sequence
}
func (v *View) BulkPutUserMemberships(memberships []*model.UserMembershipView, sequence uint64) error {
err := view.PutUserMemberships(v.Db, userTable, memberships...)
err := view.PutUserMemberships(v.Db, userMembershipTable, memberships...)
if err != nil {
return err
}

View File

@ -8,7 +8,7 @@ import (
type ProjectRepository interface {
ProjectByID(ctx context.Context, id string) (*model.ProjectView, error)
CreateProject(ctx context.Context, name string) (*model.Project, error)
CreateProject(ctx context.Context, project *model.Project) (*model.Project, error)
UpdateProject(ctx context.Context, project *model.Project) (*model.Project, error)
DeactivateProject(ctx context.Context, id string) (*model.Project, error)
ReactivateProject(ctx context.Context, id string) (*model.Project, error)

View File

@ -13,6 +13,8 @@ type ApplicationView struct {
CreationDate time.Time
ChangeDate time.Time
State AppState
ProjectRoleAssertion bool
ProjectRoleCheck bool
IsOIDC bool
OIDCVersion OIDCVersion
@ -27,6 +29,9 @@ type ApplicationView struct {
ComplianceProblems []string
DevMode bool
OriginAllowList []string
AccessTokenType OIDCTokenType
IDTokenRoleAssertion bool
AccessTokenRoleAssertion bool
Sequence uint64
}

View File

@ -34,6 +34,9 @@ type OIDCConfig struct {
OIDCVersion OIDCVersion
Compliance *Compliance
DevMode bool
AccessTokenType OIDCTokenType
AccessTokenRoleAssertion bool
IDTokenRoleAssertion bool
}
type OIDCVersion int32
@ -79,6 +82,13 @@ type Compliance struct {
Problems []string
}
type OIDCTokenType int32
const (
OIDCTokenTypeBearer OIDCTokenType = iota
OIDCTokenTypeJWT
)
func (c *OIDCConfig) IsValid() bool {
grantTypes := c.getRequiredGrantTypes()
for _, grantType := range grantTypes {

View File

@ -1,8 +1,9 @@
package model
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/golang/protobuf/ptypes/timestamp"
es_models "github.com/caos/zitadel/internal/eventstore/models"
)
type Project struct {
@ -14,6 +15,8 @@ type Project struct {
Roles []*ProjectRole
Applications []*Application
Grants []*ProjectGrant
ProjectRoleAssertion bool
ProjectRoleCheck bool
}
type ProjectChanges struct {
Changes []*ProjectChange

View File

@ -1,8 +1,9 @@
package model
import (
"github.com/caos/zitadel/internal/model"
"time"
"github.com/caos/zitadel/internal/model"
)
type ProjectView struct {
@ -12,6 +13,8 @@ type ProjectView struct {
ChangeDate time.Time
State ProjectState
ResourceOwner string
ProjectRoleAssertion bool
ProjectRoleCheck bool
Sequence uint64
}

View File

@ -321,7 +321,7 @@ func (es *ProjectEventstore) AddProjectRoles(ctx context.Context, roles ...*proj
}
for _, role := range roles {
if !role.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-idue3", "Errors.Project.MemberInvalid")
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-iduG4", "Errors.Project.RoleInvalid")
}
}
existingProject, err := es.ProjectByID(ctx, roles[0].AggregateID)

View File

@ -2,11 +2,13 @@ package model
import (
"encoding/json"
"reflect"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/project/model"
"reflect"
)
type OIDCConfig struct {
@ -22,6 +24,9 @@ type OIDCConfig struct {
AuthMethodType int32 `json:"authMethodType,omitempty"`
PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"`
DevMode bool `json:"devMode,omitempty"`
AccessTokenType int32 `json:"accessTokenType,omitempty"`
AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion,omitempty"`
IDTokenRoleAssertion bool `json:"idTokenRoleAssertion,omitempty"`
}
func (c *OIDCConfig) Changes(changed *OIDCConfig) map[string]interface{} {
@ -51,6 +56,15 @@ func (c *OIDCConfig) Changes(changed *OIDCConfig) map[string]interface{} {
if c.DevMode != changed.DevMode {
changes["devMode"] = changed.DevMode
}
if c.AccessTokenType != changed.AccessTokenType {
changes["accessTokenType"] = changed.AccessTokenType
}
if c.AccessTokenRoleAssertion != changed.AccessTokenRoleAssertion {
changes["accessTokenRoleAssertion"] = changed.AccessTokenRoleAssertion
}
if c.IDTokenRoleAssertion != changed.IDTokenRoleAssertion {
changes["idTokenRoleAssertion"] = changed.IDTokenRoleAssertion
}
return changes
}
@ -76,6 +90,9 @@ func OIDCConfigFromModel(config *model.OIDCConfig) *OIDCConfig {
AuthMethodType: int32(config.AuthMethodType),
PostLogoutRedirectUris: config.PostLogoutRedirectUris,
DevMode: config.DevMode,
AccessTokenType: int32(config.AccessTokenType),
AccessTokenRoleAssertion: config.AccessTokenRoleAssertion,
IDTokenRoleAssertion: config.IDTokenRoleAssertion,
}
}
@ -101,6 +118,9 @@ func OIDCConfigToModel(config *OIDCConfig) *model.OIDCConfig {
AuthMethodType: model.OIDCAuthMethodType(config.AuthMethodType),
PostLogoutRedirectUris: config.PostLogoutRedirectUris,
DevMode: config.DevMode,
AccessTokenType: model.OIDCTokenType(config.AccessTokenType),
AccessTokenRoleAssertion: config.AccessTokenRoleAssertion,
IDTokenRoleAssertion: config.IDTokenRoleAssertion,
}
oidcConfig.FillCompliance()
return oidcConfig

View File

@ -2,7 +2,9 @@ package model
import (
"encoding/json"
"github.com/caos/logging"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/project/model"
)
@ -14,6 +16,8 @@ const (
type Project struct {
es_models.ObjectRoot
Name string `json:"name,omitempty"`
ProjectRoleAssertion bool `json:"projectRoleAssertion,omitempty"`
ProjectRoleCheck bool `json:"projectRoleCheck,omitempty"`
State int32 `json:"-"`
Members []*ProjectMember `json:"-"`
Roles []*ProjectRole `json:"-"`
@ -35,6 +39,12 @@ func (p *Project) Changes(changed *Project) map[string]interface{} {
if changed.Name != "" && p.Name != changed.Name {
changes["name"] = changed.Name
}
if p.ProjectRoleAssertion != changed.ProjectRoleAssertion {
changes["projectRoleAssertion"] = changed.ProjectRoleAssertion
}
if p.ProjectRoleCheck != changed.ProjectRoleCheck {
changes["projectRoleCheck"] = changed.ProjectRoleCheck
}
return changes
}
@ -46,6 +56,8 @@ func ProjectFromModel(project *model.Project) *Project {
return &Project{
ObjectRoot: project.ObjectRoot,
Name: project.Name,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
State: int32(project.State),
Members: members,
Roles: roles,
@ -62,6 +74,8 @@ func ProjectToModel(project *Project) *model.Project {
return &model.Project{
ObjectRoot: project.ObjectRoot,
Name: project.Name,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
State: model.ProjectState(project.State),
Members: members,
Roles: roles,

View File

@ -69,6 +69,15 @@ func PutApplication(db *gorm.DB, table string, app *model.ApplicationView) error
return save(db, app)
}
func PutApplications(db *gorm.DB, table string, apps ...*model.ApplicationView) error {
save := repository.PrepareBulkSave(table)
s := make([]interface{}, len(apps))
for i, app := range apps {
s[i] = app
}
return save(db, s...)
}
func DeleteApplication(db *gorm.DB, table, appID string) error {
delete := repository.PrepareDeleteByKey(table, model.ApplicationSearchKey(proj_model.AppSearchKeyAppID), appID)
return delete(db)

View File

@ -29,6 +29,8 @@ type ApplicationView struct {
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
State int32 `json:"-" gorm:"column:app_state"`
ProjectRoleAssertion bool `json:"projectRoleAssertion" gorm:"column:project_role_assertion"`
ProjectRoleCheck bool `json:"projectRoleCheck" gorm:"column:project_role_check"`
IsOIDC bool `json:"-" gorm:"column:is_oidc"`
OIDCVersion int32 `json:"oidcVersion" gorm:"column:oidc_version"`
@ -43,49 +45,13 @@ type ApplicationView struct {
ComplianceProblems pq.StringArray `json:"-" gorm:"column:compliance_problems"`
DevMode bool `json:"devMode" gorm:"column:dev_mode"`
OriginAllowList pq.StringArray `json:"-" gorm:"column:origin_allow_list"`
AccessTokenType int32 `json:"accessTokenType" gorm:"column:access_token_type"`
AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion" gorm:"column:access_token_role_assertion"`
IDTokenRoleAssertion bool `json:"idTokenRoleAssertion" gorm:"column:id_token_role_assertion"`
Sequence uint64 `json:"-" gorm:"sequence"`
}
func ApplicationViewFromModel(app *model.ApplicationView) *ApplicationView {
return &ApplicationView{
ID: app.ID,
ProjectID: app.ProjectID,
Name: app.Name,
State: int32(app.State),
Sequence: app.Sequence,
CreationDate: app.CreationDate,
ChangeDate: app.ChangeDate,
IsOIDC: app.IsOIDC,
OIDCClientID: app.OIDCClientID,
OIDCRedirectUris: app.OIDCRedirectUris,
OIDCResponseTypes: OIDCResponseTypesFromModel(app.OIDCResponseTypes),
OIDCGrantTypes: OIDCGrantTypesFromModel(app.OIDCGrantTypes),
OIDCApplicationType: int32(app.OIDCApplicationType),
OIDCAuthMethodType: int32(app.OIDCAuthMethodType),
OIDCPostLogoutRedirectUris: app.OIDCPostLogoutRedirectUris,
DevMode: app.DevMode,
OriginAllowList: app.OriginAllowList,
}
}
func OIDCResponseTypesFromModel(oidctypes []model.OIDCResponseType) []int64 {
result := make([]int64, len(oidctypes))
for i, t := range oidctypes {
result[i] = int64(t)
}
return result
}
func OIDCGrantTypesFromModel(granttypes []model.OIDCGrantType) []int64 {
result := make([]int64, len(granttypes))
for i, t := range granttypes {
result[i] = int64(t)
}
return result
}
func ApplicationViewToModel(app *ApplicationView) *model.ApplicationView {
return &model.ApplicationView{
ID: app.ID,
@ -95,6 +61,8 @@ func ApplicationViewToModel(app *ApplicationView) *model.ApplicationView {
Sequence: app.Sequence,
CreationDate: app.CreationDate,
ChangeDate: app.ChangeDate,
ProjectRoleAssertion: app.ProjectRoleAssertion,
ProjectRoleCheck: app.ProjectRoleCheck,
IsOIDC: app.IsOIDC,
OIDCVersion: model.OIDCVersion(app.OIDCVersion),
@ -109,6 +77,9 @@ func ApplicationViewToModel(app *ApplicationView) *model.ApplicationView {
ComplianceProblems: app.ComplianceProblems,
DevMode: app.DevMode,
OriginAllowList: app.OriginAllowList,
AccessTokenType: model.OIDCTokenType(app.AccessTokenType),
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
IDTokenRoleAssertion: app.IDTokenRoleAssertion,
}
}
@ -152,6 +123,8 @@ func (a *ApplicationView) AppendEventIfMyApp(event *models.Event) (err error) {
}
case es_model.ApplicationRemoved:
return view.SetData(event)
case es_model.ProjectChanged:
return a.AppendEvent(event)
case es_model.ProjectRemoved:
return a.AppendEvent(event)
default:
@ -187,6 +160,8 @@ func (a *ApplicationView) AppendEvent(event *models.Event) (err error) {
}
a.setCompliance()
return a.setOriginAllowList()
case es_model.ProjectChanged:
return a.SetData(event)
case es_model.ApplicationDeactivated:
a.State = int32(model.AppStateInactive)
case es_model.ApplicationReactivated:

View File

@ -2,12 +2,14 @@ package model
import (
"encoding/json"
"time"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/project/model"
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
"time"
)
const (
@ -23,6 +25,8 @@ type ProjectView struct {
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
State int32 `json:"-" gorm:"column:project_state"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
ProjectRoleAssertion bool `json:"projectRoleAssertion" gorm:"column:project_role_assertion"`
ProjectRoleCheck bool `json:"projectRoleCheck" gorm:"column:project_role_check"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
}
@ -34,6 +38,8 @@ func ProjectFromModel(project *model.ProjectView) *ProjectView {
CreationDate: project.CreationDate,
State: int32(project.State),
ResourceOwner: project.ResourceOwner,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
Sequence: project.Sequence,
}
}
@ -46,6 +52,8 @@ func ProjectToModel(project *ProjectView) *model.ProjectView {
CreationDate: project.CreationDate,
State: model.ProjectState(project.State),
ResourceOwner: project.ResourceOwner,
ProjectRoleAssertion: project.ProjectRoleAssertion,
ProjectRoleCheck: project.ProjectRoleCheck,
Sequence: project.Sequence,
}
}

View File

@ -222,32 +222,32 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
}
func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *model.ExternalUser {
displayName := tokens.IDTokenClaims.PreferredUsername
if displayName == "" && tokens.IDTokenClaims.Email != "" {
displayName = tokens.IDTokenClaims.Email
displayName := tokens.IDTokenClaims.GetPreferredUsername()
if displayName == "" && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
displayName = tokens.IDTokenClaims.Email
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
}
externalUser := &model.ExternalUser{
IDPConfigID: idpConfig.IDPConfigID,
ExternalUserID: tokens.IDTokenClaims.Subject,
PreferredUsername: tokens.IDTokenClaims.PreferredUsername,
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(),
DisplayName: displayName,
FirstName: tokens.IDTokenClaims.GivenName,
LastName: tokens.IDTokenClaims.FamilyName,
NickName: tokens.IDTokenClaims.Nickname,
Email: tokens.IDTokenClaims.Email,
IsEmailVerified: tokens.IDTokenClaims.EmailVerified,
FirstName: tokens.IDTokenClaims.GetGivenName(),
LastName: tokens.IDTokenClaims.GetFamilyName(),
NickName: tokens.IDTokenClaims.GetNickname(),
Email: tokens.IDTokenClaims.GetEmail(),
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
}
if tokens.IDTokenClaims.PhoneNumber != "" {
externalUser.Phone = tokens.IDTokenClaims.PhoneNumber
externalUser.IsPhoneVerified = tokens.IDTokenClaims.PhoneNumberVerified
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber()
externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified()
}
return externalUser
}

View File

@ -1,8 +1,13 @@
package handler
import (
"net/http"
"strings"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/auth_request/model"
caos_errors "github.com/caos/zitadel/internal/errors"
@ -10,8 +15,6 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"net/http"
"strings"
)
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
@ -109,11 +112,11 @@ func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Reques
}
func (l *Login) mapTokenToLoginUserAndExternalIDP(orgIamPolicy *iam_model.OrgIAMPolicyView, tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*usr_model.User, *usr_model.ExternalIDP) {
username := tokens.IDTokenClaims.PreferredUsername
username := tokens.IDTokenClaims.GetPreferredUsername()
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
username = tokens.IDTokenClaims.Email
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
username = tokens.IDTokenClaims.GetEmail()
}
}
@ -128,35 +131,35 @@ func (l *Login) mapTokenToLoginUserAndExternalIDP(orgIamPolicy *iam_model.OrgIAM
UserName: username,
Human: &usr_model.Human{
Profile: &usr_model.Profile{
FirstName: tokens.IDTokenClaims.GivenName,
LastName: tokens.IDTokenClaims.FamilyName,
PreferredLanguage: tokens.IDTokenClaims.Locale,
NickName: tokens.IDTokenClaims.Nickname,
FirstName: tokens.IDTokenClaims.GetGivenName(),
LastName: tokens.IDTokenClaims.GetFamilyName(),
PreferredLanguage: language.Tag(tokens.IDTokenClaims.GetLocale()),
NickName: tokens.IDTokenClaims.GetNickname(),
},
Email: &usr_model.Email{
EmailAddress: tokens.IDTokenClaims.Email,
IsEmailVerified: tokens.IDTokenClaims.EmailVerified,
EmailAddress: tokens.IDTokenClaims.GetEmail(),
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
},
},
}
if tokens.IDTokenClaims.PhoneNumber != "" {
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
user.Phone = &usr_model.Phone{
PhoneNumber: tokens.IDTokenClaims.PhoneNumber,
IsPhoneVerified: tokens.IDTokenClaims.PhoneNumberVerified,
PhoneNumber: tokens.IDTokenClaims.GetPhoneNumber(),
IsPhoneVerified: tokens.IDTokenClaims.IsPhoneNumberVerified(),
}
}
displayName := tokens.IDTokenClaims.PreferredUsername
displayName := tokens.IDTokenClaims.GetPreferredUsername()
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
displayName = tokens.IDTokenClaims.Email
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
}
externalIDP := &usr_model.ExternalIDP{
IDPConfigID: idpConfig.IDPConfigID,
UserID: tokens.IDTokenClaims.Subject,
UserID: tokens.IDTokenClaims.GetSubject(),
DisplayName: displayName,
}
return user, externalIDP

View File

@ -211,6 +211,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
l.renderExternalNotFoundOption(w, r, authReq, err)
case *model.ExternalLoginStep:
l.handleExternalLoginStep(w, r, authReq, step.SelectedIDPConfigID)
case *model.GrantRequiredStep:
l.renderInternalError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-asb43", "Errors.User.GrantRequired"))
default:
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
}

View File

@ -187,6 +187,7 @@ Errors:
AuthRequest:
NotFound: AuthRequest konnte nicht gefunden werden
UserAgentNotCorresponding: User Agent stimmt nicht überein
RequestTypeNotSupported: Requesttyp wird nicht unterstürzt
User:
NotFound: Benutzer konnte nicht gefunden werden
NotMatchingUserID: User stimm nicht mit User in Auth Request überein
@ -226,5 +227,6 @@ Errors:
ExternalIDP:
IDPTypeNotImplemented: IDP Typ ist nicht implementiert
NotAllowed: Externer Login Provider ist nicht erlaubt
GrantRequired: Der Login an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte melde dich bei deinem Administrator.
optional: (optional)

View File

@ -187,6 +187,7 @@ Errors:
AuthRequest:
NotFound: Could not find authrequest
UserAgentNotCorresponding: User Agent does not correspond
RequestTypeNotSupported: Request type is not supported
User:
NotFound: User could not be found
NotMatchingUserID: User and user in authrequest don't match
@ -226,6 +227,7 @@ Errors:
ExternalIDP:
IDPTypeNotImplemented: IDP Type is not implemented
NotAllowed: External Login Provider not allowed
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
optional: (optional)

View File

@ -1,6 +1,6 @@
{{template "main-top" .}}
<div>
<div class="head">
{{ .ErrType }}
{{ .ErrMessage }}
</div>

View File

@ -44,7 +44,7 @@ const (
type TokenSearchQuery struct {
Key TokenSearchKey
Method model.SearchMethod
Value string
Value interface{}
}
type TokenSearchResponse struct {

View File

@ -19,7 +19,7 @@ type UserGrantView struct {
Email string
ProjectName string
OrgName string
OrgDomain string
OrgPrimaryDomain string
RoleKeys []string
CreationDate time.Time

View File

@ -5,11 +5,12 @@ import (
"time"
"github.com/caos/logging"
"github.com/lib/pq"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/model"
es_model "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
"github.com/lib/pq"
)
const (
@ -36,6 +37,7 @@ type UserGrantView struct {
Email string `json:"-" gorm:"column:email"`
ProjectName string `json:"-" gorm:"column:project_name"`
OrgName string `json:"-" gorm:"column:org_name"`
OrgPrimaryDomain string `json:"-" gorm:"column:org_primary_domain"`
RoleKeys pq.StringArray `json:"roleKeys" gorm:"column:role_keys"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
@ -61,6 +63,7 @@ func UserGrantToModel(grant *UserGrantView) *model.UserGrantView {
Email: grant.Email,
ProjectName: grant.ProjectName,
OrgName: grant.OrgName,
OrgPrimaryDomain: grant.OrgPrimaryDomain,
RoleKeys: grant.RoleKeys,
Sequence: grant.Sequence,
}

View File

@ -0,0 +1,40 @@
CREATE TABLE auth.project_roles (
project_id TEXT,
role_key TEXT,
display_name TEXT,
resource_owner TEXT,
org_id TEXT,
group_name TEXT,
creation_date TIMESTAMPTZ,
sequence BIGINT,
PRIMARY KEY (org_id, project_id, role_key)
);
ALTER TABLE authz.user_grants ADD COLUMN org_primary_domain TEXT;
ALTER TABLE auth.user_grants ADD COLUMN org_primary_domain TEXT;
ALTER TABLE management.user_grants ADD COLUMN org_primary_domain TEXT;
ALTER TABLE authz.applications ADD COLUMN access_token_type SMALLINT;
ALTER TABLE auth.applications ADD COLUMN access_token_type SMALLINT;
ALTER TABLE management.applications ADD COLUMN access_token_type SMALLINT;
ALTER TABLE management.projects ADD COLUMN project_role_assertion BOOLEAN;
ALTER TABLE management.projects ADD COLUMN project_role_check BOOLEAN;
ALTER TABLE authz.applications ADD COLUMN project_role_assertion BOOLEAN;
ALTER TABLE auth.applications ADD COLUMN project_role_assertion BOOLEAN;
ALTER TABLE management.applications ADD COLUMN project_role_assertion BOOLEAN;
ALTER TABLE authz.applications ADD COLUMN project_role_check BOOLEAN;
ALTER TABLE auth.applications ADD COLUMN project_role_check BOOLEAN;
ALTER TABLE management.applications ADD COLUMN project_role_check BOOLEAN;
ALTER TABLE authz.applications ADD COLUMN access_token_role_assertion BOOLEAN;
ALTER TABLE auth.applications ADD COLUMN access_token_role_assertion BOOLEAN;
ALTER TABLE management.applications ADD COLUMN access_token_role_assertion BOOLEAN;
ALTER TABLE authz.applications ADD COLUMN id_token_role_assertion BOOLEAN;
ALTER TABLE auth.applications ADD COLUMN id_token_role_assertion BOOLEAN;
ALTER TABLE management.applications ADD COLUMN id_token_role_assertion BOOLEAN;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6381,6 +6381,10 @@ func (m *ProjectCreateRequest) Validate() error {
}
}
// no validation rules for ProjectRoleAssertion
// no validation rules for ProjectRoleCheck
return nil
}
@ -6462,6 +6466,10 @@ func (m *ProjectUpdateRequest) Validate() error {
}
}
// no validation rules for ProjectRoleAssertion
// no validation rules for ProjectRoleCheck
return nil
}
@ -6659,6 +6667,10 @@ func (m *ProjectView) Validate() error {
// no validation rules for Sequence
// no validation rules for ProjectRoleAssertion
// no validation rules for ProjectRoleCheck
return nil
}
@ -6998,6 +7010,10 @@ func (m *Project) Validate() error {
// no validation rules for Sequence
// no validation rules for ProjectRoleAssertion
// no validation rules for ProjectRoleCheck
return nil
}
@ -8850,6 +8866,12 @@ func (m *OIDCConfig) Validate() error {
// no validation rules for DevMode
// no validation rules for AccessTokenType
// no validation rules for AccessTokenRoleAssertion
// no validation rules for IdTokenRoleAssertion
return nil
}
@ -8937,6 +8959,12 @@ func (m *OIDCApplicationCreate) Validate() error {
// no validation rules for DevMode
// no validation rules for AccessTokenType
// no validation rules for AccessTokenRoleAssertion
// no validation rules for IdTokenRoleAssertion
return nil
}
@ -9024,6 +9052,12 @@ func (m *OIDCConfigUpdate) Validate() error {
// no validation rules for DevMode
// no validation rules for AccessTokenType
// no validation rules for AccessTokenRoleAssertion
// no validation rules for IdTokenRoleAssertion
return nil
}

View File

@ -2180,11 +2180,15 @@ enum OrgMemberSearchKey {
message ProjectCreateRequest {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
bool project_role_assertion = 2;
bool project_role_check = 3;
}
message ProjectUpdateRequest {
string id = 1 [(validate.rules).string = {min_len: 1}];
string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
bool project_role_assertion = 3;
bool project_role_check = 4;
}
message ProjectSearchResponse {
@ -2204,6 +2208,8 @@ message ProjectView {
google.protobuf.Timestamp creation_date = 5;
string resource_owner = 6;
uint64 sequence = 7;
bool project_role_assertion = 8;
bool project_role_check = 9;
}
message ProjectSearchRequest {
@ -2234,6 +2240,8 @@ message Project {
google.protobuf.Timestamp change_date = 4;
google.protobuf.Timestamp creation_date = 5;
uint64 sequence = 6;
bool project_role_assertion = 7;
bool project_role_check = 8;
}
enum ProjectState {
@ -2423,6 +2431,9 @@ message OIDCConfig {
bool none_compliant = 10;
repeated caos.zitadel.api.v1.LocalizedMessage compliance_problems = 11;
bool dev_mode = 12;
OIDCTokenType access_token_type = 13;
bool access_token_role_assertion = 14;
bool id_token_role_assertion = 15;
}
message OIDCApplicationCreate {
@ -2436,12 +2447,20 @@ message OIDCApplicationCreate {
repeated string post_logout_redirect_uris = 8;
OIDCVersion version = 9;
bool dev_mode = 10;
OIDCTokenType access_token_type = 11;
bool access_token_role_assertion = 12;
bool id_token_role_assertion = 13;
}
enum OIDCVersion {
OIDCV1_0 = 0;
}
enum OIDCTokenType {
OIDCTokenType_Bearer = 0;
OIDCTokenType_JWT = 1;
}
message OIDCConfigUpdate {
string project_id = 1 [(validate.rules).string = {min_len: 1}];
string application_id = 2 [(validate.rules).string = {min_len: 1}];
@ -2452,6 +2471,9 @@ message OIDCConfigUpdate {
OIDCAuthMethodType auth_method_type = 7;
repeated string post_logout_redirect_uris = 8;
bool dev_mode = 9;
OIDCTokenType access_token_type = 10;
bool access_token_role_assertion = 11;
bool id_token_role_assertion = 12;
}
enum OIDCResponseType {