Merge branch 'main' into lookup-table

This commit is contained in:
adlerhurst 2024-06-12 11:17:08 +02:00
commit c20150c1a8
28 changed files with 307 additions and 89 deletions

View File

@ -120,6 +120,10 @@ Database:
Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
Admin:
# By default, ExistingDatabase is not specified in the connection string
# If the connection resolves to a database that is not existing in your system, configure an existing one here
# It is used in zitadel init to connect to cockroach and create a dedicated database for ZITADEL.
ExistingDatabase: # ZITADEL_DATABASE_COCKROACH_ADMIN_EXISTINGDATABASE
Username: root # ZITADEL_DATABASE_COCKROACH_ADMIN_USERNAME
Password: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_PASSWORD
SSL:
@ -147,6 +151,10 @@ Database:
Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
Admin:
# The default ExistingDatabase is postgres
# If your db system doesn't have a database named postgres, configure an existing database here
# It is used in zitadel init to connect to postgres and create a dedicated database for ZITADEL.
ExistingDatabase: # ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE
Username: # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME
Password: # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
SSL:

View File

@ -34,6 +34,10 @@ Order of execution:
3. mirror event store tables
4. recompute projections
5. verify`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := viper.MergeConfig(bytes.NewBuffer(defaultConfig))
logging.OnError(err).Fatal("unable to read default config")
},
Run: func(cmd *cobra.Command, args []string) {
config := mustNewMigrationConfig(viper.GetViper())
projectionConfig := mustNewProjectionsConfig(viper.GetViper())
@ -59,9 +63,6 @@ Order of execution:
The flag should be provided if you want to execute the mirror command multiple times so that the static data are also mirrored to prevent inconsistent states.`)
migrateProjectionsFlags(cmd)
err := viper.MergeConfig(bytes.NewBuffer(defaultConfig))
logging.OnError(err).Fatal("unable to read default config")
cmd.AddCommand(
eventstoreCmd(),
systemCmd(),

View File

@ -0,0 +1,37 @@
---
title: ZITADEL Terraform Provider
sidebar_label: Terraform Provider
---
The [ZITADEL Terraform Provider](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs) is a tool that allows you to manage ZITADEL resources through Terraform.
In other words, it lets you define and provision infrastructure for ZITADEL using Terraform configuration files.
This Terraform provider acts as a bridge, allowing you to manage various aspects of your ZITADEL instance directly through the [ZITADEL API](/docs/apis/introduction), using Terraform's declarative configuration language.
It can be used to create, update, and delete ZITADEL resources, as well as to manage the relationships between those resources.
## Before you start
Make sure you create the following resources in ZITADEL and have [Terraform installed](https://learn.hashicorp.com/tutorials/terraform/install-cli):
- [A ZITADEL Instance](../start/quickstart)
- [A service user](/docs/guides/integrate/service-users/authenticate-service-users) with [enough authorization](/docs/guides/manage/console/managers) to manage the desired resources
## Manage ZITADEL resources through terraform
The full documentation and examples are available on the [Terraform registry](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
To provide a small guide to where to start:
1. Create a folder where all the terraform files reside.
2. Configure the provider to use the right domain, port and token, with for example a `main.tf`file [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
3. Add a `zitadel_org` resource to the `main.tf` file, to create and manage a new organization in the instance, [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/org).
4. Add any resources to the organization in the `main.tf` file, [as example a human user](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/human_user).
5. (Optional) Use Terraform in the directory with the command `terraform plan`, to see which resources would be created and how.
6. Apply the changes and start managing your resources with terraform with `terraform apply`.
7. (Optional) Delete your created resources with `terraform destroy` to clean-up.
## References
- [Deploy ZITADEL in your infrastructure](/docs/self-hosting/deploy/overview)
- [ZITADEL CLI](/docs/self-hosting/manage/cli/overview)
- [Configuration Options in ZITADEL](/docs/self-hosting/manage/configure)

View File

@ -1,28 +0,0 @@
---
title: ZITADEL Terraform Provider
sidebar_label: Terraform Provider
---
It covers how to:
- Manage ZITADEL resources through the ZITADEL Terraform provider
Prerequisites:
- A ZITADEL Instance, if not present follow [this guide](../../start/quickstart)
- A user with enough authorization to manage the desired resources, if not present follow [this guide](/docs/guides/integrate/service-users/authenticate-service-users)
- Installed Terraform, if not present follow [this guide](https://learn.hashicorp.com/tutorials/terraform/install-cli)
## Manage ZITADEL resources through terraform
The full documentation and examples are available [here](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
To provide a small guide to where to start:
1. Create a folder where all the terraform files reside.
2. Configure the provider to use the right domain, port and token, with for example a `main.tf`file [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
3. Add a `zitadel_org` resource to the `main.tf` file, to create and manage a new organization in the instance, [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/org).
4. Add any resources to the organization in the `main.tf` file, [as example a human user](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/human_user).
5. (Optional) Use Terraform in the directory with the command `terraform plan`, to see which resources would be created and how.
6. Apply the changes and start managing your resources with terraform with `terraform apply`.
7. (Optional) Delete your created resources with `terraform destroy` to clean-up.

View File

@ -56,7 +56,7 @@ docker compose up --detach
mv ./machinekey/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -59,7 +59,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -61,7 +61,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -207,7 +207,7 @@ DefaultInstance:
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.
- Learn how to configure ZITADEL via the [Console user interface](/guides/manage/console/overview).
- Probably, you also want to [apply your custom branding](/guides/manage/customize/branding), [hook into certain events](/guides/manage/customize/behavior), [customize texts](/guides/manage/customize/texts) or [add metadata to your users](/guides/manage/customize/user-metadata).
- If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/guides/manage/terraform/basics).
- If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/guides/manage/terraform-provider).
## Limits and Quotas

View File

@ -154,11 +154,6 @@ module.exports = {
"guides/manage/customize/restrictions",
],
},
{
type: "category",
label: "Terraform",
items: ["guides/manage/terraform/basics"],
},
{
type: "category",
label: "Users",
@ -168,6 +163,7 @@ module.exports = {
"guides/manage/customize/user-schema",
],
},
"guides/manage/terraform-provider"
],
},
{

View File

@ -39,6 +39,7 @@
{ "source": "/docs/guides/integrate/event-api", "destination": "/docs/guides/integrate/zitadel-apis/event-api", "permanent": true },
{ "source": "/docs/examples/call-zitadel-api/go", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go", "permanent": true },
{ "source": "/docs/examples/call-zitadel-api/dot-net", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net", "permanent": true },
{ "source": "/docs/guides/manage/terraform/basics", "destination": "/docs/guides/manage/terraform-provider", "permanent": true },
{ "source": "/docs/guides/integrate/identity-providers", "destination": "/docs/guides/integrate/identity-providers/introduction", "permanent": true }
]
}

View File

@ -22,7 +22,7 @@ import (
func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
regOrgUrl, err := url.Parse("http://" + domain + ":8080/ui/login/register/org")
require.NoError(t, err)
// The CSRF cookie must be sent with every request.

View File

@ -33,7 +33,7 @@ func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
unsupportedLanguage = language.Afrikaans
)
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
t.Run("assumed defaults are correct", func(tt *testing.T) {
tt.Run("languages are not restricted by default", func(ttt *testing.T) {
restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})

View File

@ -14,7 +14,7 @@ import (
)
func TestServer_ListInstances(t *testing.T) {
domain, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
domain, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
tests := []struct {
name string

View File

@ -21,7 +21,7 @@ import (
)
func TestServer_Limits_AuditLogRetention(t *testing.T) {
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
_, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t)
beforeTime := time.Now()
farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC())

View File

@ -27,7 +27,7 @@ import (
)
func TestServer_Limits_Block(t *testing.T) {
domain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
domain, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
tests := []*test{
publicAPIBlockingTest(domain),
{

View File

@ -66,7 +66,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) {
}
func TestServer_QuotaNotification_NoLimit(t *testing.T) {
_, instanceID, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
_, instanceID, _, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100
@ -148,7 +148,7 @@ func awaitNotification(t *testing.T, bodies chan []byte, unit quota.Unit, percen
}
func TestServer_AddAndRemoveQuota(t *testing.T) {
_, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
_, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
got, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
InstanceId: instanceID,

View File

@ -46,6 +46,9 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ
nil,
false,
)
if err != nil {
return nil, err
}
return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false, true, false, false))
}

View File

@ -54,6 +54,9 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
nil,
false,
)
if err != nil {
return nil, err
}
return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false, true, false, false))
}

View File

@ -34,7 +34,7 @@ type Config struct {
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
User User
Admin User
Admin AdminUser
// Additional options to be appended as options=<Options>
// The value will be taken as is. Multiple options are space separated.
Options string
@ -114,6 +114,12 @@ type User struct {
SSL SSL
}
type AdminUser struct {
// ExistingDatabase is the database to connect to before the ZITADEL database exists
ExistingDatabase string
User `mapstructure:",squash"`
}
type SSL struct {
// type of connection security
Mode string
@ -147,7 +153,7 @@ func (c *Config) checkSSL(user User) {
func (c Config) String(useAdmin bool, appName string) string {
user := c.User
if useAdmin {
user = c.Admin
user = c.Admin.User
}
c.checkSSL(user)
fields := []string{
@ -163,6 +169,8 @@ func (c Config) String(useAdmin bool, appName string) string {
}
if !useAdmin {
fields = append(fields, "dbname="+c.Database)
} else if c.Admin.ExistingDatabase != "" {
fields = append(fields, "dbname="+c.Admin.ExistingDatabase)
}
if user.Password != "" {
fields = append(fields, "password="+user.Password)

View File

@ -35,7 +35,7 @@ type Config struct {
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
User User
Admin User
Admin AdminUser
// Additional options to be appended as options=<Options>
// The value will be taken as is. Multiple options are space separated.
Options string
@ -115,6 +115,12 @@ type User struct {
SSL SSL
}
type AdminUser struct {
// ExistingDatabase is the database to connect to before the ZITADEL database exists
ExistingDatabase string
User `mapstructure:",squash"`
}
type SSL struct {
// type of connection security
Mode string
@ -148,7 +154,7 @@ func (s *Config) checkSSL(user User) {
func (c Config) String(useAdmin bool, appName string) string {
user := c.User
if useAdmin {
user = c.Admin
user = c.Admin.User
}
c.checkSSL(user)
fields := []string{
@ -167,7 +173,11 @@ func (c Config) String(useAdmin bool, appName string) string {
if !useAdmin {
fields = append(fields, "dbname="+c.Database)
} else {
fields = append(fields, "dbname=postgres")
defaultDB := c.Admin.ExistingDatabase
if defaultDB == "" {
defaultDB = "postgres"
}
fields = append(fields, "dbname="+defaultDB)
}
if user.SSL.Mode != sslDisabledMode {
if user.SSL.RootCert != "" {

View File

@ -13,6 +13,7 @@ type ReadModel struct {
Events []Event `json:"-"`
ResourceOwner string `json:"-"`
InstanceID string `json:"-"`
Position float64 `json:"-"`
}
// AppendEvents adds all the events to the read model.
@ -43,6 +44,7 @@ func (rm *ReadModel) Reduce() error {
}
rm.ChangeDate = rm.Events[len(rm.Events)-1].CreatedAt()
rm.ProcessedSequence = rm.Events[len(rm.Events)-1].Sequence()
rm.Position = rm.Events[len(rm.Events)-1].Position()
// all events processed and not needed anymore
rm.Events = rm.Events[0:0]
return nil

View File

@ -76,7 +76,7 @@ func newClient(cc *grpc.ClientConn) Client {
}
}
func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId, adminID string, authenticatedIamOwnerCtx context.Context) {
primaryDomain = RandString(5) + ".integration.localhost"
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
InstanceName: "testinstance",
@ -89,20 +89,23 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte
},
},
})
if err != nil {
panic(err)
}
require.NoError(tt, err)
t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port))
instanceId = instance.GetInstanceId()
owner, err := t.Queries.GetUserByLoginName(authz.WithInstanceID(iamOwnerCtx, instanceId), true, "owner@"+primaryDomain)
require.NoError(tt, err)
t.Users.Set(instanceId, IAMOwner, &User{
User: owner,
Token: instance.GetPat(),
})
newCtx := t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId)
var adminUser *mgmt.ImportHumanUserResponse
// the following serves two purposes:
// 1. it ensures that the instance is ready to be used
// 2. it enables a normal login with the default admin user credentials
require.EventuallyWithT(tt, func(collectT *assert.CollectT) {
_, importErr := t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{
var importErr error
adminUser, importErr = t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{
UserName: "zitadel-admin@zitadel.localhost",
Email: &mgmt.ImportHumanUserRequest_Email{
Email: "zitadel-admin@zitadel.localhost",
@ -117,7 +120,7 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte
})
assert.NoError(collectT, importErr)
}, 2*time.Minute, 100*time.Millisecond, "instance not ready")
return primaryDomain, instanceId, newCtx
return primaryDomain, instanceId, adminUser.GetUserId(), newCtx
}
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {

View File

@ -151,7 +151,10 @@ func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*m
const CodeVerifier = "codeVerifier"
func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...)
return s.CreateOIDCAuthRequestWithDomain(ctx, s.Config.ExternalDomain, clientID, loginClient, redirectURI, scope...)
}
func (s *Tester) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := s.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...)
if err != nil {
return "", err
}
@ -212,11 +215,15 @@ func (s *Tester) OIDCIssuer() string {
}
func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
return s.CreateRelyingPartyForDomain(ctx, s.Config.ExternalDomain, clientID, redirectURI, scope...)
}
func (s *Tester) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
if len(scope) == 0 {
scope = []string{oidc.ScopeOpenID}
}
loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}}
return rp.NewRelyingPartyOIDC(ctx, s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, s.Config.Port, s.Config.ExternalSecure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
}
type loginRoundTripper struct {

View File

@ -21,7 +21,7 @@ import (
)
func TestServer_QuotaNotification_Limit(t *testing.T) {
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
_, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100
@ -67,7 +67,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) {
}
func TestServer_QuotaNotification_NoLimit(t *testing.T) {
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
_, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100

View File

@ -4,38 +4,126 @@ package handlers_test
import (
"bytes"
"context"
"encoding/json"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/object"
oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/project"
"github.com/zitadel/zitadel/pkg/grpc/system"
)
func TestServer_TelemetryPushMilestones(t *testing.T) {
primaryDomain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
primaryDomain, instanceID, adminID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
t.Log("testing against instance with primary domain", primaryDomain)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceCreated")
project, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
if err != nil {
t.Fatal(err)
}
projectAdded, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ProjectCreated")
if _, err = Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
ProjectId: project.GetId(),
Name: "integration",
}); err != nil {
t.Fatal(err)
}
redirectURI := "http://localhost:8888"
application, err := Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
ProjectId: projectAdded.GetId(),
Name: "integration",
RedirectUris: []string{redirectURI},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
DevMode: true,
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
})
require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ApplicationCreated")
// TODO: trigger and await milestone AuthenticationSucceededOnInstance
// TODO: trigger and await milestone AuthenticationSucceededOnApplication
if _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}); err != nil {
t.Fatal(err)
}
// create the session to be used for the authN of the clients
sessionID, sessionToken, _, _ := Tester.CreatePasswordSession(t, iamOwnerCtx, adminID, "Password1!")
console := consoleOIDCConfig(iamOwnerCtx, t)
loginToClient(iamOwnerCtx, t, primaryDomain, console.GetClientId(), instanceID, console.GetRedirectUris()[0], sessionID, sessionToken)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnInstance")
// make sure the client has been projected
require.EventuallyWithT(t, func(collectT *assert.CollectT) {
_, err := Tester.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{
ProjectId: projectAdded.GetId(),
AppId: application.GetAppId(),
})
assert.NoError(collectT, err)
}, 1*time.Minute, 100*time.Millisecond, "app not found")
loginToClient(iamOwnerCtx, t, primaryDomain, application.GetClientId(), instanceID, redirectURI, sessionID, sessionToken)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnApplication")
_, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID})
require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceDeleted")
}
func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, clientID, instanceID, redirectURI, sessionID, sessionToken string) {
authRequestID, err := Tester.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, primaryDomain, clientID, Tester.Users.Get(instanceID, integration.IAMOwner).ID, redirectURI, "openid")
require.NoError(t, err)
callback, err := Tester.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_v2.CreateCallbackRequest_Session{Session: &oidc_v2.Session{
SessionId: sessionID,
SessionToken: sessionToken,
}},
})
require.NoError(t, err)
provider, err := Tester.CreateRelyingPartyForDomain(iamOwnerCtx, primaryDomain, clientID, redirectURI)
require.NoError(t, err)
callbackURL, err := url.Parse(callback.GetCallbackUrl())
require.NoError(t, err)
code := callbackURL.Query().Get("code")
_, err = rp.CodeExchange[*oidc.IDTokenClaims](iamOwnerCtx, code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
require.NoError(t, err)
}
func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfig {
projects, err := Tester.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{
Queries: []*project.ProjectQuery{
{
Query: &project.ProjectQuery_NameQuery{
NameQuery: &project.ProjectNameQuery{
Name: "ZITADEL",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
})
require.NoError(t, err)
require.Len(t, projects.GetResult(), 1)
apps, err := Tester.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{
ProjectId: projects.GetResult()[0].GetId(),
Queries: []*app.AppQuery{
{
Query: &app.AppQuery_NameQuery{
NameQuery: &app.AppNameQuery{
Name: "Console",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
})
require.NoError(t, err)
require.Len(t, apps.GetResult(), 1)
return apps.GetResult()[0].GetOidcConfig()
}
func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMilestoneType string) {
for {
select {

View File

@ -17,7 +17,7 @@ import (
)
type OIDCSessionAccessTokenReadModel struct {
eventstore.WriteModel
eventstore.ReadModel
UserID string
SessionID string
@ -39,7 +39,7 @@ type OIDCSessionAccessTokenReadModel struct {
func newOIDCSessionAccessTokenReadModel(id string) *OIDCSessionAccessTokenReadModel {
return &OIDCSessionAccessTokenReadModel{
WriteModel: eventstore.WriteModel{
ReadModel: eventstore.ReadModel{
AggregateID: id,
},
}
@ -57,13 +57,11 @@ func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
wm.reduceTokenRevoked(event)
}
}
return wm.WriteModel.Reduce()
return wm.ReadModel.Reduce()
}
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AwaitOpenTransactions().
AllowTimeTravel().
AddQuery().
AggregateTypes(oidcsession.AggregateType).
AggregateIDs(wm.AggregateID).
@ -120,7 +118,7 @@ func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (m
if !model.AccessTokenExpiration.After(time.Now()) {
return nil, zerrors.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
}
if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.AccessTokenCreation, model.UserAgent.GetFingerprintID()); err != nil {
if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.Position, model.UserAgent.GetFingerprintID()); err != nil {
return nil, err
}
return model, nil
@ -142,13 +140,13 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe
// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination)
// occurred after a certain time and will return an error if so.
func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, creation time.Time, fingerprintID string) (err error) {
func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model := &sessionTerminatedModel{
sessionID: sessionID,
creation: creation,
position: position,
userID: userID,
fingerPrintID: fingerprintID,
}
@ -164,7 +162,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID,
}
type sessionTerminatedModel struct {
creation time.Time
position float64
sessionID string
userID string
fingerPrintID string
@ -184,8 +182,7 @@ func (s *sessionTerminatedModel) AppendEvents(events ...eventstore.Event) {
func (s *sessionTerminatedModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AwaitOpenTransactions().
CreationDateAfter(s.creation).
PositionAfter(s.position).
AddQuery().
AggregateTypes(session.AggregateType).
AggregateIDs(s.sessionID).

View File

@ -10,6 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/milestone"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
)
@ -104,6 +105,15 @@ func (p *milestoneProjection) Reducers() []handler.AggregateReducer {
},
},
},
{
Aggregate: oidcsession.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: oidcsession.AddedType,
Reduce: p.reduceOIDCSessionAdded,
},
},
},
{
Aggregate: milestone.AggregateType,
EventReducers: []handler.EventReducer{
@ -217,6 +227,40 @@ func (p *milestoneProjection) reduceUserTokenAdded(event eventstore.Event) (*han
return handler.NewMultiStatement(e, statements...), nil
}
func (p *milestoneProjection) reduceOIDCSessionAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*oidcsession.AddedEvent](event)
if err != nil {
return nil, err
}
statements := []func(eventstore.Event) handler.Exec{
handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()),
},
[]handler.Condition{
handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnInstance),
handler.NewIsNullCond(MilestoneColumnReachedDate),
},
),
}
// We ignore authentications without app, for example JWT profile or PAT
if e.ClientID != "" {
statements = append(statements, handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()),
},
[]handler.Condition{
handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnApplication),
handler.Not(handler.NewTextArrayContainsCond(MilestoneColumnIgnoreClientIDs, e.ClientID)),
handler.NewIsNullCond(MilestoneColumnReachedDate),
},
))
}
return handler.NewMultiStatement(e, statements...), nil
}
func (p *milestoneProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) {
if _, err := assertEvent[*instance.InstanceRemovedEvent](event); err != nil {
return nil, err

View File

@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/milestone"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@ -294,6 +295,43 @@ func TestMilestonesProjection_reduces(t *testing.T) {
},
},
},
{
name: "reduceOIDCSessionAdded",
args: args{
event: getEvent(timedTestEvent(
oidcsession.AddedType,
oidcsession.AggregateType,
[]byte(`{"clientID": "client-id"}`),
now,
), eventstore.GenericEventMapper[oidcsession.AddedEvent]),
},
reduce: (&milestoneProjection{}).reduceOIDCSessionAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("oidc_session"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
expectedArgs: []interface{}{
now,
"instance-id",
milestone.AuthenticationSucceededOnInstance,
},
},
{
expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (NOT (ignore_client_ids @> $4)) AND (reached_date IS NULL)",
expectedArgs: []interface{}{
now,
"instance-id",
milestone.AuthenticationSucceededOnApplication,
database.TextArray[string]{"client-id"},
},
},
},
},
},
},
{
name: "reduceInstanceRemoved",
args: args{