Merge branch 'main' into next

This commit is contained in:
Livio Spring 2024-04-29 07:54:05 +02:00
commit 6e60335789
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
149 changed files with 14706 additions and 2131 deletions

View File

@ -52,6 +52,8 @@ jobs:
go_version: "1.22"
core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint:
needs: [core, console]

View File

@ -12,6 +12,9 @@ on:
core_cache_path:
required: true
type: string
secrets:
CODECOV_TOKEN:
required: true
jobs:
postgres:

4
.gitignore vendored
View File

@ -82,3 +82,7 @@ go.work
go.work.sum
# Local Netlify folder
.netlify
load-test/node_modules
load-test/yarn-error.log
load-test/dist

View File

@ -96,7 +96,7 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade
- [API-first approach](https://zitadel.com/docs/apis/introduction)
- [Multi-tenancy](https://zitadel.com/docs/guides/solution-scenarios/b2b) authentication and access management
- Strong audit trail thanks to [event sourcing](https://zitadel.com/docs/concepts/eventstore/overview) as storage pattern
- [Strong audit trail](https://zitadel.com/docs/concepts/features/audit-trail) thanks to [event sourcing](https://zitadel.com/docs/concepts/eventstore/overview) as storage pattern
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs
- [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations
- [Self-service](https://zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators
@ -107,16 +107,17 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade
Authentication
- Single Sign On (SSO)
- Passkeys support (FIDO2 / WebAuthN)
- [Passkeys support (FIDO2 / WebAuthN)](https://zitadel.com/docs/concepts/features/passkeys)
- Username / Password
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
- LDAP
- External enterprise identity providers and social logins
- [LDAP](https://zitadel.com/docs/guides/integrate/identity-providers/ldap)
- [External enterprise identity providers and social logins](https://zitadel.com/docs/guides/integrate/identity-providers/introduction)
- [Device authorization](https://zitadel.com/docs/guides/solution-scenarios/device-authorization)
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
- [Custom sessions](https://zitadel.com/docs/guides/integrate/login-ui/username-password) if you need to go beyond OIDC or SAML
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/serviceusers) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
- [Token exchange and impersonation](https://zitadel.com/docs/guides/integrate/token-exchange)
Multi-Tenancy
@ -130,6 +131,10 @@ Integration
- [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction) for every functionality and resource
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens
- [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles)
- [Examples and SDKs](https://zitadel.com/docs/sdk-examples/introduction)
- [Audit Log and SOC/SIEM](https://zitadel.com/docs/guides/integrate/external-audit-log)
- [User registration and onboarding](https://zitadel.com/docs/guides/integrate/onboarding)
- [Hosted and custom login user interface](https://zitadel.com/docs/guides/integrate/login-ui)
Self-Service
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification

View File

@ -61,7 +61,7 @@
<ng-container matColumnDef="expirationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'USER.MACHINE.EXPIRATIONDATE' | translate }}</th>
<td mat-cell *matCellDef="let key">
{{ key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }}
{{ key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM yyyy, HH:mm' }}
</td>
</ng-container>

View File

@ -4,7 +4,7 @@
<span>{{ length }} </span>{{ 'PAGINATOR.COUNT' | translate }}
</p>
<p class="ts cnsl-secondary-text" *ngIf="timestamp" data-e2e="timestamp">
{{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM YYYY, HH:mm' }}
{{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM yyyy, HH:mm' }}
</p>
</div>
<span class="fill-space"></span>

View File

@ -56,7 +56,7 @@
<ng-container matColumnDef="expirationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'USER.MACHINE.EXPIRATIONDATE' | translate }}</th>
<td mat-cell *matCellDef="let key">
{{ key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }}
{{ key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM yyyy, HH:mm' }}
</td>
</ng-container>

View File

@ -13,14 +13,14 @@
<div class="row">
<p class="left cnsl-secondary-text">{{ 'USER.MACHINE.CREATIONDATE' | translate }}</p>
<p *ngIf="keyResponse.details && keyResponse.details.creationDate" class="right">
{{ keyResponse.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }}
{{ keyResponse.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM yyyy, HH:mm' }}
</p>
</div>
<div class="row" *ngIf="expirationDate">
<p class="left cnsl-secondary-text">{{ 'USER.MACHINE.EXPIRATIONDATE' | translate }}</p>
<p class="right">
{{ expirationDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }}
{{ expirationDate | localizedDate: 'EEE dd. MMM yyyy, HH:mm' }}
</p>
</div>

View File

@ -15,6 +15,7 @@ import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
@ -48,6 +49,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
private auth: GrpcAuthService,
private dialog: MatDialog,
public mgmtService: ManagementService,
private adminService: AdminService,
private toast: ToastService,
private router: Router,
breadcrumbService: BreadcrumbService,
@ -146,12 +148,24 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
width: '400px',
});
// Before we remove the org we get the current default org
// we have to query before the current org is removed
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.adminService
.getDefaultOrg()
.then((response) => {
const org = response?.org;
if (org) {
// We now remove the org
this.mgmtService
.removeOrg()
.then(() => {
setTimeout(() => {
// We change active org to default org as
// current org was deleted to avoid Organization doesn't exist
this.auth.setActiveOrg(org);
// Now we visit orgs
this.router.navigate(['/orgs']);
}, 1000);
this.toast.showInfo('ORG.TOAST.DELETED', true);
@ -159,6 +173,13 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
.catch((error) => {
this.toast.showError(error);
});
} else {
this.toast.showError('ORG.TOAST.DEFAULTORGNOTFOUND', false, true);
}
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}

View File

@ -90,6 +90,8 @@ import {
GetDefaultLanguageResponse,
GetDefaultLoginTextsRequest,
GetDefaultLoginTextsResponse,
GetDefaultOrgRequest,
GetDefaultOrgResponse,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
@ -429,6 +431,11 @@ export class AdminService {
this.storageService.getItem('onboarding-dismissed', StorageLocation.local) === 'true' ? true : false;
}
public getDefaultOrg(): Promise<GetDefaultOrgResponse.AsObject> {
const req = new GetDefaultOrgRequest();
return this.grpcService.admin.getDefaultOrg(req, null).then((resp) => resp.toObject());
}
public setDefaultOrg(orgId: string): Promise<SetDefaultOrgResponse.AsObject> {
const req = new SetDefaultOrgRequest();
req.setOrgId(orgId);

View File

@ -353,6 +353,7 @@ import {
RemoveOrgMetadataRequest,
RemoveOrgMetadataResponse,
RemoveOrgRequest,
RemoveOrgResponse,
RemovePersonalAccessTokenRequest,
RemovePersonalAccessTokenResponse,
RemoveProjectGrantMemberRequest,
@ -1749,7 +1750,7 @@ export class ManagementService {
return this.grpcService.mgmt.removeUser(req, null).then((resp) => resp.toObject());
}
public removeOrg(): Promise<RemoveUserResponse.AsObject> {
public removeOrg(): Promise<RemoveOrgResponse.AsObject> {
const req = new RemoveOrgRequest();
return this.grpcService.mgmt.removeOrg(req, null).then((resp) => resp.toObject());
}

View File

@ -1285,6 +1285,7 @@
"MEMBERCHANGED": "Сменен управител.",
"SETPRIMARY": "Основен набор от домейни.",
"DELETED": "Организацията е изтрита успешно",
"DEFAULTORGNOTFOUND": "Организацията по подразбиране не беше намерена",
"ORG_WAS_DELETED": "Организацията е изтрита."
},
"DIALOG": {

View File

@ -1292,6 +1292,7 @@
"MEMBERCHANGED": "Manažer změněn.",
"SETPRIMARY": "Nastavena primární doména.",
"DELETED": "Organizace úspěšně smazána",
"DEFAULTORGNOTFOUND": "Výchozí organizace nebyla nalezena",
"ORG_WAS_DELETED": "Organizace byla smazána."
},
"DIALOG": {

View File

@ -1291,6 +1291,7 @@
"MEMBERCHANGED": "Manager geändert.",
"SETPRIMARY": "Primäre Domain gesetzt.",
"DELETED": "Organisation erfolgreich gelöscht",
"DEFAULTORGNOTFOUND": "Die Standardorganisation wurde nicht gefunden",
"ORG_WAS_DELETED": "Organisation wurde gelöscht."
},
"DIALOG": {

View File

@ -1291,8 +1291,9 @@
"MEMBERREMOVED": "Manager removed.",
"MEMBERCHANGED": "Manager changed.",
"SETPRIMARY": "Primary domain set.",
"DELETED": "Organisation deleted successfully",
"ORG_WAS_DELETED": "Organisation has been deleted."
"DELETED": "Organization deleted successfully",
"DEFAULTORGNOTFOUND": "The default organization was not found",
"ORG_WAS_DELETED": "Organization has been deleted."
},
"DIALOG": {
"DEACTIVATE": {

View File

@ -1293,6 +1293,7 @@
"MEMBERCHANGED": "Mánager modificado.",
"SETPRIMARY": "Dominio primario establecido.",
"DELETED": "Organización borrada con éxito",
"DEFAULTORGNOTFOUND": "No se encontró la organización por defecto",
"ORG_WAS_DELETED": "La organización ha sido borrada."
},
"DIALOG": {

View File

@ -1291,6 +1291,7 @@
"MEMBERCHANGED": "Gestionnaire modifié.",
"SETPRIMARY": "Domaine primaire défini.",
"DELETED": "Organisation supprimée avec succès",
"DEFAULTORGNOTFOUND": "L'organisation par défaut est introuvable",
"ORG_WAS_DELETED": "L'organisation a été supprimée"
},
"DIALOG": {

View File

@ -1291,6 +1291,7 @@
"MEMBERCHANGED": "Manager cambiato con successo",
"SETPRIMARY": "Dominio primario cambiato con successo",
"DELETED": "Organizzazione eliminata con successo",
"DEFAULTORGNOTFOUND": "Impossibile trovare l'organizzazione predefinita",
"ORG_WAS_DELETED": "Organizzazione è stata eliminata"
},
"DIALOG": {

View File

@ -1292,6 +1292,7 @@
"MEMBERCHANGED": "マネージャーが変更されました。",
"SETPRIMARY": "プライマリドメインが設定されました。",
"DELETED": "組織が正常に削除されました。",
"DEFAULTORGNOTFOUND": "デフォルトの組織が見つかりませんでした",
"ORG_WAS_DELETED": "組織が削除されました。"
},
"DIALOG": {

View File

@ -1293,6 +1293,7 @@
"MEMBERCHANGED": "Променет менаџер.",
"SETPRIMARY": "Поставен основен домен.",
"DELETED": "Организацијата успешно избришана",
"DEFAULTORGNOTFOUND": "Стандардната организација не беше пронајдена",
"ORG_WAS_DELETED": "Организацијата е избришана."
},
"DIALOG": {

View File

@ -1292,6 +1292,7 @@
"MEMBERCHANGED": "Beheerder gewijzigd.",
"SETPRIMARY": "Primaire domein ingesteld.",
"DELETED": "Organisatie succesvol verwijderd",
"DEFAULTORGNOTFOUND": "De standaardorganisatie is niet gevonden",
"ORG_WAS_DELETED": "Organisatie is verwijderd."
},
"DIALOG": {

View File

@ -1291,6 +1291,7 @@
"MEMBERCHANGED": "Zmieniono managera.",
"SETPRIMARY": "Ustawiono domenę podstawową.",
"DELETED": "Organizacja została usunięta pomyślnie",
"DEFAULTORGNOTFOUND": "Nie znaleziono organizacji domyślnej",
"ORG_WAS_DELETED": "Organizacja została usunięta."
},
"DIALOG": {

View File

@ -1293,6 +1293,7 @@
"MEMBERCHANGED": "Gerente alterado.",
"SETPRIMARY": "Domínio principal definido.",
"DELETED": "Organização excluída com sucesso",
"DEFAULTORGNOTFOUND": "A organização padrão não foi encontrada",
"ORG_WAS_DELETED": "Organização foi excluída."
},
"DIALOG": {

View File

@ -1335,6 +1335,7 @@
"MEMBERCHANGED": "Менеджер изменён.",
"SETPRIMARY": "Установлен основной домен.",
"DELETED": "Организация успешно удалена",
"DEFAULTORGNOTFOUND": "Организация по умолчанию не найдена",
"ORG_WAS_DELETED": "Организация удалена."
},
"DIALOG": {

View File

@ -1291,6 +1291,7 @@
"MEMBERCHANGED": "管理者以改变。",
"SETPRIMARY": "已设为主域名。",
"DELETED": "成功删除的组织",
"DEFAULTORGNOTFOUND": "未找到默认组织",
"ORG_WAS_DELETED": "组织被删除"
},
"DIALOG": {

View File

@ -57,18 +57,6 @@ spec:
selector:
app: cockroachdb
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: cockroachdb-budget
labels:
app: cockroachdb
spec:
selector:
matchLabels:
app: cockroachdb
maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:

View File

@ -7,7 +7,7 @@ spec:
template:
metadata:
annotations:
client.knative.dev/user-image: ghcr.io/zitadel/zitadel:stable
client.knative.dev/user-image: ghcr.io/zitadel/zitadel:latest
creationTimestamp: null
spec:
containerConcurrency: 0
@ -28,7 +28,7 @@ spec:
value: "80"
- name: ZITADEL_EXTERNALDOMAIN
value: zitadel.default.127.0.0.1.sslip.io
image: ghcr.io/zitadel/zitadel:stable
image: ghcr.io/zitadel/zitadel:latest
name: user-container
ports:
- containerPort: 8080

View File

@ -68,6 +68,19 @@ https://github.com/zitadel/actions/blob/main/examples/custom_roles.js
</details>
### Custom role mapping including org metadata in claims
There's even a possibility to use the metadata of organizations the user is granted to
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/custom_roles_org_metadata.js
```
</details>
## Customize SAML response
Append attributes returned on SAML requests.

View File

@ -210,3 +210,5 @@ This object represents a list of user grant stored in ZITADEL.
The name of the organization, where the user was granted
- `projectId` *string*
- `projectName` *string*
- `getOrgMetadata()` [*metadataResult*](#metadata-result)
Get the metadata of the organization where the user was granted

View File

@ -1,23 +1,78 @@
---
title: Identity Brokering in ZITADEL
title: Identity Brokering
sidebar_label: Identity Brokering
---
## What are Identity Brokering and Federated Identities?
Link social logins and external identity providers with your identity management platform allowing users to login with their preferred identity provider.
Establish a trusted connection between your central identity provider (IdP) and third party identity providers.
By using a central identity brokering service you don't need to develop and establish a trust relationship between each application and each identity provider individually.
## What are federated identities?
Federated identity management is an arrangement built upon the trust between two or more domains. Users of these domains are allowed to access applications and services using the same identity.
This identity is known as federated identity and the pattern behind this is identity federation.
Compatibility across various IdPs is ensured by using industry standard protocols, such as:
* OpenID Connect (OIDC): A modern and versatile protocol for secure authentication.
* SAML2: A widely adopted protocol for secure single sign-on (SSO) in enterprise environments.
* LDAP: A lightweight protocol for accessing user data directories commonly used in corporate networks.
## What is identity brokering?
A service provider that specializes in brokering access control between multiple service providers (also referred to as relying parties) is called an identity broker.
Federated identity management is an arrangement that is made between two or more such identity brokers across organizations.
For example, if Google is configured as an identity provider in your organization, the user will get the option to use his Google Account on the Login Screen of ZITADEL. Because Google is registered as a trusted identity provider, the user will be able to login in with the Google account after the user is linked with an existing ZITADEL account (if the user is already registered) or a new one with the claims provided by Google.
For example, if Google is configured as an identity provider in your organization, the user will get the option to use his Google Account on the Login Screen of ZITADEL.
Because Google is registered as a trusted identity provider, the user will be able to login in with the Google account after the user is linked with an existing ZITADEL account (if the user is already registered) or a new one with the claims provided by Google.
![Identity Brokering](/img/guides/identity_brokering.png)
![Diagram of an identity brokering scheme using a central identity provider that has a trust link to the Google IdP and Entra ID](/img/concepts/features/identity-brokering.png)
## How to use external identity providers in ZITADEL
The schema is a very simplified version, but shows the essential steps for identity brokering
Configure external identity providers on the instance level or just for one organization via the [Console](/guides/manage/console/default-settings#identity-providers) or ZITADEL APIs.
1. An unauthenticated user wants to use the alpha.com's application.
2. The application redirects the user to alpha.com's identity provider (IdP).
3. Based on the user's tenants configuration the IdP presents the configured identity providers, or redirects the user directly to the primary external IdP. The user authenticates with their external identity provider (eg, Entra ID).
4. After the authentication, the user is redirected back to alpha.com's identity provider. If the user doesn't exist in the IdP the user will be created just-in-time and linked to the external identity provider for future reference.
5. As with a local authentication, the IdP issues a token to the user that can be used to access the application. The IdP redirects the user, which is now authenticated, eventually to the application.
You will find [detailed integration guides for many Identity Providers](/guides/integrate/identity-providers/introduction) in our docs.
ZITADEL also provides templates to configure generic identity providers, which don't have templates.
## Is single-sign-on (SSO) the same as identity brokering?
Sometimes single-sign-on (SSO) and login with third party identity providers is used interchangeably.
Typically SSO describes an authentication scheme that allows users to log in once at a central identity provider and access service providers (client applications) without to login again.
Identity brokering describes an authentication scheme where users can login with external identity providers that have a established trust with an identity provider which facilitates the authentication for the requested applications.
The connection between the two lies in how SSO can be implemented as part of an identity brokering solution.
In such cases, the identity broker uses SSO to enable seamless access across multiple systems, handling the complexities of different authentication protocols and standards behind the scenes.
This allows users to log in once and gain access to multiple systems that the broker facilitates.
## Multitenancy and identity brokering
In a multi-tenancy application, you want to be able to configure an external identity provider per tenant.
For example some organizations might use their EntraID, some other want to login with their OKTA, or Google Workspace.
Using an identity provider with strong multitenancy capabilities such as ZITADEL, you can configure a different set of external identity providers per organization.
[Domain discovery](/docs/guides/solution-scenarios/domain-discovery) ensures that users are redirected to their external identity provider based on their email-address or username.
[Managers](../structure/managers) can configure organization domains that are used for domain-based redirection to an external IdP.
![Diagram explaining domain discovery](/img/concepts/features/domain-discovery.png)
## Simplify identity brokering with ZITADEL templates
ZITADEL works with SAML, OpenID Connect, and LDAP external identity providers.
For popular IdPs such as EntraID, Okta, Google, Facebook, and GitHub, ZITADEL [offers pre-configured templates](/docs/guides/integrate/identity-providers/introduction).
These templates expedite the configuration process, allowing organizations to quickly integrate these providers with minimal effort.
ZITADEL recognizes that specific needs may extend beyond pre-built templates.
To address this, ZITADEL provides generic templates that enable connection to virtually any IdP. This ensures maximum flexibility and future-proofs login infrastructure, accommodating future integrations with ease.
### References
* [Detailed integration guide for many identity providers](/guides/integrate/identity-providers/introduction)
* [Setup identity providers with Console](/guides/manage/console/default-settings#identity-providers)
* [Configure identity providers with the ZITADEL API](/docs/category/apis/resources/mgmt/identity-providers)

View File

@ -9,9 +9,13 @@ import CustomLoginPolicy from './_custom_login_policy.mdx';
import IDPsOverview from './_idps_overview.mdx';
import Activate from './_activate.mdx';
import TestSetup from './_test_setup.mdx';
import { ResponsivePlayer } from "../../../../src/components/player";
<Intro provider="Google"/>
<ResponsivePlayer playing controls url='https://www.youtube.com/watch?v=wg-ee-EnHdE' />
## Open the Google Identity Provider Template
<IDPsOverview templates="Google"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

View File

@ -1,6 +1,7 @@
package object
import (
"context"
"encoding/json"
"time"
@ -77,6 +78,21 @@ func UserMetadataListFromSlice(c *actions.FieldConfig, metadata []query.UserMeta
return c.Runtime.ToValue(result)
}
func GetOrganizationMetadata(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, organizationID string) goja.Value {
metadata, err := queries.SearchOrgMetadata(
ctx,
true,
organizationID,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return OrgMetadataListFromQuery(c, metadata)
}
func metadataByteArrayToValue(val []byte, runtime *goja.Runtime) goja.Value {
var value interface{}
if !json.Valid(val) {

View File

@ -1,6 +1,7 @@
package object
import (
"context"
"time"
"github.com/dop251/goja"
@ -44,6 +45,8 @@ type userGrant struct {
ProjectId string
ProjectName string
GetOrgMetadata func(goja.FunctionCall) goja.Value
}
func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
@ -58,10 +61,11 @@ func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(c
}
}
func UserGrantsFromQuery(c *actions.FieldConfig, userGrants *query.UserGrants) goja.Value {
func UserGrantsFromQuery(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants *query.UserGrants) goja.Value {
if userGrants == nil {
return c.Runtime.ToValue(nil)
}
orgMetadata := make(map[string]goja.Value)
grantList := &userGrantList{
Count: userGrants.Count,
Sequence: userGrants.Sequence,
@ -84,16 +88,24 @@ func UserGrantsFromQuery(c *actions.FieldConfig, userGrants *query.UserGrants) g
UserGrantResourceOwnerName: grant.OrgName,
ProjectId: grant.ProjectID,
ProjectName: grant.ProjectName,
GetOrgMetadata: func(call goja.FunctionCall) goja.Value {
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
return md
}
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
return orgMetadata[grant.ResourceOwner]
},
}
}
return c.Runtime.ToValue(grantList)
}
func UserGrantsFromSlice(c *actions.FieldConfig, userGrants []query.UserGrant) goja.Value {
func UserGrantsFromSlice(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants []query.UserGrant) goja.Value {
if userGrants == nil {
return c.Runtime.ToValue(nil)
}
orgMetadata := make(map[string]goja.Value)
grantList := &userGrantList{
Count: uint64(len(userGrants)),
Grants: make([]*userGrant, len(userGrants)),
@ -114,6 +126,13 @@ func UserGrantsFromSlice(c *actions.FieldConfig, userGrants []query.UserGrant) g
UserGrantResourceOwnerName: grant.OrgName,
ProjectId: grant.ProjectID,
ProjectName: grant.ProjectName,
GetOrgMetadata: func(goja.FunctionCall) goja.Value {
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
return md
}
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
return orgMetadata[grant.ResourceOwner]
},
}
}

View File

@ -65,7 +65,7 @@ func (s *Server) ResendMyEmailVerification(ctx context.Context, _ *auth_pb.Resen
if err != nil {
return nil, err
}
objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner, emailCodeGenerator)
objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner, emailCodeGenerator, "")
if err != nil {
return nil, err
}

View File

@ -473,7 +473,7 @@ func (s *Server) ResendHumanInitialization(ctx context.Context, req *mgmt_pb.Res
if err != nil {
return nil, err
}
details, err := s.command.ResendInitialMail(ctx, req.UserId, domain.EmailAddress(req.Email), authz.GetCtxData(ctx).OrgID, initCodeGenerator)
details, err := s.command.ResendInitialMail(ctx, req.UserId, domain.EmailAddress(req.Email), authz.GetCtxData(ctx).OrgID, initCodeGenerator, "")
if err != nil {
return nil, err
}
@ -487,7 +487,7 @@ func (s *Server) ResendHumanEmailVerification(ctx context.Context, req *mgmt_pb.
if err != nil {
return nil, err
}
objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, emailCodeGenerator)
objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, emailCodeGenerator, "")
if err != nil {
return nil, err
}
@ -590,7 +590,7 @@ func (s *Server) SendHumanResetPasswordNotification(ctx context.Context, req *mg
if err != nil {
return nil, err
}
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), passwordCodeGenerator)
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), passwordCodeGenerator, "")
if err != nil {
return nil, err
}
@ -774,14 +774,23 @@ func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKe
func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRequest) (*mgmt_pb.AddMachineKeyResponse, error) {
machineKey := AddMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID)
// If there is no pubkey supplied, then AddUserMachineKey will generate a new one
pubkeySupplied := len(machineKey.PublicKey) > 0
details, err := s.command.AddUserMachineKey(ctx, machineKey)
if err != nil {
return nil, err
}
keyDetails, err := machineKey.Detail()
// Return key details only if the pubkey wasn't supplied, otherwise the user already has
// private key locally
var keyDetails []byte
if !pubkeySupplied {
var err error
keyDetails, err = machineKey.Detail()
if err != nil {
return nil, err
}
}
return &mgmt_pb.AddMachineKeyResponse{
KeyId: machineKey.KeyID,
KeyDetails: keyDetails,

View File

@ -237,6 +237,7 @@ func AddMachineKeyRequestToCommand(req *mgmt_pb.AddMachineKeyRequest, resourceOw
},
ExpirationDate: expDate,
Type: authn.KeyTypeToDomain(req.Type),
PublicKey: req.PublicKey,
}
}

View File

@ -490,25 +490,16 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, user *query.User, userGra
return object.UserMetadataListFromQuery(c, metadata)
}
}),
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
return object.UserGrantsFromQuery(c, userGrants)
}),
actions.SetFields("grants",
func(c *actions.FieldConfig) interface{} {
return object.UserGrantsFromQuery(ctx, o.query, c, userGrants)
},
),
),
actions.SetFields("org",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value {
metadata, err := o.query.SearchOrgMetadata(
ctx,
true,
user.ResourceOwner,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return object.OrgMetadataListFromQuery(c, metadata)
return object.GetOrganizationMetadata(ctx, o.query, c, user.ResourceOwner)
}
}),
),
@ -714,24 +705,13 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userG
}
}),
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
return object.UserGrantsFromQuery(c, userGrants)
return object.UserGrantsFromQuery(ctx, o.query, c, userGrants)
}),
),
actions.SetFields("org",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value {
metadata, err := o.query.SearchOrgMetadata(
ctx,
true,
user.ResourceOwner,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return object.OrgMetadataListFromQuery(c, metadata)
return object.GetOrganizationMetadata(ctx, o.query, c, user.ResourceOwner)
}
}),
),

View File

@ -0,0 +1,76 @@
//go:build integration
package oidc_test
import (
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/schema"
)
func TestServer_RefreshToken_Status(t *testing.T) {
clientID, _ := createClient(t)
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err)
tests := []struct {
name string
refreshToken string
}{
{
name: "invalid base64",
refreshToken: "~!~@#$%",
},
{
name: "invalid after decrypt",
refreshToken: "DEADBEEFDEADBEEF",
},
{
name: "short input",
refreshToken: "DEAD",
},
{
name: "empty input",
refreshToken: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
request := rp.RefreshTokenRequest{
RefreshToken: tt.refreshToken,
ClientID: clientID,
GrantType: oidc.GrantTypeRefreshToken,
}
client.CallTokenEndpoint(CTX, request, tokenEndpointCaller{RelyingParty: provider})
values := make(url.Values)
err := schema.NewEncoder().Encode(request, values)
require.NoError(t, err)
resp, err := http.Post(provider.OAuthConfig().Endpoint.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(values.Encode()))
require.NoError(t, err)
defer resp.Body.Close()
assert.Less(t, resp.StatusCode, 500)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
t.Log(string(body))
})
}
}
type tokenEndpointCaller struct {
rp.RelyingParty
}
func (t tokenEndpointCaller) TokenEndpoint() string {
return t.OAuthConfig().Endpoint.TokenURL
}

View File

@ -252,24 +252,13 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
}
}),
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
return object.UserGrantsFromSlice(c, qu.UserGrants)
return object.UserGrantsFromSlice(ctx, s.query, c, qu.UserGrants)
}),
),
actions.SetFields("org",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value {
metadata, err := s.query.SearchOrgMetadata(
ctx,
true,
qu.User.ResourceOwner,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return object.OrgMetadataListFromQuery(c, metadata)
return object.GetOrganizationMetadata(ctx, s.query, c, qu.User.ResourceOwner)
}
}),
),

View File

@ -246,24 +246,13 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
}
}),
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
return object.UserGrantsFromQuery(c, userGrants)
return object.UserGrantsFromQuery(ctx, p.query, c, userGrants)
}),
),
actions.SetFields("org",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value {
metadata, err := p.query.SearchOrgMetadata(
ctx,
true,
user.ResourceOwner,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return object.OrgMetadataListFromQuery(c, metadata)
return object.GetOrganizationMetadata(ctx, p.query, c, user.ResourceOwner)
}
}),
),

View File

@ -3,6 +3,8 @@ package login
import (
"net/http"
"github.com/zitadel/logging"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
)
@ -33,3 +35,23 @@ func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*
func (l *Login) getParseData(r *http.Request, data interface{}) error {
return l.parser.Parse(r, data)
}
// checkOptionalAuthRequestOfEmailLinks tries to get the [domain.AuthRequest] from the request.
// In case any error occurs, e.g. if the user agent does not correspond, the `authRequestID` query parameter will be
// removed from the request URL and form to ensure subsequent functions and pages do not use it.
// This function is used for handling links in emails, which could possibly be opened on another device than the
// auth request was initiated.
func (l *Login) checkOptionalAuthRequestOfEmailLinks(r *http.Request) *domain.AuthRequest {
authReq, err := l.getAuthRequest(r)
if err == nil {
return authReq
}
logging.WithError(err).Infof("authrequest could not be found for email link on path %s", r.URL.RequestURI())
queries := r.URL.Query()
queries.Del(QueryAuthRequestID)
r.URL.RawQuery = queries.Encode()
r.RequestURI = r.URL.RequestURI()
r.Form.Del(QueryAuthRequestID)
r.PostForm.Del(QueryAuthRequestID)
return nil
}

View File

@ -1,8 +1,8 @@
package login
import (
"fmt"
"net/http"
"net/url"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
@ -38,14 +38,20 @@ type initPasswordData struct {
HasSymbol string
}
func InitPasswordLink(origin, userID, code, orgID string) string {
return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointInitPassword, userID, code, orgID)
func InitPasswordLink(origin, userID, code, orgID, authRequestID string) string {
v := url.Values{}
v.Set(queryInitPWUserID, userID)
v.Set(queryInitPWCode, code)
v.Set(queryOrgID, orgID)
v.Set(QueryAuthRequestID, authRequestID)
return externalLink(origin) + EndpointInitPassword + "?" + v.Encode()
}
func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryInitPWUserID)
code := r.FormValue(queryInitPWCode)
l.renderInitPassword(w, r, nil, userID, code, nil)
l.renderInitPassword(w, r, authReq, userID, code, nil)
}
func (l *Login) handleInitPasswordCheck(w http.ResponseWriter, r *http.Request) {
@ -94,7 +100,7 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
l.renderInitPassword(w, r, authReq, userID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator)
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator, authReq.ID)
l.renderInitPassword(w, r, authReq, userID, "", err)
}

View File

@ -1,8 +1,8 @@
package login
import (
"fmt"
"net/http"
"net/url"
"strconv"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
@ -44,16 +44,24 @@ type initUserData struct {
HasSymbol string
}
func InitUserLink(origin, userID, loginName, code, orgID string, passwordSet bool) string {
return fmt.Sprintf("%s%s?userID=%s&loginname=%s&code=%s&orgID=%s&passwordset=%t", externalLink(origin), EndpointInitUser, userID, loginName, code, orgID, passwordSet)
func InitUserLink(origin, userID, loginName, code, orgID string, passwordSet bool, authRequestID string) string {
v := url.Values{}
v.Set(queryInitUserUserID, userID)
v.Set(queryInitUserLoginName, loginName)
v.Set(queryInitUserCode, code)
v.Set(queryOrgID, orgID)
v.Set(queryInitUserPassword, strconv.FormatBool(passwordSet))
v.Set(QueryAuthRequestID, authRequestID)
return externalLink(origin) + EndpointInitUser + "?" + v.Encode()
}
func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryInitUserUserID)
code := r.FormValue(queryInitUserCode)
loginName := r.FormValue(queryInitUserLoginName)
passwordSet, _ := strconv.ParseBool(r.FormValue(queryInitUserPassword))
l.renderInitUser(w, r, nil, userID, loginName, code, passwordSet, nil)
l.renderInitUser(w, r, authReq, userID, loginName, code, passwordSet, nil)
}
func (l *Login) handleInitUserCheck(w http.ResponseWriter, r *http.Request) {
@ -105,7 +113,7 @@ func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *
l.renderInitUser(w, r, authReq, userID, loginName, "", showPassword, err)
return
}
_, err = l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID, initCodeGenerator)
_, err = l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID, initCodeGenerator, authReq.ID)
l.renderInitUser(w, r, authReq, userID, loginName, "", showPassword, err)
}

View File

@ -1,8 +1,8 @@
package login
import (
"fmt"
"net/http"
"net/url"
"github.com/zitadel/zitadel/internal/domain"
)
@ -27,18 +27,24 @@ type mailVerificationData struct {
UserID string
}
func MailVerificationLink(origin, userID, code, orgID string) string {
return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointMailVerification, userID, code, orgID)
func MailVerificationLink(origin, userID, code, orgID, authRequestID string) string {
v := url.Values{}
v.Set(queryUserID, userID)
v.Set(queryCode, code)
v.Set(queryOrgID, orgID)
v.Set(QueryAuthRequestID, authRequestID)
return externalLink(origin) + EndpointMailVerification + "?" + v.Encode()
}
func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryUserID)
code := r.FormValue(queryCode)
if code != "" {
l.checkMailCode(w, r, nil, userID, code)
l.checkMailCode(w, r, authReq, userID, code)
return
}
l.renderMailVerification(w, r, nil, userID, nil)
l.renderMailVerification(w, r, authReq, userID, nil)
}
func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Request) {
@ -61,7 +67,7 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
return
}
_, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator)
_, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator, authReq.ID)
l.renderMailVerification(w, r, authReq, data.UserID, err)
}

View File

@ -33,7 +33,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderPasswordResetDone(w, r, authReq, err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, passwordCodeGenerator)
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, passwordCodeGenerator, authReq.ID)
l.renderPasswordResetDone(w, r, authReq, err)
}

View File

@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -67,22 +68,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
if authRequest != nil && authRequest.RequestedOrgID != "" && authRequest.RequestedOrgID != resourceOwner {
resourceOwner = authRequest.RequestedOrgID
}
initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.userCodeAlg)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
// For consistency with the external authentication flow,
// the setMetadata() function is provided on the pre creation hook, for now,
// like for the ExternalAuthentication flow.
@ -96,22 +81,14 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
l.renderRegister(w, r, authRequest, data, err)
return
}
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
human := command.AddHumanFromDomain(user, metadatas, authRequest, nil)
err = l.command.AddUserHuman(setContext(r.Context(), resourceOwner), resourceOwner, human, true, l.userCodeAlg)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
if len(metadatas) > 0 {
_, err = l.command.BulkSetUserMetadata(r.Context(), user.AggregateID, resourceOwner, metadatas...)
if err != nil {
// TODO: What if action is configured to be allowed to fail? Same question for external registration.
l.renderRegister(w, r, authRequest, data, err)
return
}
}
userGrants, err := l.runPostCreationActions(user.AggregateID, authRequest, r, resourceOwner, domain.FlowTypeInternalAuthentication)
userGrants, err := l.runPostCreationActions(human.ID, authRequest, r, resourceOwner, domain.FlowTypeInternalAuthentication)
if err != nil {
l.renderError(w, r, authRequest, err)
return
@ -128,7 +105,7 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectUser(r.Context(), authRequest.ID, user.AggregateID, userAgentID)
err = l.authRepo.SelectUser(r.Context(), authRequest.ID, human.ID, userAgentID)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return

View File

@ -543,23 +543,19 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis
if err != nil {
return err
}
initCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, repo.UserCodeAlg)
addMetadata := make([]*command.AddMetadataEntry, len(metadatas))
for i, metadata := range metadatas {
addMetadata[i] = &command.AddMetadataEntry{
Key: metadata.Key,
Value: metadata.Value,
}
}
human := command.AddHumanFromDomain(registerUser, metadatas, request, externalIDP)
err = repo.Command.AddUserHuman(ctx, resourceOwner, human, true, repo.UserCodeAlg)
if err != nil {
return err
}
emailCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, repo.UserCodeAlg)
if err != nil {
return err
}
phoneCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, repo.UserCodeAlg)
if err != nil {
return err
}
human, err := repo.Command.RegisterHuman(ctx, resourceOwner, registerUser, externalIDP, orgMemberRoles, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
return err
}
request.SetUserInfo(human.AggregateID, human.Username, human.PreferredLoginName, human.DisplayName, "", human.ResourceOwner)
request.SetUserInfo(human.ID, human.Username, human.Username, human.DisplayName, "", resourceOwner)
request.SelectedIDPConfigID = externalIDP.IDPConfigID
request.LinkingUsers = nil
err = repo.Command.UserIDPLoginChecked(ctx, request.UserOrgID, request.UserID, request.WithCurrentInfo(info))

View File

@ -160,6 +160,10 @@ func (s *UserSession) Reducers() []handler.AggregateReducer {
Event: user.UserRemovedType,
Reduce: s.Reduce,
},
{
Event: user.HumanRegisteredType,
Reduce: s.Reduce,
},
},
},
{
@ -234,6 +238,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
handler.NewCol("multi_factor_verification_type", domain.MFALevelNotSetUp),
handler.NewCol("external_login_verification", time.Time{}),
handler.NewCol("state", domain.UserSessionStateTerminated),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
@ -247,16 +253,30 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
if err != nil {
return nil, err
}
return handler.NewUpdateStatement(event,
return handler.NewMultiStatement(event,
handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol("password_verification", event.CreatedAt()),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
handler.NewCond("user_id", event.Aggregate().ID),
handler.NewCond("user_agent_id", userAgent),
}),
handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol("password_verification", time.Time{}),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
handler.NewCond("user_id", event.Aggregate().ID),
handler.Not(handler.NewCond("user_agent_id", userAgent)),
handler.Not(handler.NewCond("state", domain.UserSessionStateTerminated)),
},
}),
), nil
case user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType,
@ -264,6 +284,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
return handler.NewUpdateStatement(event,
[]handler.Column{
handler.NewCol("second_factor_verification", time.Time{}),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
@ -277,6 +299,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
[]handler.Column{
handler.NewCol("external_login_verification", time.Time{}),
handler.NewCol("selected_idp_config_id", ""),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
@ -289,6 +313,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
[]handler.Column{
handler.NewCol("passwordless_verification", time.Time{}),
handler.NewCol("multi_factor_verification", time.Time{}),
handler.NewCol("change_date", event.CreatedAt()),
handler.NewCol("sequence", event.Sequence()),
},
[]handler.Condition{
handler.NewCond("instance_id", event.Aggregate().InstanceID),
@ -300,6 +326,23 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error {
return u.view.DeleteUserSessions(event.Aggregate().ID, event.Aggregate().InstanceID)
}), nil
case user.HumanRegisteredType:
return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error {
eventData, err := view_model.UserSessionFromEvent(event)
if err != nil {
return err
}
session := &view_model.UserSessionView{
CreationDate: event.CreatedAt(),
ResourceOwner: event.Aggregate().ResourceOwner,
UserAgentID: eventData.UserAgentID,
UserID: event.Aggregate().ID,
State: int32(domain.UserSessionStateActive),
InstanceID: event.Aggregate().InstanceID,
PasswordVerification: event.CreatedAt(),
}
return u.updateSession(session, event)
}), nil
case instance.InstanceRemovedEventType:
return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error {
return u.view.DeleteInstanceUserSessions(event.Aggregate().InstanceID)

View File

@ -1427,6 +1427,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) {
Crypted: []byte("userinit"),
},
1*time.Hour,
"",
),
),
eventFromEventPusher(org.NewMemberAddedEvent(context.Background(),

View File

@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@ -57,6 +56,12 @@ type AddHuman struct {
Passwordless bool
ExternalIDP bool
Register bool
// UserAgentID is optional and can be passed in case the user registered themselves.
// This will be used in the login UI to handle authentication automatically.
UserAgentID string
// AuthRequestID is optional and can be passed in case the user registered themselves.
// This will be used to pass the information in notifications for links to the login UI.
AuthRequestID string
Metadata []*AddMetadataEntry
// Links are optional
@ -200,6 +205,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
"", // no user agent id available
)
} else {
createCmd = user.NewHumanAddedEvent(
@ -272,7 +278,7 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
if err != nil {
return nil, err
}
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), nil
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry, human.AuthRequestID)), nil
}
if !human.Email.Verified {
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
@ -460,61 +466,6 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.
return writeModelToHuman(addedHuman), passwordlessCode, nil
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) {
if orgID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound")
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound")
}
loginPolicy, err := c.getOrgLoginPolicy(ctx, orgID)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-Dfg3g", "Errors.Org.LoginPolicy.NotFound")
}
// check only if local registration is allowed, the idp will be checked separately
if !loginPolicy.AllowRegister && link == nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed")
}
userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, err
}
orgMemberWriteModel := NewOrgMemberWriteModel(orgID, registeredHuman.AggregateID)
orgAgg := OrgAggregateFromWriteModel(&orgMemberWriteModel.WriteModel)
if len(orgMemberRoles) > 0 {
orgMember := &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: orgID,
},
UserID: human.AggregateID,
Roles: orgMemberRoles,
}
memberEvent, err := c.addOrgMember(ctx, orgAgg, orgMemberWriteModel, orgMember)
if err != nil {
return nil, err
}
userEvents = append(userEvents, memberEvent)
}
pushedEvents, err := c.eventstore.Push(ctx, userEvents...)
if err != nil {
return nil, err
}
err = AppendAndReduce(registeredHuman, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToHuman(registeredHuman), nil
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" {
return nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty")
@ -522,7 +473,7 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.
if err := human.Normalize(); err != nil {
return nil, nil, nil, "", err
}
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
@ -537,33 +488,8 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.
return events, humanWriteModel, passwordlessCodeWriteModel, code, nil
}
func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
if human == nil {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-JKefw", "Errors.User.Invalid")
}
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
human.Username = string(human.EmailAddress)
}
if orgID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-hYsVH", "Errors.Org.Empty")
}
if err := human.Normalize(); err != nil {
return nil, nil, err
}
if link == nil && (human.Password == nil || human.Password.SecretString == "") {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-X23na", "Errors.User.Password.Empty")
}
if human.Password != nil && human.Password.SecretString != "" {
human.Password.ChangeRequired = false
}
var links []*domain.UserIDPLink
if link != nil {
links = append(links, link)
}
return c.createHuman(ctx, orgID, human, links, true, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
}
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) {
//nolint:gocognit
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) {
if err := human.CheckDomainPolicy(domainPolicy); err != nil {
return nil, nil, err
}
@ -601,11 +527,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
//TODO: adlerhurst maybe we could simplify the code below
userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel)
if selfregister {
events = append(events, createRegisterHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain))
} else {
events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain))
}
for _, link := range links {
event, err := c.addUserIDPLink(ctx, userAgg, link, false)
@ -620,7 +542,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
if err != nil {
return nil, nil, err
}
events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry))
events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, ""))
} else {
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
@ -629,7 +551,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
if err != nil {
return nil, nil, err
}
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry))
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, ""))
}
}
@ -699,40 +621,6 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h
return addEvent
}
func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, human *domain.Human, userLoginMustBeDomain bool) *user.HumanRegisteredEvent {
addEvent := user.NewHumanRegisteredEvent(
ctx,
aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.EmailAddress,
userLoginMustBeDomain,
)
if human.Phone != nil {
addEvent.AddPhoneData(human.PhoneNumber)
}
if human.Address != nil {
addEvent.AddAddressData(
human.Country,
human.Locality,
human.PostalCode,
human.Region,
human.StreetAddress)
}
if human.Password != nil {
addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
}
if human.HashedPassword != "" {
addEvent.AddPasswordData(human.HashedPassword, false)
}
return addEvent
}
func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error {
if agentID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
@ -783,3 +671,53 @@ func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryRe
err = humanWriteModel.Reduce()
return humanWriteModel, err
}
func AddHumanFromDomain(user *domain.Human, metadataList []*domain.Metadata, authRequest *domain.AuthRequest, idp *domain.UserIDPLink) *AddHuman {
addMetadata := make([]*AddMetadataEntry, len(metadataList))
for i, metadata := range metadataList {
addMetadata[i] = &AddMetadataEntry{
Key: metadata.Key,
Value: metadata.Value,
}
}
human := new(AddHuman)
if user.Profile != nil {
human.Username = user.Username
human.FirstName = user.FirstName
human.LastName = user.LastName
human.NickName = user.NickName
human.DisplayName = user.DisplayName
human.PreferredLanguage = user.PreferredLanguage
human.Gender = user.Gender
human.Password = user.Password.SecretString
human.Register = true
human.Metadata = addMetadata
human.UserAgentID = authRequest.AgentID
human.AuthRequestID = authRequest.ID
}
if user.Email != nil {
human.Email = Email{
Address: user.EmailAddress,
Verified: user.IsEmailVerified,
}
}
if user.Phone != nil {
human.Phone = Phone{
Number: user.Phone.PhoneNumber,
Verified: user.Phone.IsPhoneVerified,
}
}
if idp != nil {
human.Links = []*AddLink{
{
IDPID: idp.IDPConfigID,
DisplayName: idp.DisplayName,
IDPExternalID: idp.ExternalUserID,
},
}
}
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
human.Username = string(human.Email.Address)
}
return human
}

View File

@ -50,7 +50,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em
if err != nil {
return nil, err
}
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry))
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, ""))
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
@ -99,7 +99,7 @@ func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceo
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid")
}
func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string, emailCodeGenerator crypto.Generator, authRequestID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
@ -122,7 +122,10 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID,
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry))
if authRequestID == "" {
authRequestID = existingEmail.AuthRequestID
}
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, authRequestID))
if err != nil {
return nil, err
}

View File

@ -19,6 +19,7 @@ type HumanEmailWriteModel struct {
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
AuthRequestID string
UserState domain.UserState
}
@ -53,6 +54,7 @@ func (wm *HumanEmailWriteModel) Reduce() error {
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
wm.AuthRequestID = e.AuthRequestID
case *user.HumanEmailVerifiedEvent:
wm.IsEmailVerified = true
wm.Code = nil

View File

@ -18,7 +18,7 @@ import (
func TestCommandSide_ChangeHumanEmail(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -39,9 +39,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "invalid email, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -59,8 +57,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -81,8 +78,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "user not initialized, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -102,6 +98,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -124,8 +121,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "email not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -161,8 +157,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "verified email changed, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -215,8 +210,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "email verified, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -265,8 +259,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "email verified, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -315,8 +308,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
{
name: "email changed with code, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -347,6 +339,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -376,7 +369,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.ChangeHumanEmail(tt.args.ctx, tt.args.email, tt.args.secretGenerator)
if tt.res.err == nil {
@ -394,7 +387,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
func TestCommandSide_VerifyHumanEmail(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -416,9 +409,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -432,9 +423,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "code missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -448,8 +437,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -466,8 +454,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "code not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -499,8 +486,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "invalid code, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -526,6 +512,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -550,8 +537,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
{
name: "valid code, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -577,6 +563,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -604,7 +591,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.VerifyHumanEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.secretGenerator)
if tt.res.err == nil {
@ -622,13 +609,14 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
userID string
resourceOwner string
secretGenerator crypto.Generator
authRequestID string
}
type res struct {
want *domain.ObjectDetails
@ -643,9 +631,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -658,8 +644,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -675,8 +660,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
{
name: "user not initialized, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -696,6 +680,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -713,8 +698,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
{
name: "email already verified, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -750,8 +734,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
{
name: "new code, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -789,6 +772,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -805,13 +789,72 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
},
},
},
{
name: "new code with authRequestID, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusher(
user.NewHumanEmailChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"email2@test.ch",
),
),
),
expectPush(
user.NewHumanEmailCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
authRequestID: "authRequestID",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator)
got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator, tt.args.authRequestID)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -827,7 +870,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) {
func TestCommandSide_EmailVerificationCodeSent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -846,9 +889,7 @@ func TestCommandSide_EmailVerificationCodeSent(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -861,8 +902,7 @@ func TestCommandSide_EmailVerificationCodeSent(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -878,8 +918,7 @@ func TestCommandSide_EmailVerificationCodeSent(t *testing.T) {
{
name: "code sent, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -925,7 +964,7 @@ func TestCommandSide_EmailVerificationCodeSent(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
err := r.HumanEmailVerificationCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID)
if tt.res.err == nil {

View File

@ -13,7 +13,7 @@ import (
)
// ResendInitialMail resend initial mail and changes email if provided
func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email domain.EmailAddress, resourceOwner string, initCodeGenerator crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email domain.EmailAddress, resourceOwner string, initCodeGenerator crypto.Generator, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
}
@ -38,7 +38,10 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email d
if err != nil {
return nil, err
}
events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry))
if authRequestID == "" {
authRequestID = existingCode.AuthRequestID
}
events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, authRequestID))
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, err

View File

@ -19,6 +19,7 @@ type HumanInitCodeWriteModel struct {
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
AuthRequestID string
UserState domain.UserState
}
@ -50,6 +51,7 @@ func (wm *HumanInitCodeWriteModel) Reduce() error {
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
wm.AuthRequestID = e.AuthRequestID
wm.UserState = domain.UserStateInitial
case *user.HumanInitializedCheckSucceededEvent:
wm.Code = nil

View File

@ -18,7 +18,7 @@ import (
func TestCommandSide_ResendInitialMail(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -26,6 +26,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
email string
resourceOwner string
secretGenerator crypto.Generator
authRequestID string
}
type res struct {
want *domain.ObjectDetails
@ -40,9 +41,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -55,8 +54,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -72,8 +70,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
{
name: "user not initialized, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -107,8 +104,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
{
name: "new code email not changed, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -128,6 +124,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -141,6 +138,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -159,10 +157,9 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
},
},
{
name: "new code, ok",
name: "new code email not changed with authRequestID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -182,6 +179,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"authRequestID",
),
),
),
@ -195,6 +193,63 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
email: "email@test.ch",
secretGenerator: GetMockSecretGenerator(t),
authRequestID: "authRequestID",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "new code, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
expectPush(
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -212,10 +267,9 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
},
},
{
name: "new code with change email, ok",
name: "new code with authRequestID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -235,6 +289,62 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"authRequestID",
),
),
),
expectPush(
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
authRequestID: "authRequestID",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "new code with change email, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -252,6 +362,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -273,9 +384,9 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, domain.EmailAddress(tt.args.email), tt.args.resourceOwner, tt.args.secretGenerator)
got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, domain.EmailAddress(tt.args.email), tt.args.resourceOwner, tt.args.secretGenerator, tt.args.authRequestID)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -291,7 +402,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
func TestCommandSide_VerifyInitCode(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
userPasswordHasher *crypto.Hasher
}
type args struct {
@ -316,9 +427,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -332,9 +441,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "code missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -348,8 +455,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -366,8 +472,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "code not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -399,8 +504,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "invalid code, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -426,6 +530,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -450,8 +555,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "valid code, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -477,6 +581,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -506,8 +611,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "valid code with password, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -536,6 +640,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -582,8 +687,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
{
name: "valid code with password and userAgentID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -612,6 +716,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
@ -660,7 +765,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
}
err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.userAgentID, tt.args.secretGenerator)
@ -676,7 +781,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
func TestCommandSide_InitCodeSent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -695,9 +800,7 @@ func TestCommandSide_InitCodeSent(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -710,8 +813,7 @@ func TestCommandSide_InitCodeSent(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -727,8 +829,7 @@ func TestCommandSide_InitCodeSent(t *testing.T) {
{
name: "code sent, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -763,7 +864,7 @@ func TestCommandSide_InitCodeSent(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
err := r.HumanInitCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID)
if tt.res.err == nil {

View File

@ -165,7 +165,7 @@ func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, re
}
// RequestSetPassword generate and send out new code to change password for a specific user
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-M00oL", "Errors.User.UserIDMissing")
}
@ -185,7 +185,7 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.Code, passwordCode.Expiry, notifyType))
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.Code, passwordCode.Expiry, notifyType, authRequestID))
if err != nil {
return nil, err
}

View File

@ -22,7 +22,7 @@ import (
func TestCommandSide_SetOneTimePassword(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
userPasswordHasher *crypto.Hasher
checkPermission domain.PermissionCheck
}
@ -46,9 +46,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -61,8 +59,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -78,8 +75,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
{
name: "missing permission, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -121,8 +117,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
{
name: "change password onetime, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -184,8 +179,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
{
name: "change password no one time, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -248,7 +242,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
checkPermission: tt.fields.checkPermission,
}
@ -268,7 +262,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm
userPasswordHasher *crypto.Hasher
}
@ -293,9 +287,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -308,9 +300,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "password missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -324,8 +314,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -342,8 +331,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "code not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -376,8 +364,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "invalid code, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -404,6 +391,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
),
),
),
@ -424,8 +412,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "set password, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -457,6 +444,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
),
),
),
@ -500,8 +488,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
{
name: "set password with userAgentID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -533,6 +520,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
),
),
),
@ -578,7 +566,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
userEncryption: tt.fields.userEncryption,
}
@ -915,7 +903,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
func TestCommandSide_RequestSetPassword(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -923,6 +911,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
resourceOwner string
notifyType domain.NotificationType
secretGenerator crypto.Generator
authRequestID string
}
type res struct {
want *domain.ObjectDetails
@ -937,9 +926,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -952,8 +939,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -969,8 +955,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
{
name: "user initial, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -990,6 +975,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
eventFromEventPusher(
@ -1018,8 +1004,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
{
name: "new code, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1055,6 +1040,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
),
),
),
@ -1071,13 +1057,70 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
},
},
},
{
name: "new code with authRequestID, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusher(
user.NewHumanInitializedCheckSucceededEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate)),
),
expectPush(
user.NewHumanPasswordCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
domain.NotificationTypeEmail,
"authRequestID",
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
authRequestID: "authRequestID",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType, tt.args.secretGenerator)
got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType, tt.args.secretGenerator, tt.args.authRequestID)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -1093,7 +1136,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
func TestCommandSide_PasswordCodeSent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -1112,9 +1155,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -1127,8 +1168,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -1144,8 +1184,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
{
name: "code sent, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1186,7 +1225,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
err := r.PasswordCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID)
if tt.res.err == nil {
@ -1201,7 +1240,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
func TestCommandSide_CheckPassword(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
userPasswordHasher *crypto.Hasher
}
type args struct {
@ -1224,9 +1263,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -1240,9 +1277,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "password missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@ -1256,8 +1291,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "login policy not found, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
expectFilter(),
),
@ -1275,8 +1309,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "login policy login password not allowed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1316,8 +1349,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1358,8 +1390,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "user locked, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1420,8 +1451,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "existing password empty, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1478,8 +1508,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "password not matching lockout policy not relevant, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1562,8 +1591,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "password not matching, max password attempts reached - user locked, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1653,8 +1681,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "check password, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1734,8 +1761,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "check password, ok, updated hash",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1820,8 +1846,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "check password ok, locked in the mean time",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1900,8 +1925,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
{
name: "regression test old version event",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
@ -1996,7 +2020,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
}
err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq, tt.args.lockoutPolicy)

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -78,6 +79,12 @@ func (key *MachineKey) valid() (err error) {
if err := key.content(); err != nil {
return err
}
// If a key is supplied, it should be a valid public key
if len(key.PublicKey) > 0 {
if _, err := crypto.BytesToPublicKey(key.PublicKey); err != nil {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-5F3h1", "Errors.User.Machine.Key.Invalid")
}
}
key.ExpirationDate, err = domain.ValidateExpirationDate(key.ExpirationDate)
return err
}

View File

@ -18,6 +18,16 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
const fakePubkey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp4qNBuUu/HekF2E5bOtA
oEL76zS0NQdZL3ByEJ3hZplJhE30ITPIOLW3+uaMMM+obl/LLapwG2vdhvutQtx/
FOLJmXysbG3RL9zjXDBT5IE+nGFC7ctsi5FGbHQbAm45E3HHCSk7gfmTy9hxyk1K
GsyU8BDeOWasJO6aeXqpOnRM8vw/fY+6mHVC9CxcIroSfrIabFGe/mP6qpBGeFSn
APymBc/8lca4JaPv2/u/rBhnaAHZiUuCS1+MonWelOb+MSfq48VgtpiaYIVY9szI
esorA6EJ9pO17ROEUpX5wP5Oir+yGJU27jSvLCjvK6fOFX+OwUM9L8047JKoo+Nf
PwIDAQAB
-----END PUBLIC KEY-----`
func TestCommands_AddMachineKey(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -145,7 +155,7 @@ func TestCommands_AddMachineKey(t *testing.T) {
"key1",
domain.AuthNKeyTypeJSON,
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
[]byte("public"),
[]byte(fakePubkey),
),
),
),
@ -161,14 +171,14 @@ func TestCommands_AddMachineKey(t *testing.T) {
},
Type: domain.AuthNKeyTypeJSON,
ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
PublicKey: []byte("public"),
PublicKey: []byte(fakePubkey),
},
},
res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
key: true,
key: false,
},
},
{
@ -194,7 +204,7 @@ func TestCommands_AddMachineKey(t *testing.T) {
"key1",
domain.AuthNKeyTypeJSON,
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
[]byte("public"),
[]byte(fakePubkey),
),
),
),
@ -210,14 +220,35 @@ func TestCommands_AddMachineKey(t *testing.T) {
KeyID: "key1",
Type: domain.AuthNKeyTypeJSON,
ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC),
PublicKey: []byte("public"),
PublicKey: []byte(fakePubkey),
},
},
res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
key: true,
key: false,
},
},
{
"key added with invalid public key",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
key: &MachineKey{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
KeyID: "key1",
Type: domain.AuthNKeyTypeJSON,
PublicKey: []byte("incorrect"),
},
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
}
@ -237,9 +268,8 @@ func TestCommands_AddMachineKey(t *testing.T) {
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
if tt.res.key {
assert.NotEqual(t, "", tt.args.key.PrivateKey)
}
receivedKey := len(tt.args.key.PrivateKey) > 0
assert.Equal(t, tt.res.key, receivedKey)
}
})
}

View File

@ -1797,6 +1797,7 @@ func TestExistsUser(t *testing.T) {
domain.GenderFemale,
"support@zitadel.com",
true,
"userAgentID",
),
}, nil
},

View File

@ -1838,7 +1838,7 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) {
func TestCommands_NewUserEmailEvents(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
userID string
@ -1852,7 +1852,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) {
{
name: "missing userID",
fields: fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args: args{
userID: "",
@ -1862,7 +1862,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) {
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t, expectFilter()),
eventstore: expectEventstore(expectFilter()),
},
args: args{
userID: "user1",
@ -1872,8 +1872,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) {
{
name: "user not initialized",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1893,6 +1892,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -1907,7 +1907,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
_, err := c.NewUserEmailEvents(context.Background(), tt.args.userID)
require.ErrorIs(t, err, tt.wantErr)

View File

@ -131,9 +131,11 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting")
}
// check for permission to create user on resourceOwner
if !human.Register {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil {
return err
}
}
// add resourceowner for the events with the aggregate
existingHuman.ResourceOwner = resourceOwner
@ -159,6 +161,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
human.UserAgentID,
)
} else {
createCmd = user.NewHumanAddedEvent(

View File

@ -232,6 +232,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
domain.GenderUnspecified,
"email@test.ch",
true,
"userAgentID",
),
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
@ -242,6 +243,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("userinit"),
},
time.Hour*1,
"authRequestID",
),
),
),
@ -261,6 +263,8 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
@ -344,6 +348,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("userinit"),
},
time.Hour*1,
"",
),
),
),
@ -414,6 +419,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("userinit"),
},
1*time.Hour,
"",
),
),
),
@ -1031,6 +1037,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("userinit"),
},
1*time.Hour,
"",
),
user.NewHumanPhoneVerifiedEvent(
context.Background(),
@ -1174,6 +1181,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("userinit"),
},
1*time.Hour,
"",
),
user.NewMetadataSetEvent(
context.Background(),
@ -1993,6 +2001,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
nil, time.Hour*1,
"",
),
),
),

View File

@ -167,6 +167,7 @@ func TestCommandSide_userExistsWriteModel(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
),
@ -225,6 +226,7 @@ func TestCommandSide_userExistsWriteModel(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
eventFromEventPusher(
@ -280,6 +282,7 @@ func TestCommandSide_userExistsWriteModel(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"authRequestID",
),
),
eventFromEventPusher(

View File

@ -10,6 +10,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@ -69,7 +70,7 @@ func TestCommands_RequestPasswordReset(t *testing.T) {
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second, ""),
),
),
),
@ -167,7 +168,7 @@ func TestCommands_RequestPasswordResetReturnCode(t *testing.T) {
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second, ""),
),
),
),
@ -279,7 +280,7 @@ func TestCommands_RequestPasswordResetURLTemplate(t *testing.T) {
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second, ""),
),
),
),
@ -390,7 +391,7 @@ func TestCommands_requestPasswordReset(t *testing.T) {
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second, ""),
),
),
),

View File

@ -18,7 +18,7 @@ import (
func TestCommandSide_LockUserV2(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
@ -40,9 +40,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
@ -58,8 +56,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "user not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -77,8 +74,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "user already locked, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -116,8 +112,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "user already locked, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -151,8 +146,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "lock user, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -190,8 +184,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "lock user, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -224,8 +217,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
{
name: "lock user machine, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -260,7 +252,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.LockUserV2(tt.args.ctx, tt.args.userID)
@ -279,7 +271,7 @@ func TestCommandSide_LockUserV2(t *testing.T) {
func TestCommandSide_UnlockUserV2(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
@ -301,9 +293,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
@ -319,8 +309,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "user not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -338,8 +327,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "user already active, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -372,8 +360,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "user already active, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@ -400,8 +387,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "unlock user, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -443,8 +429,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "unlock user, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -481,8 +466,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
{
name: "unlock user machine, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -521,7 +505,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.UnlockUserV2(tt.args.ctx, tt.args.userID)
@ -540,7 +524,7 @@ func TestCommandSide_UnlockUserV2(t *testing.T) {
func TestCommandSide_DeactivateUserV2(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
@ -562,9 +546,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
@ -580,8 +562,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "user not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -599,8 +580,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "user initial, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -620,6 +600,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
user.NewHumanInitialCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil, time.Hour*1,
"",
),
),
),
@ -639,8 +620,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "user already inactive, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -678,8 +658,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "deactivate user, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -722,8 +701,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "deactivate user, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -761,8 +739,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "user machine already inactive, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -796,8 +773,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
{
name: "deactivate user machine, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -832,7 +808,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.DeactivateUserV2(tt.args.ctx, tt.args.userID)
@ -851,7 +827,7 @@ func TestCommandSide_DeactivateUserV2(t *testing.T) {
func TestCommandSide_ReactivateUserV2(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
@ -873,9 +849,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
@ -891,8 +865,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "user not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -910,8 +883,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "user already active, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -944,8 +916,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "user machine already active, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -974,8 +945,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "reactivate user, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1017,8 +987,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "reactivate user, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1055,8 +1024,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
{
name: "reactivate user machine, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -1095,7 +1063,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.ReactivateUserV2(tt.args.ctx, tt.args.userID)
@ -1114,7 +1082,7 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
func TestCommandSide_RemoveUserV2(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
@ -1138,9 +1106,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
@ -1156,8 +1122,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "user not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -1175,8 +1140,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "user removed, notfound error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1217,8 +1181,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "remove user, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1269,8 +1232,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "remove user, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@ -1308,8 +1270,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "user machine already removed, notfound error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -1346,8 +1307,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
{
name: "remove user machine, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewMachineAddedEvent(context.Background(),
@ -1395,7 +1355,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, tt.args.cascadingMemberships, tt.args.grantIDs...)

View File

@ -169,7 +169,7 @@ func (u *userNotifier) reduceInitCodeAdded(event eventstore.Event) (*handler.Sta
return err
}
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e).
SendUserInitCode(ctx, notifyUser, code)
SendUserInitCode(ctx, notifyUser, code, e.AuthRequestID)
if err != nil {
return err
}
@ -226,7 +226,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
return err
}
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e).
SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate)
SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
if err != nil {
return err
}
@ -285,7 +285,7 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler
if e.NotificationType == domain.NotificationTypeSms {
notify = types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, e)
}
err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate)
err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
if err != nil {
return err
}

View File

@ -45,6 +45,7 @@ const (
externalSecure = false
externalProtocol = "http"
defaultOTPEmailTemplate = "/otp/verify?loginName={{.LoginName}}&code={{.Code}}"
authRequestID = "authRequestID"
)
func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
@ -128,7 +129,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname=%s&code=%s&orgID=%s&passwordset=%t", eventOrigin, userID, preferredLoginName, testCode, orgID, false)
expectContent := fmt.Sprintf("%s/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", eventOrigin, "", testCode, preferredLoginName, orgID, false, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -162,7 +163,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname=%s&code=%s&orgID=%s&passwordset=%t", externalProtocol, instancePrimaryDomain, externalPort, userID, preferredLoginName, testCode, orgID, false)
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, preferredLoginName, orgID, false, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -196,6 +197,46 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
},
}, w
},
}, {
name: "button url without event trigger url with authRequestID",
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, preferredLoginName, orgID, false, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
Content: expectContent,
}
codeAlg, code := cryptoValue(t, ctrl, testCode)
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
Domains: []*query.InstanceDomain{{
Domain: instancePrimaryDomain,
IsPrimary: true,
}},
}, nil)
expectTemplateQueries(queries, givenTemplate)
commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil)
return fields{
queries: queries,
commands: commands,
es: eventstore.NewEventstore(&eventstore.Config{
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
}),
userDataCrypto: codeAlg,
}, args{
event: &user.HumanInitialCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
AggregateID: userID,
ResourceOwner: sql.NullString{String: orgID},
CreationDate: time.Now().UTC(),
}),
Code: code,
Expiry: time.Hour,
AuthRequestID: authRequestID,
},
}, w
},
}}
// TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent?
for _, tt := range tests {
@ -305,7 +346,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code=%s&orgID=%s", eventOrigin, userID, testCode, orgID)
expectContent := fmt.Sprintf("%s/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -342,7 +383,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code=%s&orgID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, testCode, orgID)
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -378,6 +419,48 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) {
},
}, w
},
}, {
name: "button url without event trigger url with authRequestID",
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
Content: expectContent,
}
codeAlg, code := cryptoValue(t, ctrl, testCode)
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
Domains: []*query.InstanceDomain{{
Domain: instancePrimaryDomain,
IsPrimary: true,
}},
}, nil)
expectTemplateQueries(queries, givenTemplate)
commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil)
return fields{
queries: queries,
commands: commands,
es: eventstore.NewEventstore(&eventstore.Config{
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
}),
userDataCrypto: codeAlg,
}, args{
event: &user.HumanEmailCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
AggregateID: userID,
ResourceOwner: sql.NullString{String: orgID},
CreationDate: time.Now().UTC(),
}),
Code: code,
Expiry: time.Hour,
URLTemplate: "",
CodeReturned: false,
AuthRequestID: authRequestID,
},
}, w
},
}, {
name: "button url with url template and event trigger url",
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
@ -524,7 +607,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s/ui/login/password/init?userID=%s&code=%s&orgID=%s", eventOrigin, userID, testCode, orgID)
expectContent := fmt.Sprintf("%s/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -561,7 +644,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) {
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?userID=%s&code=%s&orgID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, testCode, orgID)
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
@ -597,6 +680,48 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) {
},
}, w
},
}, {
name: "button url without event trigger url with authRequestID",
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
givenTemplate := "{{.URL}}"
testCode := "testcode"
expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID)
w.message = messages.Email{
Recipients: []string{lastEmail},
Subject: expectMailSubject,
Content: expectContent,
}
codeAlg, code := cryptoValue(t, ctrl, testCode)
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
Domains: []*query.InstanceDomain{{
Domain: instancePrimaryDomain,
IsPrimary: true,
}},
}, nil)
expectTemplateQueries(queries, givenTemplate)
commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil)
return fields{
queries: queries,
commands: commands,
es: eventstore.NewEventstore(&eventstore.Config{
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
}),
userDataCrypto: codeAlg,
}, args{
event: &user.HumanPasswordCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
AggregateID: userID,
ResourceOwner: sql.NullString{String: orgID},
CreationDate: time.Now().UTC(),
}),
Code: code,
Expiry: time.Hour,
URLTemplate: "",
CodeReturned: false,
AuthRequestID: authRequestID,
},
}, w
},
}, {
name: "button url with url template and event trigger url",
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {

View File

@ -10,10 +10,10 @@ import (
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl string) error {
func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl, authRequestID string) error {
var url string
if urlTmpl == "" {
url = login.MailVerificationLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner)
url = login.MailVerificationLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID)
} else {
var buf strings.Builder
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {

View File

@ -19,6 +19,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
origin string
code string
urlTmpl string
authRequestID string
}
tests := []struct {
name string
@ -36,9 +37,10 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
origin: "https://example.com",
code: "123",
urlTmpl: "",
authRequestID: "authRequestID",
},
want: &notifyResult{
url: "https://example.com/ui/login/mail/verification?userID=user1&code=123&orgID=org1",
url: "https://example.com/ui/login/mail/verification?authRequestID=authRequestID&code=123&orgID=org1&userID=user1",
args: map[string]interface{}{"Code": "123"},
messageType: domain.VerifyEmailMessageType,
allowUnverifiedNotificationChannel: true,
@ -54,6 +56,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
origin: "https://example.com",
code: "123",
urlTmpl: "{{",
authRequestID: "authRequestID",
},
want: &notifyResult{},
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
@ -68,6 +71,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
origin: "https://example.com",
code: "123",
urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
authRequestID: "authRequestID",
},
want: &notifyResult{
url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
@ -80,7 +84,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, notify := mockNotify()
err := notify.SendEmailVerificationCode(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl)
err := notify.SendEmailVerificationCode(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})

View File

@ -9,8 +9,8 @@ import (
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code string) error {
url := login.InitUserLink(http_utils.ComposedOrigin(ctx), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet)
func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code, authRequestID string) error {
url := login.InitUserLink(http_utils.ComposedOrigin(ctx), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID)
args := make(map[string]interface{})
args["Code"] = code
return notify(url, args, domain.InitCodeMessageType, true)

View File

@ -10,10 +10,10 @@ import (
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl string) error {
func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl, authRequestID string) error {
var url string
if urlTmpl == "" {
url = login.InitPasswordLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner)
url = login.InitPasswordLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID)
} else {
var buf strings.Builder
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {

View File

@ -157,6 +157,8 @@ type HumanRegisteredEvent struct {
Secret *crypto.CryptoValue `json:"secret,omitempty"` // legacy
EncodedHash string `json:"encodedHash,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
UserAgentID string `json:"userAgentID,omitempty"`
}
func (e *HumanRegisteredEvent) Payload() interface{} {
@ -208,6 +210,7 @@ func NewHumanRegisteredEvent(
gender domain.Gender,
emailAddress domain.EmailAddress,
userLoginMustBeDomain bool,
userAgentID string,
) *HumanRegisteredEvent {
return &HumanRegisteredEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -224,6 +227,7 @@ func NewHumanRegisteredEvent(
Gender: gender,
EmailAddress: emailAddress,
userLoginMustBeDomain: userLoginMustBeDomain,
UserAgentID: userAgentID,
}
}
@ -244,6 +248,7 @@ type HumanInitialCodeAddedEvent struct {
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
AuthRequestID string `json:"authRequestID,omitempty"`
}
func (e *HumanInitialCodeAddedEvent) Payload() interface{} {
@ -263,6 +268,7 @@ func NewHumanInitialCodeAddedEvent(
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
authRequestID string,
) *HumanInitialCodeAddedEvent {
return &HumanInitialCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -273,6 +279,7 @@ func NewHumanInitialCodeAddedEvent(
Code: code,
Expiry: expiry,
TriggeredAtOrigin: http.ComposedOrigin(ctx),
AuthRequestID: authRequestID,
}
}

View File

@ -126,6 +126,8 @@ type HumanEmailCodeAddedEvent struct {
URLTemplate string `json:"url_template,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
// AuthRequest is only used in V1 Login UI
AuthRequestID string `json:"authRequestID,omitempty"`
}
func (e *HumanEmailCodeAddedEvent) Payload() interface{} {
@ -145,8 +147,19 @@ func NewHumanEmailCodeAddedEvent(
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
authRequestID string,
) *HumanEmailCodeAddedEvent {
return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, "", false)
return &HumanEmailCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanEmailCodeAddedType,
),
Code: code,
Expiry: expiry,
TriggeredAtOrigin: http.ComposedOrigin(ctx),
AuthRequestID: authRequestID,
}
}
func NewHumanEmailCodeAddedEventV2(

View File

@ -87,6 +87,8 @@ type HumanPasswordCodeAddedEvent struct {
URLTemplate string `json:"url_template,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
// AuthRequest is only used in V1 Login UI
AuthRequestID string `json:"authRequestID,omitempty"`
}
func (e *HumanPasswordCodeAddedEvent) Payload() interface{} {
@ -107,8 +109,20 @@ func NewHumanPasswordCodeAddedEvent(
code *crypto.CryptoValue,
expiry time.Duration,
notificationType domain.NotificationType,
authRequestID string,
) *HumanPasswordCodeAddedEvent {
return NewHumanPasswordCodeAddedEventV2(ctx, aggregate, code, expiry, notificationType, "", false)
return &HumanPasswordCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordCodeAddedType,
),
Code: code,
Expiry: expiry,
NotificationType: notificationType,
TriggeredAtOrigin: http.ComposedOrigin(ctx),
AuthRequestID: authRequestID,
}
}
func NewHumanPasswordCodeAddedEventV2(

View File

@ -111,6 +111,7 @@ Errors:
Key:
NotFound: Машинният ключ не е намерен
AlreadyExisting: Машинният ключ вече съществува
Invalid: Публичният ключ не е валиден RSA публичен ключ във формат PKIX с PEM кодиране
Secret:
NotExisting: Тайната не съществува
Invalid: Тайната е невалидна

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Klíč stroje nenalezen
AlreadyExisting: Klíč stroje již existuje
Invalid: Veřejný klíč není platný veřejný klíč RSA ve formátu PKIX s kódováním PEM
Secret:
NotExisting: Tajemství neexistuje
Invalid: Tajemství je neplatné

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Maschinen Schlüssel nicht gefunden
AlreadyExisting: Machine Schlüssel exisiert bereits
Invalid: Der öffentliche Schlüssel ist kein gültiger öffentlicher RSA-Schlüssel im PKIX-Format mit PEM-Kodierung
Secret:
NotExisting: Secret existiert nicht
Invalid: Secret ist ungültig

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Machine key not found
AlreadyExisting: Machine key already existing
Invalid: Public key is not a valid RSA public key in PKIX format with PEM encoding
Secret:
NotExisting: Secret doesn't exist
Invalid: Secret is invalid

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Clave de máquina no encontrada
AlreadyExisting: La clave de máquina ya existe
Invalid: La clave pública no es una clave pública RSA válida en formato PKIX con codificación PEM
Secret:
NotExisting: El secreto no existe
Invalid: El secret no es válido

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Clé de la machine non trouvée
AlreadyExisting: Clé de la machine déjà existante
Invalid: La clé publique n'est pas une clé publique RSA valide au format PKIX avec encodage PEM
Secret:
NotExisting: Secret n'existe pas
Invalid: Secret n'est pas valide

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Chiave macchina non trovato
AlreadyExisting: Chiave macchina già esistente
Invalid: La chiave pubblica non è una chiave pubblica RSA valida in formato PKIX con codifica PEM
Secret:
NotExisting: Secret non esiste
Invalid: Secret non è valido

View File

@ -102,6 +102,7 @@ Errors:
Key:
NotFound: マシーンキーが見つかりません
AlreadyExisting: すでに存在しているマシーンキーです
Invalid: 公開キーは、PEM エンコードを使用した PKIX 形式の有効な RSA 公開キーではありません
Secret:
NotExisting: シークレットは存在しません
Invalid: 無効なシークレットです

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Machine key не е пронајден
AlreadyExisting: Machine key веќе постои
Invalid: Јавниот клуч не е валиден јавен клуч RSA во формат PKIX со PEM кодирање
Secret:
NotExisting: Тајната не постои
Invalid: Тајната е невалидна

View File

@ -108,6 +108,7 @@ Errors:
Key:
NotFound: Machine sleutel niet gevonden
AlreadyExisting: Machine sleutel al bestaand
Invalid: De openbare sleutel is geen geldige openbare RSA-sleutel in PKIX-indeling met PEM-codering
Secret:
NotExisting: Geheim bestaat niet
Invalid: Geheim is ongeldig

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Klucz maszyny nie znaleziony
AlreadyExisting: Klucz maszyny już istnieje
Invalid: Klucz publiczny nie jest prawidłowym kluczem publicznym RSA w formacie PKIX z kodowaniem PEM
Secret:
NotExisting: Sekret nie istnieje
Invalid: Sekret jest nieprawidłowy

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: Chave de máquina não encontrada
AlreadyExisting: Chave de máquina já existe
Invalid: A chave pública não é uma chave pública RSA válida no formato PKIX com codificação PEM
Secret:
NotExisting: Segredo não existe
Invalid: Segredo é inválido

View File

@ -110,6 +110,7 @@ Errors:
Key:
NotFound: Машинный ключ не найден
AlreadyExisting: Машинный ключ уже существует
Invalid: Открытый ключ не является допустимым открытым ключом RSA в формате PKIX с кодировкой PEM
Secret:
NotExisting: Ключ не существует
Invalid: Ключ недействителен

View File

@ -109,6 +109,7 @@ Errors:
Key:
NotFound: 未找到机器密钥
AlreadyExisting: 已有的机器钥匙
Invalid: 公钥不是采用 PEM 编码的 PKIX 格式的有效 RSA 公钥
Secret:
NotExisting: 秘密并不存在
Invalid: 秘密是无效的

View File

@ -0,0 +1,33 @@
package database
type Condition interface {
Write(stmt *Statement, columnName string)
}
type Filter[C compare, V value] struct {
comp C
value V
}
func (f Filter[C, V]) Write(stmt *Statement, columnName string) {
prepareWrite(stmt, columnName, f.comp)
stmt.WriteArg(f.value)
}
func prepareWrite[C compare](stmt *Statement, columnName string, comp C) {
stmt.WriteString(columnName)
stmt.WriteRune(' ')
stmt.WriteString(comp.String())
stmt.WriteRune(' ')
}
type compare interface {
numberCompare | textCompare | listCompare
String() string
}
type value interface {
number | text
// TODO: condition must know if it's args are named parameters or not
// number | text | placeholder
}

View File

@ -0,0 +1,57 @@
package database
import "github.com/zitadel/logging"
type ListFilter[V value] struct {
comp listCompare
list []V
}
func NewListEquals[V value](list ...V) *ListFilter[V] {
return newListFilter[V](listEqual, list)
}
func NewListContains[V value](list ...V) *ListFilter[V] {
return newListFilter[V](listContain, list)
}
func NewListNotContains[V value](list ...V) *ListFilter[V] {
return newListFilter[V](listNotContain, list)
}
func newListFilter[V value](comp listCompare, list []V) *ListFilter[V] {
return &ListFilter[V]{
comp: comp,
list: list,
}
}
func (f ListFilter[V]) Write(stmt *Statement, columnName string) {
if len(f.list) == 0 {
logging.WithFields("column", columnName).Debug("skip list filter because no entries defined")
return
}
if f.comp == listNotContain {
stmt.WriteString("NOT(")
}
stmt.WriteString(columnName)
stmt.WriteString(" = ")
if f.comp != listEqual {
stmt.WriteString("ANY(")
}
stmt.WriteArg(f.list)
if f.comp != listEqual {
stmt.WriteString(")")
}
if f.comp == listNotContain {
stmt.WriteRune(')')
}
}
type listCompare uint8
const (
listEqual listCompare = iota
listContain
listNotContain
)

View File

@ -0,0 +1,122 @@
package database
import (
"reflect"
"testing"
)
func TestNewListConstructors(t *testing.T) {
type args struct {
constructor func(t ...string) *ListFilter[string]
t []string
}
tests := []struct {
name string
args args
want *ListFilter[string]
}{
{
name: "NewListEquals",
args: args{
constructor: NewListEquals[string],
t: []string{"as", "df"},
},
want: &ListFilter[string]{
comp: listEqual,
list: []string{"as", "df"},
},
},
{
name: "NewListContains",
args: args{
constructor: NewListContains[string],
t: []string{"as", "df"},
},
want: &ListFilter[string]{
comp: listContain,
list: []string{"as", "df"},
},
},
{
name: "NewListNotContains",
args: args{
constructor: NewListNotContains[string],
t: []string{"as", "df"},
},
want: &ListFilter[string]{
comp: listNotContain,
list: []string{"as", "df"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.args.constructor(tt.args.t...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("number constructor = %v, want %v", got, tt.want)
}
})
}
}
func TestNewListConditionWrite(t *testing.T) {
type args struct {
constructor func(t ...string) *ListFilter[string]
t []string
}
tests := []struct {
name string
args args
want wantQuery
}{
{
name: "ListEquals",
args: args{
constructor: NewListEquals[string],
t: []string{"as", "df"},
},
want: wantQuery{
query: "test = $1",
args: []any{[]string{"as", "df"}},
},
},
{
name: "ListContains",
args: args{
constructor: NewListContains[string],
t: []string{"as", "df"},
},
want: wantQuery{
query: "test = ANY($1)",
args: []any{[]string{"as", "df"}},
},
},
{
name: "ListNotContains",
args: args{
constructor: NewListNotContains[string],
t: []string{"as", "df"},
},
want: wantQuery{
query: "NOT(test = ANY($1))",
args: []any{[]string{"as", "df"}},
},
},
{
name: "empty list",
args: args{
constructor: NewListNotContains[string],
},
want: wantQuery{
query: "",
args: nil,
},
},
}
for _, tt := range tests {
var stmt Statement
t.Run(tt.name, func(t *testing.T) {
tt.args.constructor(tt.args.t...).Write(&stmt, "test")
assertQuery(t, &stmt, tt.want)
})
}
}

View File

@ -0,0 +1,139 @@
package mock
import (
"database/sql"
"database/sql/driver"
"reflect"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
type SQLMock struct {
DB *sql.DB
mock sqlmock.Sqlmock
}
type Expectation func(m sqlmock.Sqlmock)
func NewSQLMock(t *testing.T, expectations ...Expectation) *SQLMock {
db, mock, err := sqlmock.New(
sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
sqlmock.ValueConverterOption(new(TypeConverter)),
)
if err != nil {
t.Fatal("create mock failed", err)
}
for _, expectation := range expectations {
expectation(mock)
}
return &SQLMock{
DB: db,
mock: mock,
}
}
func (m *SQLMock) Assert(t *testing.T) {
t.Helper()
if err := m.mock.ExpectationsWereMet(); err != nil {
t.Errorf("expectations not met: %v", err)
}
m.DB.Close()
}
func ExpectBegin(err error) Expectation {
return func(m sqlmock.Sqlmock) {
e := m.ExpectBegin()
if err != nil {
e.WillReturnError(err)
}
}
}
func ExpectCommit(err error) Expectation {
return func(m sqlmock.Sqlmock) {
e := m.ExpectCommit()
if err != nil {
e.WillReturnError(err)
}
}
}
type ExecOpt func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec
func WithExecArgs(args ...driver.Value) ExecOpt {
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
return e.WithArgs(args...)
}
}
func WithExecErr(err error) ExecOpt {
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
return e.WillReturnError(err)
}
}
func WithExecNoRowsAffected() ExecOpt {
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
return e.WillReturnResult(driver.ResultNoRows)
}
}
func WithExecRowsAffected(affected driver.RowsAffected) ExecOpt {
return func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec {
return e.WillReturnResult(affected)
}
}
func ExpectExec(stmt string, opts ...ExecOpt) Expectation {
return func(m sqlmock.Sqlmock) {
e := m.ExpectExec(stmt)
for _, opt := range opts {
e = opt(e)
}
}
}
type QueryOpt func(m sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery
func WithQueryArgs(args ...driver.Value) QueryOpt {
return func(_ sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
return e.WithArgs(args...)
}
}
func WithQueryErr(err error) QueryOpt {
return func(_ sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
return e.WillReturnError(err)
}
}
func WithQueryResult(columns []string, rows [][]driver.Value) QueryOpt {
return func(m sqlmock.Sqlmock, e *sqlmock.ExpectedQuery) *sqlmock.ExpectedQuery {
mockedRows := m.NewRows(columns)
for _, row := range rows {
mockedRows = mockedRows.AddRow(row...)
}
return e.WillReturnRows(mockedRows)
}
}
func ExpectQuery(stmt string, opts ...QueryOpt) Expectation {
return func(m sqlmock.Sqlmock) {
e := m.ExpectQuery(stmt)
for _, opt := range opts {
e = opt(m, e)
}
}
}
type AnyType[T interface{}] struct{}
// Match satisfies sqlmock.Argument interface
func (a AnyType[T]) Match(v driver.Value) bool {
return reflect.TypeOf(new(T)).Elem().Kind().String() == reflect.TypeOf(v).Kind().String()
}

View File

@ -0,0 +1,78 @@
package mock
import (
"database/sql/driver"
"encoding/hex"
"encoding/json"
"reflect"
"strconv"
"strings"
)
var _ driver.ValueConverter = (*TypeConverter)(nil)
type TypeConverter struct{}
// ConvertValue converts a value to a driver Value.
func (s TypeConverter) ConvertValue(v any) (driver.Value, error) {
if driver.IsValue(v) {
return v, nil
}
value := reflect.ValueOf(v)
if rawMessage, ok := v.(json.RawMessage); ok {
return convertBytes(rawMessage), nil
}
if value.Kind() == reflect.Slice {
//nolint: exhaustive
// only defined types
switch value.Type().Elem().Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return convertSigned(value), nil
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return convertUnsigned(value), nil
case reflect.String:
return convertText(value), nil
}
}
return v, nil
}
// converts a text array to valid pgx v5 representation
func convertSigned(array reflect.Value) string {
slice := make([]string, array.Len())
for i := 0; i < array.Len(); i++ {
slice[i] = strconv.FormatInt(array.Index(i).Int(), 10)
}
return "{" + strings.Join(slice, ",") + "}"
}
// converts a text array to valid pgx v5 representation
func convertUnsigned(array reflect.Value) string {
slice := make([]string, array.Len())
for i := 0; i < array.Len(); i++ {
slice[i] = strconv.FormatUint(array.Index(i).Uint(), 10)
}
return "{" + strings.Join(slice, ",") + "}"
}
// converts a text array to valid pgx v5 representation
func convertText(array reflect.Value) string {
slice := make([]string, array.Len())
for i := 0; i < array.Len(); i++ {
slice[i] = array.Index(i).String()
}
return "{" + strings.Join(slice, ",") + "}"
}
func convertBytes(array []byte) string {
var builder strings.Builder
builder.Grow(hex.EncodedLen(len(array)) + 4)
builder.WriteString(`\x`)
builder.Write(hex.AppendEncode(nil, array))
return builder.String()
}

View File

@ -0,0 +1,100 @@
package database
import (
"time"
"github.com/zitadel/logging"
"golang.org/x/exp/constraints"
)
type NumberFilter[N number] struct {
Filter[numberCompare, N]
}
func NewNumberEquals[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberEqual, n)
}
func NewNumberAtLeast[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberAtLeast, n)
}
func NewNumberAtMost[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberAtMost, n)
}
func NewNumberGreater[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberGreater, n)
}
func NewNumberLess[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberLess, n)
}
func NewNumberUnequal[N number](n N) *NumberFilter[N] {
return newNumberFilter(numberUnequal, n)
}
func newNumberFilter[N number](comp numberCompare, n N) *NumberFilter[N] {
return &NumberFilter[N]{
Filter: Filter[numberCompare, N]{
comp: comp,
value: n,
},
}
}
// NumberBetweenFilter combines [AtLeast] and [AtMost] comparisons
type NumberBetweenFilter[N number] struct {
min, max N
}
func NewNumberBetween[N number](min, max N) *NumberBetweenFilter[N] {
return &NumberBetweenFilter[N]{
min: min,
max: max,
}
}
func (f NumberBetweenFilter[N]) Write(stmt *Statement, columnName string) {
NewNumberAtLeast[N](f.min).Write(stmt, columnName)
stmt.WriteString(" AND ")
NewNumberAtMost[N](f.max).Write(stmt, columnName)
}
type numberCompare uint8
const (
numberEqual numberCompare = iota
numberAtLeast
numberAtMost
numberGreater
numberLess
numberUnequal
)
func (c numberCompare) String() string {
switch c {
case numberEqual:
return "="
case numberAtLeast:
return ">="
case numberAtMost:
return "<="
case numberGreater:
return ">"
case numberLess:
return "<"
case numberUnequal:
return "<>"
default:
logging.WithFields("compare", c).Panic("comparison type not implemented")
return ""
}
}
type number interface {
constraints.Integer | constraints.Float | time.Time
// TODO: condition must know if it's args are named parameters or not
// constraints.Integer | constraints.Float | time.Time | placeholder
}

Some files were not shown because too many files have changed in this diff Show More