mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +00:00
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:
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
|
21
internal/auth/repository/eventsourcing/eventstore/project.go
Normal file
21
internal/auth/repository/eventsourcing/eventstore/project.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user