feat: permission check on OIDC and SAML service session API (#9304)

# Which Problems Are Solved

Through configuration on projects, there can be additional permission
checks enabled through an OIDC or SAML flow, which were not included in
the OIDC and SAML services.

# How the Problems Are Solved

Add permission check through the query-side of Zitadel in a singular SQL
query, when an OIDC or SAML flow should be linked to a SSO session. That
way it is eventual consistent, but will not impact the performance on
the eventstore. The permission check is defined in the API, which
provides the necessary function to the command side.

# Additional Changes

Added integration tests for the permission check on OIDC and SAML
service for every combination.
Corrected session list integration test, to content checks without
ordering.
Corrected get auth and saml request integration tests, to check for
timestamp of creation, not start of test.

# Additional Context

Closes #9265

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz 2025-02-11 19:45:09 +01:00 committed by GitHub
parent 13f9d2d142
commit 840da5be2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1977 additions and 232 deletions

View File

@ -5,11 +5,11 @@ package oidc_test
import ( import (
"context" "context"
"net/url" "net/url"
"os"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -22,92 +22,62 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/session/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2"
) )
var (
CTX context.Context
CTXLoginClient context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
)
const (
redirectURI = "oidcintegrationtest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.OIDCv2
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
CTXLoginClient = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
return m.Run()
}())
}
func TestServer_GetAuthRequest(t *testing.T) { func TestServer_GetAuthRequest(t *testing.T) {
project, err := Instance.CreateProject(CTX) project, err := Instance.CreateProject(CTX)
require.NoError(t, err) require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err) require.NoError(t, err)
now := time.Now()
tests := []struct { tests := []struct {
name string name string
AuthRequestID string dep func() (time.Time, string, error)
ctx context.Context ctx context.Context
want *oidc_pb.GetAuthRequestResponse want *oidc_pb.GetAuthRequestResponse
wantErr bool wantErr bool
}{ }{
{ {
name: "Not found", name: "Not found",
AuthRequestID: "123", dep: func() (time.Time, string, error) {
ctx: CTX, return time.Now(), "123", nil
wantErr: true, },
ctx: CTX,
wantErr: true,
}, },
{ {
name: "success", name: "success",
AuthRequestID: func() string { dep: func() (time.Time, string, error) {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI) return Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err) },
return authRequestID
}(),
ctx: CTX, ctx: CTX,
}, },
{ {
name: "without login client, no permission", name: "without login client, no permission",
AuthRequestID: func() string { dep: func() (time.Time, string, error) {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err) require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "") return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err) },
return authRequestID
}(),
ctx: CTX, ctx: CTX,
wantErr: true, wantErr: true,
}, },
{ {
name: "without login client, with permission", name: "without login client, with permission",
AuthRequestID: func() string { dep: func() (time.Time, string, error) {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err) require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "") return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID },
}(),
ctx: CTXLoginClient, ctx: CTXLoginClient,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
now, authRequestID, err := tt.dep()
require.NoError(t, err)
got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: tt.AuthRequestID, AuthRequestId: authRequestID,
}) })
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
@ -116,7 +86,7 @@ func TestServer_GetAuthRequest(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
authRequest := got.GetAuthRequest() authRequest := got.GetAuthRequest()
assert.NotNil(t, authRequest) assert.NotNil(t, authRequest)
assert.Equal(t, tt.AuthRequestID, authRequest.GetId()) assert.Equal(t, authRequestID, authRequest.GetId())
assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
assert.Contains(t, authRequest.GetScope(), "openid") assert.Contains(t, authRequest.GetScope(), "openid")
}) })
@ -130,16 +100,7 @@ func TestServer_CreateCallback(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err) require.NoError(t, err)
sessionResp, err := Instance.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{ sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: Instance.Users[integration.UserTypeOrgOwner].ID,
},
},
},
})
require.NoError(t, err)
tests := []struct { tests := []struct {
name string name string
@ -169,7 +130,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX, ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -187,7 +148,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX, ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -205,7 +166,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX, ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -231,7 +192,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTXLoginClient, ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "") _, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -257,7 +218,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX, ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -282,7 +243,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX, ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "") _, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -300,7 +261,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTXLoginClient, ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "") _, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -390,3 +351,324 @@ func TestServer_CreateCallback(t *testing.T) {
}) })
} }
} }
func TestServer_CreateCallback_Permission(t *testing.T) {
tests := []struct {
name string
ctx context.Context
dep func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest
want *oidc_pb.CreateCallbackResponse
wantURL *url.URL
wantErr bool
}{
{
name: "usergrant to project and different resourceowner with different project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
projectID2, _ := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant to project and different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "usergrant to project grant and different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "no usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "no usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, true)
user := Instance.CreateHumanUser(ctx)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, false)
user := Instance.CreateHumanUser(ctx)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant on project grant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant on project grant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "hasProjectCheck, same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
user := Instance.CreateHumanUser(ctx)
_, clientID := createOIDCApplication(ctx, t, false, true)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "hasProjectCheck, different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, false, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "hasProjectCheck, different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, false, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := tt.dep(IAMCTX, t)
got, err := Client.CreateCallback(tt.ctx, req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl())
}
})
}
}
func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse {
sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
},
})
require.NoError(t, err)
return sessionResp
}
func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, clientID, loginClient, userID string) *oidc_pb.CreateCallbackRequest {
_, authRequestID, err := Instance.CreateOIDCAuthRequest(ctx, clientID, loginClient, redirectURI)
require.NoError(t, err)
sessionResp := createSession(t, ctx, userID)
return &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
}
}
func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) {
project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck)
require.NoError(t, err)
clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
return project.GetId(), clientV2.GetClientId()
}

View File

@ -0,0 +1,44 @@
//go:build integration
package oidc_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
var (
CTX context.Context
CTXLoginClient context.Context
IAMCTX context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
)
const (
redirectURI = "oidcintegrationtest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.OIDCv2
IAMCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
CTXLoginClient = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
return m.Run()
}())
}

View File

@ -73,6 +73,20 @@ func promptToPb(p domain.Prompt) oidc_pb.Prompt {
} }
} }
func (s *Server) checkPermission(ctx context.Context, clientID string, userID string) error {
permission, err := s.query.CheckProjectPermissionByClientID(ctx, clientID, userID)
if err != nil {
return err
}
if !permission.HasProjectChecked {
return zerrors.ThrowPermissionDenied(nil, "OIDC-foSyH49RvL", "Errors.User.ProjectRequired")
}
if !permission.ProjectRoleChecked {
return zerrors.ThrowPermissionDenied(nil, "OIDC-foSyH49RvL", "Errors.User.GrantRequired")
}
return nil
}
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
switch v := req.GetCallbackKind().(type) { switch v := req.GetCallbackKind().(type) {
case *oidc_pb.CreateCallbackRequest_Error: case *oidc_pb.CreateCallbackRequest_Error:
@ -101,7 +115,7 @@ func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *
} }
func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) { func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true) details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -5,76 +5,79 @@ package oidc_test
import ( import (
"context" "context"
"net/url" "net/url"
"os"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
) )
var (
CTX context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
)
const (
redirectURI = "oidcintegrationtest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.OIDCv2beta
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
return m.Run()
}())
}
func TestServer_GetAuthRequest(t *testing.T) { func TestServer_GetAuthRequest(t *testing.T) {
project, err := Instance.CreateProject(CTX) project, err := Instance.CreateProject(CTX)
require.NoError(t, err) require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err) require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err)
now := time.Now()
tests := []struct { tests := []struct {
name string name string
AuthRequestID string dep func() (time.Time, string, error)
want *oidc_pb.GetAuthRequestResponse ctx context.Context
wantErr bool want *oidc_pb.GetAuthRequestResponse
wantErr bool
}{ }{
{ {
name: "Not found", name: "Not found",
AuthRequestID: "123", dep: func() (time.Time, string, error) {
wantErr: true, return time.Now(), "123", nil
},
ctx: CTX,
wantErr: true,
}, },
{ {
name: "success", name: "success",
AuthRequestID: authRequestID, dep: func() (time.Time, string, error) {
return Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
},
ctx: CTX,
},
{
name: "without login client, no permission",
dep: func() (time.Time, string, error) {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
},
ctx: CTX,
wantErr: true,
},
{
name: "without login client, with permission",
dep: func() (time.Time, string, error) {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
},
ctx: CTXLoginClient,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{ now, authRequestID, err := tt.dep()
AuthRequestId: tt.AuthRequestID, require.NoError(t, err)
got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: authRequestID,
}) })
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
@ -95,19 +98,13 @@ func TestServer_CreateCallback(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err) require.NoError(t, err)
sessionResp, err := Instance.Client.SessionV2beta.CreateSession(CTX, &session.CreateSessionRequest{ clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: Instance.Users.Get(integration.UserTypeOrgOwner).ID,
},
},
},
})
require.NoError(t, err) require.NoError(t, err)
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct { tests := []struct {
name string name string
ctx context.Context
req *oidc_pb.CreateCallbackRequest req *oidc_pb.CreateCallbackRequest
AuthError string AuthError string
want *oidc_pb.CreateCallbackResponse want *oidc_pb.CreateCallbackResponse
@ -116,6 +113,7 @@ func TestServer_CreateCallback(t *testing.T) {
}{ }{
{ {
name: "Not found", name: "Not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: "123", AuthRequestId: "123",
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -129,9 +127,10 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
{ {
name: "session not found", name: "session not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -146,9 +145,10 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
{ {
name: "session token invalid", name: "session token invalid",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -163,9 +163,36 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
{ {
name: "fail callback", name: "fail callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Error{
Error: &oidc_pb.AuthorizationError{
Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
ErrorDescription: gu.Ptr("nope"),
ErrorUri: gu.Ptr("https://example.com/docs"),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "fail callback, no login client header",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -188,9 +215,53 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
{ {
name: "code callback", name: "code callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "code callback, no login client header, no permission, error",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: true,
},
{
name: "code callback, no login client header, with permission",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -212,6 +283,7 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
{ {
name: "implicit", name: "implicit",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{ req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string { AuthRequestId: func() string {
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil)
@ -236,10 +308,37 @@ func TestServer_CreateCallback(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "implicit, no login client header",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateCallback(CTX, tt.req) got, err := Client.CreateCallback(tt.ctx, tt.req)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
return return
@ -252,3 +351,324 @@ func TestServer_CreateCallback(t *testing.T) {
}) })
} }
} }
func TestServer_CreateCallback_Permission(t *testing.T) {
tests := []struct {
name string
ctx context.Context
dep func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest
want *oidc_pb.CreateCallbackResponse
wantURL *url.URL
wantErr bool
}{
{
name: "usergrant to project and different resourceowner with different project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
projectID2, _ := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant to project and different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "usergrant to project grant and different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "no usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "no usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, true)
user := Instance.CreateHumanUser(ctx)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, true)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, false)
user := Instance.CreateHumanUser(ctx)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant on project grant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant on project grant and different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, true, false)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "hasProjectCheck, same resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
user := Instance.CreateHumanUser(ctx)
_, clientID := createOIDCApplication(ctx, t, false, true)
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "hasProjectCheck, different resourceowner",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
_, clientID := createOIDCApplication(ctx, t, false, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
wantErr: true,
},
{
name: "hasProjectCheck, different resourceowner with project grant",
ctx: CTX,
dep: func(ctx context.Context, t *testing.T) *oidc_pb.CreateCallbackRequest {
projectID, clientID := createOIDCApplication(ctx, t, false, true)
orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId())
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := tt.dep(IAMCTX, t)
got, err := Client.CreateCallback(tt.ctx, req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl())
}
})
}
}
func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse {
sessionResp, err := Instance.Client.SessionV2beta.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
},
})
require.NoError(t, err)
return sessionResp
}
func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, clientID, loginClient, userID string) *oidc_pb.CreateCallbackRequest {
_, authRequestID, err := Instance.CreateOIDCAuthRequest(ctx, clientID, loginClient, redirectURI)
require.NoError(t, err)
sessionResp := createSession(t, ctx, userID)
return &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
}
}
func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) {
project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck)
require.NoError(t, err)
clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
return project.GetId(), clientV2.GetClientId()
}

View File

@ -0,0 +1,44 @@
//go:build integration
package oidc_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
)
var (
CTX context.Context
CTXLoginClient context.Context
IAMCTX context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
)
const (
redirectURI = "oidcintegrationtest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.OIDCv2beta
IAMCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
CTXLoginClient = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
return m.Run()
}())
}

View File

@ -100,8 +100,22 @@ func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *
}, nil }, nil
} }
func (s *Server) checkPermission(ctx context.Context, clientID string, userID string) error {
permission, err := s.query.CheckProjectPermissionByClientID(ctx, clientID, userID)
if err != nil {
return err
}
if !permission.HasProjectChecked {
return zerrors.ThrowPermissionDenied(nil, "OIDC-BakSFPjbfN", "Errors.User.ProjectRequired")
}
if !permission.ProjectRoleChecked {
return zerrors.ThrowPermissionDenied(nil, "OIDC-EP688AF2jA", "Errors.User.GrantRequired")
}
return nil
}
func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) { func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true) details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -5,13 +5,13 @@ package saml_test
import ( import (
"context" "context"
"net/url" "net/url"
"os"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6" "github.com/brianvoe/gofakeit/v6"
"github.com/crewjam/saml" "github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -19,80 +19,48 @@ import (
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2"
) )
var ( func TestServer_GetSAMLRequest(t *testing.T) {
CTX context.Context
Instance *integration.Instance
Client saml_pb.SAMLServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SAMLv2
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
return m.Run()
}())
}
func TestServer_GetAuthRequest(t *testing.T) {
rootURL := "https://sp.example.com"
idpMetadata, err := Instance.GetSAMLIDPMetadata() idpMetadata, err := Instance.GetSAMLIDPMetadata()
require.NoError(t, err) require.NoError(t, err)
spMiddlewareRedirect, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPRedirectBinding)
require.NoError(t, err)
spMiddlewarePost, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPPostBinding)
require.NoError(t, err)
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0] acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1] acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
project, err := Instance.CreateProject(CTX) _, _, spMiddlewareRedirect := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPRedirectBinding, false, false)
require.NoError(t, err) _, _, spMiddlewarePost := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPPostBinding, false, false)
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
require.NoError(t, err)
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
require.NoError(t, err)
now := time.Now()
tests := []struct { tests := []struct {
name string name string
dep func() (string, error) dep func() (time.Time, string, error)
want *oidc_pb.GetAuthRequestResponse
wantErr bool wantErr bool
}{ }{
{ {
name: "Not found", name: "Not found",
dep: func() (string, error) { dep: func() (time.Time, string, error) {
return "123", nil return time.Time{}, "123", nil
}, },
wantErr: true, wantErr: true,
}, },
{ {
name: "success, redirect binding", name: "success, redirect binding",
dep: func() (string, error) { dep: func() (time.Time, string, error) {
return Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding) return Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
}, },
}, },
{ {
name: "success, post binding", name: "success, post binding",
dep: func() (string, error) { dep: func() (time.Time, string, error) {
return Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) return Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
authRequestID, err := tt.dep() creationTime, authRequestID, err := tt.dep()
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@ -108,7 +76,7 @@ func TestServer_GetAuthRequest(t *testing.T) {
authRequest := got.GetSamlRequest() authRequest := got.GetSamlRequest()
assert.NotNil(ttt, authRequest) assert.NotNil(ttt, authRequest)
assert.Equal(ttt, authRequestID, authRequest.GetId()) assert.Equal(ttt, authRequestID, authRequest.GetId())
assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), creationTime.Add(-time.Second), creationTime.Add(time.Second))
}, retryDuration, tick, "timeout waiting for expected saml request result") }, retryDuration, tick, "timeout waiting for expected saml request result")
}) })
} }
@ -117,33 +85,12 @@ func TestServer_GetAuthRequest(t *testing.T) {
func TestServer_CreateResponse(t *testing.T) { func TestServer_CreateResponse(t *testing.T) {
idpMetadata, err := Instance.GetSAMLIDPMetadata() idpMetadata, err := Instance.GetSAMLIDPMetadata()
require.NoError(t, err) require.NoError(t, err)
rootURLRedirect := "spredirect.example.com"
spMiddlewareRedirect, err := integration.CreateSAMLSP("https://"+rootURLRedirect, idpMetadata, saml.HTTPRedirectBinding)
require.NoError(t, err)
rootURLPost := "sppost.example.com"
spMiddlewarePost, err := integration.CreateSAMLSP("https://"+rootURLPost, idpMetadata, saml.HTTPPostBinding)
require.NoError(t, err)
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0] acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1] acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
project, err := Instance.CreateProject(CTX) _, rootURLPost, spMiddlewarePost := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPPostBinding, false, false)
require.NoError(t, err) _, rootURLRedirect, spMiddlewareRedirect := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPRedirectBinding, false, false)
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect) sessionResp := createSession(CTX, t, Instance.Users[integration.UserTypeOrgOwner].ID)
require.NoError(t, err)
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
require.NoError(t, err)
sessionResp, err := Instance.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: Instance.Users[integration.UserTypeOrgOwner].ID,
},
},
},
})
require.NoError(t, err)
tests := []struct { tests := []struct {
name string name string
@ -170,7 +117,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "session not found", name: "session not found",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -187,7 +134,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "session token invalid", name: "session token invalid",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -204,7 +151,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, post", name: "fail callback, post",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -232,7 +179,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, post, already failed", name: "fail callback, post, already failed",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
require.NoError(t, err) require.NoError(t, err)
Instance.FailSAMLAuthRequest(CTX, authRequestID, saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED) Instance.FailSAMLAuthRequest(CTX, authRequestID, saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED)
return authRequestID return authRequestID
@ -250,7 +197,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, redirect", name: "fail callback, redirect",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -275,7 +222,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, redirect", name: "callback, redirect",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -300,7 +247,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, post", name: "callback, post",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
require.NoError(t, err) require.NoError(t, err)
return authRequestID return authRequestID
}(), }(),
@ -328,7 +275,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, post", name: "callback, post",
req: &saml_pb.CreateResponseRequest{ req: &saml_pb.CreateResponseRequest{
SamlRequestId: func() string { SamlRequestId: func() string {
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) _, authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
require.NoError(t, err) require.NoError(t, err)
Instance.SuccessfulSAMLAuthRequest(CTX, Instance.Users[integration.UserTypeOrgOwner].ID, authRequestID) Instance.SuccessfulSAMLAuthRequest(CTX, Instance.Users[integration.UserTypeOrgOwner].ID, authRequestID)
return authRequestID return authRequestID
@ -365,3 +312,338 @@ func TestServer_CreateResponse(t *testing.T) {
}) })
} }
} }
func TestServer_CreateResponse_Permission(t *testing.T) {
idpMetadata, err := Instance.GetSAMLIDPMetadata()
require.NoError(t, err)
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
tests := []struct {
name string
dep func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest
want *saml_pb.CreateResponseResponse
wantURL *url.URL
wantErr bool
}{
{
name: "usergrant to project and different resourceowner with different project grant",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
projectID2, _, _ := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "usergrant to project and different resourceowner with project grant",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "usergrant to project grant and different resourceowner with project grant",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "no usergrant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permisison-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "no usergrant and same resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
user := Instance.CreateHumanUser(ctx)
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "usergrant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permisison-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "usergrant and same resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, usergrant and same resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
user := Instance.CreateHumanUser(ctx)
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and same resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
user := Instance.CreateHumanUser(ctx)
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
orgResp := Instance.CreateOrganization(ctx, "saml-permisison-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
orgResp := Instance.CreateOrganization(ctx, "saml-permisison-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "projectRoleCheck, usergrant on project grant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email())
projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "projectRoleCheck, no usergrant on project grant and different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false)
orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "hasProjectCheck, same resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true)
user := Instance.CreateHumanUser(ctx)
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
{
name: "hasProjectCheck, different resourceowner",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
_, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permisison-"+gofakeit.AppName(), gofakeit.Email())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
wantErr: true,
},
{
name: "hasProjectCheck, different resourceowner with project grant",
dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest {
projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true)
orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email())
Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId())
user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone())
return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding)
},
want: &saml_pb.CreateResponseResponse{
Url: `https:\/\/(.*)\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := tt.dep(IAMCTX, t)
got, err := Client.CreateResponse(CTX, req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.Regexp(t, regexp.MustCompile(tt.want.Url), got.GetUrl())
if tt.want.GetPost() != nil {
assert.NotEmpty(t, got.GetPost().GetRelayState())
assert.NotEmpty(t, got.GetPost().GetSamlResponse())
}
if tt.want.GetRedirect() != nil {
assert.NotNil(t, got.GetRedirect())
}
}
})
}
}
func createSession(ctx context.Context, t *testing.T, userID string) *session.CreateSessionResponse {
sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
},
})
require.NoError(t, err)
return sessionResp
}
func createSessionAndSmlRequestForCallback(ctx context.Context, t *testing.T, sp *samlsp.Middleware, loginClient string, acsRedirect saml.Endpoint, userID, binding string) *saml_pb.CreateResponseRequest {
_, authRequestID, err := Instance.CreateSAMLAuthRequest(sp, loginClient, acsRedirect, gofakeit.BitcoinAddress(), binding)
require.NoError(t, err)
sessionResp := createSession(ctx, t, userID)
return &saml_pb.CreateResponseRequest{
SamlRequestId: authRequestID,
ResponseKind: &saml_pb.CreateResponseRequest_Session{
Session: &saml_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
}
}
func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding string) (string, *samlsp.Middleware) {
rootURL := "example." + gofakeit.DomainName()
spMiddleware, err := integration.CreateSAMLSP("https://"+rootURL, idpMetadata, binding)
require.NoError(t, err)
return rootURL, spMiddleware
}
func createSAMLApplication(ctx context.Context, t *testing.T, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) {
project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck)
require.NoError(t, err)
rootURL, sp := createSAMLSP(t, idpMetadata, binding)
_, err = Instance.CreateSAMLClient(ctx, project.GetId(), sp)
require.NoError(t, err)
return project.GetId(), rootURL, sp
}

View File

@ -0,0 +1,34 @@
//go:build integration
package saml_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
)
var (
CTX context.Context
IAMCTX context.Context
Instance *integration.Instance
Client saml_pb.SAMLServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SAMLv2
IAMCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
return m.Run()
}())
}

View File

@ -14,7 +14,7 @@ import (
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
) )
func (s *Server) GetAuthRequest(ctx context.Context, req *saml_pb.GetSAMLRequestRequest) (*saml_pb.GetSAMLRequestResponse, error) { func (s *Server) GetSAMLRequest(ctx context.Context, req *saml_pb.GetSAMLRequestRequest) (*saml_pb.GetSAMLRequestResponse, error) {
authRequest, err := s.query.SamlRequestByID(ctx, true, req.GetSamlRequestId(), true) authRequest, err := s.query.SamlRequestByID(ctx, true, req.GetSamlRequestId(), true)
if err != nil { if err != nil {
logging.WithError(err).Error("query samlRequest by ID") logging.WithError(err).Error("query samlRequest by ID")
@ -56,8 +56,22 @@ func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *
return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil
} }
func (s *Server) checkPermission(ctx context.Context, issuer string, userID string) error {
permission, err := s.query.CheckProjectPermissionByEntityID(ctx, issuer, userID)
if err != nil {
return err
}
if !permission.HasProjectChecked {
return zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.User.ProjectRequired")
}
if !permission.ProjectRoleChecked {
return zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.User.GrantRequired")
}
return nil
}
func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*saml_pb.CreateResponseResponse, error) { func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*saml_pb.CreateResponseResponse, error) {
details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true) details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -695,6 +695,12 @@ func TestServer_ListSessions(t *testing.T) {
return return
} }
// expected count of sessions is not equal to created dependencies
if !assert.Len(ttt, tt.want.Sessions, len(infos)) {
return
}
// expected count of sessions is not equal to received sessions
if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) { if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) {
return return
} }
@ -705,8 +711,17 @@ func TestServer_ListSessions(t *testing.T) {
tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate() tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate()
tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate() tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate()
verifySession(ttt, got.Sessions[i], tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...) // only check for contents of the session, not sorting for now
found := false
for _, session := range got.Sessions {
if session.Id == infos[i].ID {
verifySession(ttt, session, tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...)
found = true
}
}
assert.True(t, found)
} }
integration.AssertListDetails(ttt, tt.want, got) integration.AssertListDetails(ttt, tt.want, got)
}, retryDuration, tick) }, retryDuration, tick)
}) })

View File

@ -307,7 +307,7 @@ func TestServer_VerifyClient(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID) _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID)
require.NoError(t, err) require.NoError(t, err)
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,

View File

@ -441,13 +441,13 @@ func createImplicitClientNoLoginClientHeader(t testing.TB) string {
} }
func createAuthRequest(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string { func createAuthRequest(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string {
redURL, err := instance.CreateOIDCAuthRequest(CTX, clientID, instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, scope...) _, redURL, err := instance.CreateOIDCAuthRequest(CTX, clientID, instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, scope...)
require.NoError(t, err) require.NoError(t, err)
return redURL return redURL
} }
func createAuthRequestNoLoginClientHeader(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string { func createAuthRequestNoLoginClientHeader(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string {
redURL, err := instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientID, redirectURI, "", scope...) _, redURL, err := instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientID, redirectURI, "", scope...)
require.NoError(t, err) require.NoError(t, err)
return redURL return redURL
} }

View File

@ -80,7 +80,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest)
return authRequestWriteModelToCurrentAuthRequest(writeModel), nil return authRequestWriteModelToCurrentAuthRequest(writeModel), nil
} }
func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentAuthRequest, error) { func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool, projectPermissionCheck domain.ProjectPermissionCheck) (*domain.ObjectDetails, *CurrentAuthRequest, error) {
writeModel, err := c.getAuthRequestWriteModel(ctx, id) writeModel, err := c.getAuthRequestWriteModel(ctx, id)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -96,6 +96,7 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
return nil, nil, err return nil, nil, err
} }
} }
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
if err != nil { if err != nil {
@ -108,6 +109,12 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
return nil, nil, err return nil, nil, err
} }
if projectPermissionCheck != nil {
if err := projectPermissionCheck(ctx, writeModel.ClientID, sessionWriteModel.UserID); err != nil {
return nil, nil, err
}
}
if err := c.pushAppendAndReduce(ctx, writeModel, authrequest.NewSessionLinkedEvent( if err := c.pushAppendAndReduce(ctx, writeModel, authrequest.NewSessionLinkedEvent(
ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate, ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
sessionID, sessionID,

View File

@ -181,6 +181,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
sessionID string sessionID string
sessionToken string sessionToken string
checkLoginClient bool checkLoginClient bool
permissionCheck domain.ProjectPermissionCheck
} }
type res struct { type res struct {
details *domain.ObjectDetails details *domain.ObjectDetails
@ -712,6 +713,163 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
}, },
}, },
}, },
{
"linked with permission, application permission check",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"otherLoginClient",
"clientID",
"redirectURI",
"state",
"nonce",
[]string{"openid"},
[]string{"audience"},
domain.OIDCResponseTypeCode,
domain.OIDCResponseModeQuery,
nil,
nil,
nil,
nil,
nil,
nil,
true,
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
expectPush(
authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"sessionID",
"userID",
testNow,
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
checkLoginClient: true,
permissionCheck: newMockProjectPermissionCheckAllowed(),
},
res{
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
authReq: &CurrentAuthRequest{
AuthRequest: &AuthRequest{
ID: "V2_id",
LoginClient: "otherLoginClient",
ClientID: "clientID",
RedirectURI: "redirectURI",
State: "state",
Nonce: "nonce",
Scope: []string{"openid"},
Audience: []string{"audience"},
ResponseType: domain.OIDCResponseTypeCode,
ResponseMode: domain.OIDCResponseModeQuery,
},
SessionID: "sessionID",
UserID: "userID",
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
},
},
},
{
"linked with permission, no application permission",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"otherLoginClient",
"clientID",
"redirectURI",
"state",
"nonce",
[]string{"openid"},
[]string{"audience"},
domain.OIDCResponseTypeCode,
domain.OIDCResponseModeQuery,
nil,
nil,
nil,
nil,
nil,
nil,
true,
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
checkLoginClient: true,
permissionCheck: newMockProjectPermissionCheckOIDCNotAllowed(),
},
res{
wantErr: zerrors.ThrowPermissionDenied(nil, "OIDC-foSyH49RvL", "Errors.PermissionDenied"),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -720,7 +878,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
sessionTokenVerifier: tt.fields.tokenVerifier, sessionTokenVerifier: tt.fields.tokenVerifier,
checkPermission: tt.fields.checkPermission, checkPermission: tt.fields.checkPermission,
} }
details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient) details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient, tt.args.permissionCheck)
require.ErrorIs(t, err, tt.res.wantErr) require.ErrorIs(t, err, tt.res.wantErr)
assertObjectDetails(t, tt.res.details, details) assertObjectDetails(t, tt.res.details, details)
if err == nil { if err == nil {

View File

@ -232,6 +232,24 @@ func newMockPermissionCheckNotAllowed() domain.PermissionCheck {
} }
} }
func newMockProjectPermissionCheckAllowed() domain.ProjectPermissionCheck {
return func(ctx context.Context, clientID, userID string) (err error) {
return nil
}
}
func newMockProjectPermissionCheckOIDCNotAllowed() domain.ProjectPermissionCheck {
return func(ctx context.Context, clientID, userID string) (err error) {
return zerrors.ThrowPermissionDenied(nil, "OIDC-foSyH49RvL", "Errors.PermissionDenied")
}
}
func newMockProjectPermissionCheckSAMLNotAllowed() domain.ProjectPermissionCheck {
return func(ctx context.Context, clientID, userID string) (err error) {
return zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.PermissionDenied")
}
}
func newMockTokenVerifierValid() func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { func newMockTokenVerifierValid() func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return nil return nil

View File

@ -63,7 +63,7 @@ func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest)
return samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil return samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
} }
func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentSAMLRequest, error) { func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool, projectPermissionCheck domain.ProjectPermissionCheck) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
writeModel, err := c.getSAMLRequestWriteModel(ctx, id) writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -89,6 +89,12 @@ func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID,
return nil, nil, err return nil, nil, err
} }
if projectPermissionCheck != nil {
if err := projectPermissionCheck(ctx, writeModel.Issuer, sessionWriteModel.UserID); err != nil {
return nil, nil, err
}
}
if err := c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewSessionLinkedEvent( if err := c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewSessionLinkedEvent(
ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate, ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
sessionID, sessionID,

View File

@ -141,6 +141,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
sessionID string sessionID string
sessionToken string sessionToken string
checkLoginClient bool checkLoginClient bool
checkPermission domain.ProjectPermissionCheck
} }
type res struct { type res struct {
details *domain.ObjectDetails details *domain.ObjectDetails
@ -524,6 +525,144 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
}, },
}, },
}, },
{
"linked with login client check, application permission check",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"loginClient",
"application",
"acs",
"relaystate",
"request",
"binding",
"issuer",
"destination",
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
expectPush(
samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"sessionID",
"userID",
testNow,
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
checkLoginClient: true,
checkPermission: newMockProjectPermissionCheckAllowed(),
},
res{
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
authReq: &CurrentSAMLRequest{
SAMLRequest: &SAMLRequest{
ID: "V2_id",
LoginClient: "loginClient",
ApplicationID: "application",
ACSURL: "acs",
RelayState: "relaystate",
RequestID: "request",
Binding: "binding",
Issuer: "issuer",
Destination: "destination",
},
SessionID: "sessionID",
UserID: "userID",
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
},
},
},
{
"linked with login client check, no application permission",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"loginClient",
"application",
"acs",
"relaystate",
"request",
"binding",
"issuer",
"destination",
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
checkLoginClient: true,
checkPermission: newMockProjectPermissionCheckSAMLNotAllowed(),
},
res{
wantErr: zerrors.ThrowPermissionDenied(nil, "SAML-foSyH49RvL", "Errors.PermissionDenied"),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -531,7 +670,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
sessionTokenVerifier: tt.fields.tokenVerifier, sessionTokenVerifier: tt.fields.tokenVerifier,
} }
details, got, err := c.LinkSessionToSAMLRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient) details, got, err := c.LinkSessionToSAMLRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient, tt.args.checkPermission)
require.ErrorIs(t, err, tt.res.wantErr) require.ErrorIs(t, err, tt.res.wantErr)
assertObjectDetails(t, tt.res.details, details) assertObjectDetails(t, tt.res.details, details)
if err == nil { if err == nil {

View File

@ -39,3 +39,7 @@ const (
PermissionIDPRead = "iam.idp.read" PermissionIDPRead = "iam.idp.read"
PermissionOrgIDPRead = "org.idp.read" PermissionOrgIDPRead = "org.idp.read"
) )
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
// Configurable on the project the application belongs to through the flags related to authentication.
type ProjectPermissionCheck func(ctx context.Context, clientID, userID string) (err error)

View File

@ -678,6 +678,15 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
} }
func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse {
resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{
GrantedOrgId: grantedOrgID,
ProjectId: projectID,
})
logging.OnError(err).Panic("create project grant")
return resp
}
func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string { func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string {
resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{ resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
UserId: userID, UserId: userID,
@ -687,6 +696,16 @@ func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, pro
return resp.GetUserGrantId() return resp.GetUserGrantId()
} }
func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) string {
resp, err := i.Client.Mgmt.AddUserGrant(SetOrgID(ctx, orgID), &mgmt.AddUserGrantRequest{
UserId: userID,
ProjectId: projectID,
ProjectGrantId: projectGrantID,
})
logging.OnError(err).Panic("create project grant user grant")
return resp.GetUserGrantId()
}
func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) { func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{ _, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
UserId: userID, UserId: userID,

View File

@ -194,6 +194,14 @@ func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectRes
}) })
} }
func (i *Instance) CreateProjectWithPermissionCheck(ctx context.Context, projectRoleCheck, hasProjectCheck bool) (*management.AddProjectResponse, error) {
return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
HasProjectCheck: hasProjectCheck,
ProjectRoleCheck: projectRoleCheck,
})
}
func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID, ProjectId: projectID,
@ -212,22 +220,22 @@ func (i *Instance) CreateAPIClientBasic(ctx context.Context, projectID string) (
const CodeVerifier = "codeVerifier" const CodeVerifier = "codeVerifier"
func (i *Instance) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { func (i *Instance) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (now time.Time, authRequestID string, err error) {
return i.CreateOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, loginClient, redirectURI, scope...) return i.CreateOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, loginClient, redirectURI, scope...)
} }
func (i *Instance) CreateOIDCAuthRequestWithoutLoginClientHeader(ctx context.Context, clientID, redirectURI, loginBaseURI string, scope ...string) (authRequestID string, err error) { func (i *Instance) CreateOIDCAuthRequestWithoutLoginClientHeader(ctx context.Context, clientID, redirectURI, loginBaseURI string, scope ...string) (now time.Time, authRequestID string, err error) {
return i.createOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, redirectURI, "", loginBaseURI, scope...) return i.createOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, redirectURI, "", loginBaseURI, scope...)
} }
func (i *Instance) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { func (i *Instance) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (now time.Time, authRequestID string, err error) {
return i.createOIDCAuthRequestWithDomain(ctx, domain, clientID, redirectURI, loginClient, "", scope...) return i.createOIDCAuthRequestWithDomain(ctx, domain, clientID, redirectURI, loginClient, "", scope...)
} }
func (i *Instance) createOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, redirectURI, loginClient, loginBaseURI string, scope ...string) (authRequestID string, err error) { func (i *Instance) createOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, redirectURI, loginClient, loginBaseURI string, scope ...string) (now time.Time, authRequestID string, err error) {
provider, err := i.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, loginClient, scope...) provider, err := i.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, loginClient, scope...)
if err != nil { if err != nil {
return "", fmt.Errorf("create relying party: %w", err) return now, "", fmt.Errorf("create relying party: %w", err)
} }
codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier) codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier)
authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge)) authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge))
@ -238,21 +246,22 @@ func (i *Instance) createOIDCAuthRequestWithDomain(ctx context.Context, domain,
} }
req, err := GetRequest(authURL, headers) req, err := GetRequest(authURL, headers)
if err != nil { if err != nil {
return "", fmt.Errorf("get request: %w", err) return now, "", fmt.Errorf("get request: %w", err)
} }
now = time.Now()
loc, err := CheckRedirect(req) loc, err := CheckRedirect(req)
if err != nil { if err != nil {
return "", fmt.Errorf("check redirect: %w", err) return now, "", fmt.Errorf("check redirect: %w", err)
} }
if loginBaseURI == "" { if loginBaseURI == "" {
loginBaseURI = provider.Issuer() + i.Config.LoginURLV2 loginBaseURI = provider.Issuer() + i.Config.LoginURLV2
} }
if !strings.HasPrefix(loc.String(), loginBaseURI) { if !strings.HasPrefix(loc.String(), loginBaseURI) {
return "", fmt.Errorf("login location has not prefix %s, but is %s", loginBaseURI, loc.String()) return now, "", fmt.Errorf("login location has not prefix %s, but is %s", loginBaseURI, loc.String())
} }
return strings.TrimPrefix(loc.String(), loginBaseURI), nil return now, strings.TrimPrefix(loc.String(), loginBaseURI), nil
} }
func (i *Instance) CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(ctx context.Context, clientID, redirectURI string, scope ...string) (authRequestID string, err error) { func (i *Instance) CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(ctx context.Context, clientID, redirectURI string, scope ...string) (authRequestID string, err error) {

View File

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/brianvoe/gofakeit/v6" "github.com/brianvoe/gofakeit/v6"
"github.com/crewjam/saml" "github.com/crewjam/saml"
@ -135,32 +136,33 @@ func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *sa
}) })
} }
func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (authRequestID string, err error) { func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (now time.Time, authRequestID string, err error) {
authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding) authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding)
if err != nil { if err != nil {
return "", err return now, "", err
} }
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider) redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
if err != nil { if err != nil {
return "", err return now, "", err
} }
req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient}) req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient})
if err != nil { if err != nil {
return "", fmt.Errorf("get request: %w", err) return now, "", fmt.Errorf("get request: %w", err)
} }
now = time.Now()
loc, err := CheckRedirect(req) loc, err := CheckRedirect(req)
if err != nil { if err != nil {
return "", fmt.Errorf("check redirect: %w", err) return now, "", fmt.Errorf("check redirect: %w", err)
} }
prefixWithHost := i.Issuer() + i.Config.LoginURLV2 prefixWithHost := i.Issuer() + i.Config.LoginURLV2
if !strings.HasPrefix(loc.String(), prefixWithHost) { if !strings.HasPrefix(loc.String(), prefixWithHost) {
return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String()) return now, "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
} }
return strings.TrimPrefix(loc.String(), prefixWithHost), nil return now, strings.TrimPrefix(loc.String(), prefixWithHost), nil
} }
func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse { func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse {

View File

@ -79,7 +79,7 @@ func TestServer_TelemetryPushMilestones(t *testing.T) {
func loginToClient(t *testing.T, instance *integration.Instance, clientID, redirectURI, sessionID, sessionToken string) { func loginToClient(t *testing.T, instance *integration.Instance, clientID, redirectURI, sessionID, sessionToken string) {
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
authRequestID, err := instance.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, instance.Domain, clientID, instance.Users.Get(integration.UserTypeIAMOwner).ID, redirectURI, "openid") _, authRequestID, err := instance.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, instance.Domain, clientID, instance.Users.Get(integration.UserTypeIAMOwner).ID, redirectURI, "openid")
require.NoError(t, err) require.NoError(t, err)
callback, err := instance.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{ callback, err := instance.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,

View File

@ -3,6 +3,7 @@ package query
import ( import (
"context" "context"
"database/sql" "database/sql"
_ "embed"
"errors" "errors"
"time" "time"
@ -368,6 +369,77 @@ func (q *Queries) ProjectByClientID(ctx context.Context, appID string) (project
return project, err return project, err
} }
//go:embed app_oidc_project_permission.sql
var appOIDCProjectPermissionQuery string
func (q *Queries) CheckProjectPermissionByClientID(ctx context.Context, clientID, userID string) (_ *projectPermission, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
var p *projectPermission
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
p, err = scanProjectPermissionByClientID(row)
return err
}, appOIDCProjectPermissionQuery,
authz.GetInstance(ctx).InstanceID(),
clientID,
domain.AppStateActive,
domain.ProjectStateActive,
userID,
domain.UserStateActive,
domain.ProjectGrantStateActive,
domain.UserGrantStateActive,
)
return p, err
}
//go:embed app_saml_project_permission.sql
var appSAMLProjectPermissionQuery string
func (q *Queries) CheckProjectPermissionByEntityID(ctx context.Context, entityID, userID string) (_ *projectPermission, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
var p *projectPermission
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
p, err = scanProjectPermissionByClientID(row)
return err
}, appSAMLProjectPermissionQuery,
authz.GetInstance(ctx).InstanceID(),
entityID,
domain.AppStateActive,
domain.ProjectStateActive,
userID,
domain.UserStateActive,
domain.ProjectGrantStateActive,
domain.UserGrantStateActive,
)
return p, err
}
type projectPermission struct {
HasProjectChecked bool
ProjectRoleChecked bool
}
func scanProjectPermissionByClientID(row *sql.Row) (*projectPermission, error) {
var hasProjectChecked, projectRoleChecked sql.NullBool
err := row.Scan(
&hasProjectChecked,
&projectRoleChecked,
)
if err != nil || !hasProjectChecked.Valid || !projectRoleChecked.Valid {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-4tq8wCTCgf", "Errors.App.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-NwH4lAqlZC", "Errors.Internal")
}
return &projectPermission{
HasProjectChecked: hasProjectChecked.Bool,
ProjectRoleChecked: projectRoleChecked.Bool,
}, nil
}
func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id string, err error) { func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id string, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()

View File

@ -0,0 +1,74 @@
with application as (
SELECT a.instance_id,
a.resource_owner,
a.project_id,
a.id as app_id,
p.project_role_check,
p.has_project_check
FROM projections.apps7 as a
LEFT JOIN projections.apps7_oidc_configs as aoc
ON aoc.app_id = a.id
AND aoc.instance_id = a.instance_id
INNER JOIN projections.projects4 as p
ON p.instance_id = a.instance_id
AND p.resource_owner = a.resource_owner
AND p.id = a.project_id
WHERE a.instance_id = $1
AND aoc.client_id = $2
AND a.state = $3
AND p.state = $4
), user_resourceowner as (
/* resourceowner of the active user */
SELECT u.instance_id,
u.resource_owner,
u.id as user_id
FROM projections.users14 as u
WHERE u.instance_id = $1
AND u.id = $5
AND u.state = $6
), has_project_grant_check as (
/* all projectgrants active, then filtered with the project and user resourceowner */
SELECT pg.instance_id,
pg.resource_owner,
pg.project_id,
pg.granted_org_id
FROM projections.project_grants4 as pg
WHERE pg.instance_id = $1
AND pg.state = $7
), project_role_check as (
/* all usergrants active and associated with the user, then filtered with the project */
SELECT ug.instance_id,
ug.resource_owner,
ug.project_id
FROM projections.user_grants5 as ug
WHERE ug.instance_id = $1
AND ug.user_id = $5
AND ug.state = $8
)
SELECT
/* project existence does not need to be checked, or resourceowner of user and project are equal, or resourceowner of user has project granted*/
bool_and(COALESCE(
(NOT a.has_project_check OR
a.resource_owner = uro.resource_owner OR
uro.resource_owner = hpgc.granted_org_id)
, FALSE)
) as project_checked,
/* authentication existence does not need to checked, or authentication for project is existing*/
bool_and(COALESCE(
(NOT a.project_role_check OR
a.project_id = prc.project_id)
, FALSE)
) as role_checked
FROM application as a
LEFT JOIN user_resourceowner as uro
ON uro.instance_id = a.instance_id
LEFT JOIN has_project_grant_check as hpgc
ON hpgc.instance_id = a.instance_id
AND hpgc.project_id = a.project_id
AND hpgc.granted_org_id = uro.resource_owner
LEFT JOIN project_role_check as prc
ON prc.instance_id = a.instance_id
AND prc.project_id = a.project_id
GROUP BY a.instance_id, a.resource_owner, a.project_id, a.app_id, uro.resource_owner, hpgc.granted_org_id,
prc.project_id
LIMIT 1;

View File

@ -0,0 +1,74 @@
with application as (
SELECT a.instance_id,
a.resource_owner,
a.project_id,
a.id as app_id,
p.project_role_check,
p.has_project_check
FROM projections.apps7 as a
LEFT JOIN projections.apps7_saml_configs as asaml
ON asaml.app_id = a.id
AND asaml.instance_id = a.instance_id
INNER JOIN projections.projects4 as p
ON p.instance_id = a.instance_id
AND p.resource_owner = a.resource_owner
AND p.id = a.project_id
WHERE a.instance_id = $1
AND asaml.entity_id = $2
AND a.state = $3
AND p.state = $4
), user_resourceowner as (
/* resourceowner of the active user */
SELECT u.instance_id,
u.resource_owner,
u.id as user_id
FROM projections.users14 as u
WHERE u.instance_id = $1
AND u.id = $5
AND u.state = $6
), has_project_grant_check as (
/* all projectgrants active, then filtered with the project and user resourceowner */
SELECT pg.instance_id,
pg.resource_owner,
pg.project_id,
pg.granted_org_id
FROM projections.project_grants4 as pg
WHERE pg.instance_id = $1
AND pg.state = $7
), project_role_check as (
/* all usergrants active and associated with the user, then filtered with the project */
SELECT ug.instance_id,
ug.resource_owner,
ug.project_id
FROM projections.user_grants5 as ug
WHERE ug.instance_id = $1
AND ug.user_id = $5
AND ug.state = $8
)
SELECT
/* project existence does not need to be checked, or resourceowner of user and project are equal, or resourceowner of user has project granted*/
bool_and(COALESCE(
(NOT a.has_project_check OR
a.resource_owner = uro.resource_owner OR
uro.resource_owner = hpgc.granted_org_id)
, FALSE)
) as project_checked,
/* authentication existence does not need to checked, or authentication for project is existing*/
bool_and(COALESCE(
(NOT a.project_role_check OR
a.project_id = prc.project_id)
, FALSE)
) as role_checked
FROM application as a
LEFT JOIN user_resourceowner as uro
ON uro.instance_id = a.instance_id
LEFT JOIN has_project_grant_check as hpgc
ON hpgc.instance_id = a.instance_id
AND hpgc.project_id = a.project_id
AND hpgc.granted_org_id = uro.resource_owner
LEFT JOIN project_role_check as prc
ON prc.instance_id = a.instance_id
AND prc.project_id = a.project_id
GROUP BY a.instance_id, a.resource_owner, a.project_id, a.app_id, uro.resource_owner, hpgc.granted_org_id,
prc.project_id
LIMIT 1;