mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
Merge branch 'main' into lookup-table
This commit is contained in:
commit
c20150c1a8
@ -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:
|
||||
|
@ -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(),
|
||||
|
37
docs/docs/guides/manage/terraform-provider.md
Normal file
37
docs/docs/guides/manage/terraform-provider.md
Normal 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)
|
@ -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.
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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 }
|
||||
]
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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{})
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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),
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 != "" {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user