feat: check has project (#2206)

* feat: define org grant check on project

* feat: has project check

* feat: has project check

* feat: check has project

* feat: check has project

* feat: add has project check to console

* Update internal/auth/repository/eventsourcing/eventstore/auth_request.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request_test.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request_test.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/auth/repository/eventsourcing/eventstore/auth_request_test.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/ui/login/static/i18n/en.yaml

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix: add has project tests

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi
2021-08-18 10:49:04 +02:00
committed by GitHub
parent 0b3155b8ab
commit 0ab973b967
45 changed files with 732 additions and 190 deletions

View File

@@ -44,6 +44,7 @@ type AuthRequestRepo struct {
LockoutPolicyViewProvider lockoutPolicyViewProvider
IDPProviderViewProvider idpProviderViewProvider
UserGrantProvider userGrantProvider
ProjectProvider projectProvider
IdGenerator id.Generator
@@ -96,6 +97,11 @@ type userGrantProvider interface {
UserGrantsByProjectAndUserID(string, string) ([]*grant_view_model.UserGrantView, error)
}
type projectProvider interface {
ApplicationByClientID(context.Context, string) (*project_view_model.ApplicationView, error)
OrgProjectMappingByIDs(orgID, projectID string) (*project_view_model.OrgProjectMapping, error)
}
func (repo *AuthRequestRepo) Health(ctx context.Context) error {
return repo.AuthRequests.Health(ctx)
}
@@ -680,7 +686,15 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
}
//PLANNED: consent step
missing, err := userGrantRequired(ctx, request, user, repo.UserGrantProvider)
missing, err := projectRequired(ctx, request, repo.ProjectProvider)
if err != nil {
return nil, err
}
if missing {
return append(steps, &domain.ProjectRequiredStep{}), nil
}
missing, err = userGrantRequired(ctx, request, user, repo.UserGrantProvider)
if err != nil {
return nil, err
}
@@ -1081,6 +1095,7 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser,
}
return true
}
func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
var app *project_view_model.ApplicationView
switch request.Request.Type() {
@@ -1101,3 +1116,27 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u
}
return len(grants) == 0, nil
}
func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (_ bool, err error) {
var app *project_view_model.ApplicationView
switch request.Request.Type() {
case domain.AuthRequestTypeOIDC:
app, err = projectProvider.ApplicationByClientID(ctx, request.ApplicationID)
if err != nil {
return false, err
}
default:
return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported")
}
if !app.HasProjectCheck {
return false, nil
}
_, err = projectProvider.OrgProjectMappingByIDs(request.UserOrgID, app.ProjectID)
if errors.IsNotFound(err) {
return true, nil
}
if err != nil {
return false, err
}
return false, nil
}

View File

@@ -6,9 +6,10 @@ import (
"testing"
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/auth_request/repository/cache"
@@ -227,6 +228,22 @@ func (m *mockUserGrants) UserGrantsByProjectAndUserID(s string, s2 string) ([]*g
return grants, nil
}
type mockProject struct {
hasProject bool
projectCheck bool
}
func (m *mockProject) ApplicationByClientID(ctx context.Context, s string) (*proj_view_model.ApplicationView, error) {
return &proj_view_model.ApplicationView{HasProjectCheck: m.projectCheck}, nil
}
func (m *mockProject) OrgProjectMappingByIDs(orgID, projectID string) (*proj_view_model.OrgProjectMapping, error) {
if m.hasProject {
return &proj_view_model.OrgProjectMapping{OrgID: orgID, ProjectID: projectID}, nil
}
return nil, errors.ThrowNotFound(nil, "ERROR", "error")
}
func TestAuthRequestRepo_nextSteps(t *testing.T) {
type fields struct {
AuthRequests *cache.AuthRequestCache
@@ -236,6 +253,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userEventProvider userEventProvider
orgViewProvider orgViewProvider
userGrantProvider userGrantProvider
projectProvider projectProvider
loginPolicyProvider loginPolicyViewProvider
lockoutPolicyProvider lockoutPolicyViewProvider
PasswordCheckLifeTime time.Duration
@@ -684,6 +702,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
loginPolicyProvider: &mockLoginPolicy{
policy: &iam_view_model.LoginPolicyView{},
},
@@ -741,6 +760,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
@@ -971,6 +991,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
@@ -1004,6 +1025,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
@@ -1041,6 +1063,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
roleCheck: true,
userGrants: 0,
},
projectProvider: &mockProject{},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
@@ -1078,6 +1101,83 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
roleCheck: true,
userGrants: 2,
},
projectProvider: &mockProject{},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
},
},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
SecondFactorCheckLifeTime: 18 * time.Hour,
},
args{&domain.AuthRequest{
UserID: "UserID",
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
},
}, true},
[]domain.NextStep{&domain.RedirectToCallbackStep{}},
nil,
},
{
"prompt none, checkLoggedIn true, authenticated and required project missing, project required step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MFAMaxSetUp: int32(model.MFALevelSecondFactor),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{
projectCheck: true,
hasProject: false,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
},
},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
SecondFactorCheckLifeTime: 18 * time.Hour,
},
args{&domain.AuthRequest{
UserID: "UserID",
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
},
}, true},
[]domain.NextStep{&domain.ProjectRequiredStep{}},
nil,
},
{
"prompt none, checkLoggedIn true, authenticated and required project exist, redirect to callback step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MFAMaxSetUp: int32(model.MFALevelSecondFactor),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{
projectCheck: true,
hasProject: true,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &iam_view_model.LockoutPolicyView{
ShowLockOutFailures: true,
@@ -1172,6 +1272,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserEventProvider: tt.fields.userEventProvider,
OrgViewProvider: tt.fields.orgViewProvider,
UserGrantProvider: tt.fields.userGrantProvider,
ProjectProvider: tt.fields.projectProvider,
LoginPolicyViewProvider: tt.fields.loginPolicyProvider,
LockoutPolicyViewProvider: tt.fields.lockoutPolicyProvider,
PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime,

View File

@@ -82,6 +82,7 @@ func (a *Application) Reduce(event *models.Event) (err error) {
return err
}
app.ProjectRoleCheck = project.ProjectRoleCheck
app.HasProjectCheck = project.HasProjectCheck
app.ProjectRoleAssertion = project.ProjectRoleAssertion
err = app.AppendEvent(event)

View File

@@ -74,6 +74,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
newCustomText(handler{view, bulkLimit, configs.cycleDuration("CustomTexts"), errorCount, es}),
newMetadata(handler{view, bulkLimit, configs.cycleDuration("Metadata"), errorCount, es}),
newLockoutPolicy(handler{view, bulkLimit, configs.cycleDuration("LockoutPolicy"), errorCount, es}),
newOrgProjectMapping(handler{view, bulkLimit, configs.cycleDuration("OrgProjectMapping"), errorCount, es}),
}
}

View File

@@ -0,0 +1,113 @@
package handler
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/v1"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
"github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
proj_view "github.com/caos/zitadel/internal/project/repository/view"
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
)
const (
orgProjectMappingTable = "auth.org_project_mapping"
)
type OrgProjectMapping struct {
handler
subscription *v1.Subscription
}
func newOrgProjectMapping(
handler handler,
) *OrgProjectMapping {
h := &OrgProjectMapping{
handler: handler,
}
h.subscribe()
return h
}
func (k *OrgProjectMapping) subscribe() {
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
go func() {
for event := range k.subscription.Events {
query.ReduceEvent(k, event)
}
}()
}
func (p *OrgProjectMapping) ViewModel() string {
return orgProjectMappingTable
}
func (p *OrgProjectMapping) Subscription() *v1.Subscription {
return p.subscription
}
func (_ *OrgProjectMapping) AggregateTypes() []es_models.AggregateType {
return []es_models.AggregateType{model.ProjectAggregate}
}
func (p *OrgProjectMapping) CurrentSequence() (uint64, error) {
sequence, err := p.view.GetLatestOrgProjectMappingSequence()
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (p *OrgProjectMapping) EventQuery() (*es_models.SearchQuery, error) {
sequence, err := p.view.GetLatestOrgProjectMappingSequence()
if err != nil {
return nil, err
}
return proj_view.ProjectQuery(sequence.CurrentSequence), nil
}
func (p *OrgProjectMapping) Reduce(event *es_models.Event) (err error) {
mapping := new(view_model.OrgProjectMapping)
switch event.Type {
case model.ProjectAdded:
mapping.OrgID = event.ResourceOwner
mapping.ProjectID = event.AggregateID
case model.ProjectRemoved:
err := p.view.DeleteOrgProjectMappingsByProjectID(event.AggregateID)
if err == nil {
return p.view.ProcessedOrgProjectMappingSequence(event)
}
case model.ProjectGrantAdded:
projectGrant := new(view_model.ProjectGrant)
projectGrant.SetData(event)
mapping.OrgID = projectGrant.GrantedOrgID
mapping.ProjectID = event.AggregateID
mapping.ProjectGrantID = projectGrant.GrantID
case model.ProjectGrantRemoved:
projectGrant := new(view_model.ProjectGrant)
projectGrant.SetData(event)
err := p.view.DeleteOrgProjectMappingsByProjectGrantID(event.AggregateID)
if err == nil {
return p.view.ProcessedOrgProjectMappingSequence(event)
}
default:
return p.view.ProcessedOrgProjectMappingSequence(event)
}
if err != nil {
return err
}
return p.view.PutOrgProjectMapping(mapping, event)
}
func (p *OrgProjectMapping) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-2k0fS", "id", event.AggregateID).WithError(err).Warn("something went wrong in org project mapping handler")
return spooler.HandleError(event, err, p.view.GetLatestOrgProjectMappingFailedEvent, p.view.ProcessedOrgProjectMappingFailedEvent, p.view.ProcessedOrgProjectMappingSequence, p.errorCountUntilSkip)
}
func (p *OrgProjectMapping) OnSuccess() error {
return spooler.HandleSuccess(p.view.UpdateOrgProjectMappingSpoolerRunTimestamp)
}

View File

@@ -111,6 +111,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
LoginPolicyViewProvider: view,
LockoutPolicyViewProvider: view,
UserGrantProvider: view,
ProjectProvider: view,
IdGenerator: idGenerator,
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,

View File

@@ -0,0 +1,61 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"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 (
orgPrgojectMappingTable = "auth.org_project_mapping"
)
func (v *View) OrgProjectMappingByIDs(orgID, projectID string) (*model.OrgProjectMapping, error) {
return view.OrgProjectMappingByIDs(v.Db, orgPrgojectMappingTable, orgID, projectID)
}
func (v *View) PutOrgProjectMapping(mapping *model.OrgProjectMapping, event *models.Event) error {
err := view.PutOrgProjectMapping(v.Db, orgPrgojectMappingTable, mapping)
if err != nil {
return err
}
return v.ProcessedOrgProjectMappingSequence(event)
}
func (v *View) DeleteOrgProjectMapping(orgID, projectID string, event *models.Event) error {
err := view.DeleteOrgProjectMapping(v.Db, orgPrgojectMappingTable, orgID, projectID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedOrgProjectMappingSequence(event)
}
func (v *View) DeleteOrgProjectMappingsByProjectID(projectID string) error {
return view.DeleteOrgProjectMappingsByProjectID(v.Db, orgPrgojectMappingTable, projectID)
}
func (v *View) DeleteOrgProjectMappingsByProjectGrantID(projectGrantID string) error {
return view.DeleteOrgProjectMappingsByProjectGrantID(v.Db, orgPrgojectMappingTable, projectGrantID)
}
func (v *View) GetLatestOrgProjectMappingSequence() (*repository.CurrentSequence, error) {
return v.latestSequence(orgPrgojectMappingTable)
}
func (v *View) ProcessedOrgProjectMappingSequence(event *models.Event) error {
return v.saveCurrentSequence(orgPrgojectMappingTable, event)
}
func (v *View) UpdateOrgProjectMappingSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(orgPrgojectMappingTable)
}
func (v *View) GetLatestOrgProjectMappingFailedEvent(sequence uint64) (*repository.FailedEvent, error) {
return v.latestFailedEvent(orgPrgojectMappingTable, sequence)
}
func (v *View) ProcessedOrgProjectMappingFailedEvent(failedEvent *repository.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}