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 (
"context"
"net/url"
"os"
"regexp"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -22,92 +22,62 @@ import (
"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) {
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
now := time.Now()
tests := []struct {
name string
AuthRequestID string
dep func() (time.Time, string, error)
ctx context.Context
want *oidc_pb.GetAuthRequestResponse
wantErr bool
}{
{
name: "Not found",
AuthRequestID: "123",
dep: func() (time.Time, string, error) {
return time.Now(), "123", nil
},
ctx: CTX,
wantErr: true,
},
{
name: "success",
AuthRequestID: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err)
return 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",
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)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
},
ctx: CTX,
wantErr: true,
},
{
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)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
return Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
},
ctx: CTXLoginClient,
},
}
for _, tt := range tests {
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{
AuthRequestId: tt.AuthRequestID,
AuthRequestId: authRequestID,
})
if tt.wantErr {
require.Error(t, err)
@ -116,7 +86,7 @@ func TestServer_GetAuthRequest(t *testing.T) {
require.NoError(t, err)
authRequest := got.GetAuthRequest()
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.Contains(t, authRequest.GetScope(), "openid")
})
@ -130,16 +100,7 @@ func TestServer_CreateCallback(t *testing.T) {
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)
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)
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct {
name string
@ -169,7 +130,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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)
return authRequestID
}(),
@ -187,7 +148,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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
}(),
@ -205,7 +166,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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
}(),
@ -231,7 +192,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
@ -257,7 +218,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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
}(),
@ -282,7 +243,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
@ -300,7 +261,7 @@ func TestServer_CreateCallback(t *testing.T) {
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
_, authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
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) {
switch v := req.GetCallbackKind().(type) {
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) {
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 {
return nil, err
}

View File

@ -5,76 +5,79 @@ package oidc_test
import (
"context"
"net/url"
"os"
"regexp"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/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) {
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
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 {
name string
AuthRequestID string
dep func() (time.Time, string, error)
ctx context.Context
want *oidc_pb.GetAuthRequestResponse
wantErr bool
}{
{
name: "Not found",
AuthRequestID: "123",
dep: func() (time.Time, string, error) {
return time.Now(), "123", nil
},
ctx: CTX,
wantErr: true,
},
{
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 {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: tt.AuthRequestID,
now, authRequestID, err := tt.dep()
require.NoError(t, err)
got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: authRequestID,
})
if tt.wantErr {
require.Error(t, err)
@ -95,19 +98,13 @@ func TestServer_CreateCallback(t *testing.T) {
require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
sessionResp, err := Instance.Client.SessionV2beta.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: Instance.Users.Get(integration.UserTypeOrgOwner).ID,
},
},
},
})
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)
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct {
name string
ctx context.Context
req *oidc_pb.CreateCallbackRequest
AuthError string
want *oidc_pb.CreateCallbackResponse
@ -116,6 +113,7 @@ func TestServer_CreateCallback(t *testing.T) {
}{
{
name: "Not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: "123",
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -129,9 +127,10 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "session not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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)
return authRequestID
}(),
@ -146,9 +145,10 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "session token invalid",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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
}(),
@ -163,9 +163,36 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "fail callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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)
return authRequestID
}(),
@ -188,9 +215,53 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "code callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
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)
return authRequestID
}(),
@ -212,6 +283,7 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "implicit",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil)
@ -236,10 +308,37 @@ func TestServer_CreateCallback(t *testing.T) {
},
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 {
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 {
require.Error(t, err)
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
}
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) {
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 {
return nil, err
}

View File

@ -5,13 +5,13 @@ package saml_test
import (
"context"
"net/url"
"os"
"regexp"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -19,80 +19,48 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"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"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (
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"
func TestServer_GetSAMLRequest(t *testing.T) {
idpMetadata, err := Instance.GetSAMLIDPMetadata()
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]
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
_, 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()
_, _, spMiddlewareRedirect := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPRedirectBinding, false, false)
_, _, spMiddlewarePost := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPPostBinding, false, false)
tests := []struct {
name string
dep func() (string, error)
want *oidc_pb.GetAuthRequestResponse
dep func() (time.Time, string, error)
wantErr bool
}{
{
name: "Not found",
dep: func() (string, error) {
return "123", nil
dep: func() (time.Time, string, error) {
return time.Time{}, "123", nil
},
wantErr: true,
},
{
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)
},
},
{
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)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authRequestID, err := tt.dep()
creationTime, authRequestID, err := tt.dep()
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@ -108,7 +76,7 @@ func TestServer_GetAuthRequest(t *testing.T) {
authRequest := got.GetSamlRequest()
assert.NotNil(ttt, authRequest)
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")
})
}
@ -117,33 +85,12 @@ func TestServer_GetAuthRequest(t *testing.T) {
func TestServer_CreateResponse(t *testing.T) {
idpMetadata, err := Instance.GetSAMLIDPMetadata()
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]
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
project, err := Instance.CreateProject(CTX)
require.NoError(t, err)
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
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)
_, rootURLPost, spMiddlewarePost := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPPostBinding, false, false)
_, rootURLRedirect, spMiddlewareRedirect := createSAMLApplication(CTX, t, idpMetadata, saml.HTTPRedirectBinding, false, false)
sessionResp := createSession(CTX, t, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct {
name string
@ -170,7 +117,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "session not found",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -187,7 +134,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "session token invalid",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -204,7 +151,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, post",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -232,7 +179,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, post, already failed",
req: &saml_pb.CreateResponseRequest{
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)
Instance.FailSAMLAuthRequest(CTX, authRequestID, saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED)
return authRequestID
@ -250,7 +197,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "fail callback, redirect",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -275,7 +222,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, redirect",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -300,7 +247,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, post",
req: &saml_pb.CreateResponseRequest{
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)
return authRequestID
}(),
@ -328,7 +275,7 @@ func TestServer_CreateResponse(t *testing.T) {
name: "callback, post",
req: &saml_pb.CreateResponseRequest{
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)
Instance.SuccessfulSAMLAuthRequest(CTX, Instance.Users[integration.UserTypeOrgOwner].ID, 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"
)
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)
if err != nil {
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
}
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) {
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 {
return nil, err
}

View File

@ -695,6 +695,12 @@ func TestServer_ListSessions(t *testing.T) {
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)) {
return
}
@ -705,8 +711,17 @@ func TestServer_ListSessions(t *testing.T) {
tt.want.Sessions[i].CreationDate = 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)
}, retryDuration, tick)
})

View File

@ -307,7 +307,7 @@ func TestServer_VerifyClient(t *testing.T) {
}
for _, tt := range tests {
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)
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
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 {
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)
return redURL
}
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)
return redURL
}

View File

@ -80,7 +80,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest)
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)
if err != nil {
return nil, nil, err
@ -96,6 +96,7 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
return nil, nil, err
}
}
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
if err != nil {
@ -108,6 +109,12 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
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(
ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
sessionID,

View File

@ -181,6 +181,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
sessionID string
sessionToken string
checkLoginClient bool
permissionCheck domain.ProjectPermissionCheck
}
type res struct {
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 {
t.Run(tt.name, func(t *testing.T) {
@ -720,7 +878,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
sessionTokenVerifier: tt.fields.tokenVerifier,
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)
assertObjectDetails(t, tt.res.details, details)
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) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return nil

View File

@ -63,7 +63,7 @@ func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest)
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)
if err != nil {
return nil, nil, err
@ -89,6 +89,12 @@ func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID,
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(
ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
sessionID,

View File

@ -141,6 +141,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
sessionID string
sessionToken string
checkLoginClient bool
checkPermission domain.ProjectPermissionCheck
}
type res struct {
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 {
t.Run(tt.name, func(t *testing.T) {
@ -531,7 +670,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
eventstore: tt.fields.eventstore(t),
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)
assertObjectDetails(t, tt.res.details, details)
if err == nil {

View File

@ -39,3 +39,7 @@ const (
PermissionIDPRead = "iam.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()
}
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 {
resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
UserId: userID,
@ -687,6 +696,16 @@ func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, pro
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) {
_, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
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) {
return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID,
@ -212,22 +220,22 @@ func (i *Instance) CreateAPIClientBasic(ctx context.Context, projectID string) (
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...)
}
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...)
}
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...)
}
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...)
if err != nil {
return "", fmt.Errorf("create relying party: %w", err)
return now, "", fmt.Errorf("create relying party: %w", err)
}
codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier)
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)
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)
if err != nil {
return "", fmt.Errorf("check redirect: %w", err)
return now, "", fmt.Errorf("check redirect: %w", err)
}
if loginBaseURI == "" {
loginBaseURI = provider.Issuer() + i.Config.LoginURLV2
}
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) {

View File

@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/brianvoe/gofakeit/v6"
"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)
if err != nil {
return "", err
return now, "", err
}
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
if err != nil {
return "", err
return now, "", err
}
req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient})
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)
if err != nil {
return "", fmt.Errorf("check redirect: %w", err)
return now, "", fmt.Errorf("check redirect: %w", err)
}
prefixWithHost := i.Issuer() + i.Config.LoginURLV2
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 {

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) {
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)
callback, err := instance.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{
AuthRequestId: authRequestID,

View File

@ -3,6 +3,7 @@ package query
import (
"context"
"database/sql"
_ "embed"
"errors"
"time"
@ -368,6 +369,77 @@ func (q *Queries) ProjectByClientID(ctx context.Context, appID string) (project
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) {
ctx, span := tracing.NewSpan(ctx)
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;