mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 01:17:23 +00:00
Merge branch 'main' into integration-tests
This commit is contained in:
commit
11ab645bb7
@ -1,7 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
branches: [
|
branches: [
|
||||||
{name: 'main', channel: 'next'},
|
{name: 'main', channel: 'next'},
|
||||||
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
|
|
||||||
{name: 'next', prerelease: true}
|
{name: 'next', prerelease: true}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -16,11 +16,11 @@ ENV PROTOC_ARCH x86_64
|
|||||||
## protoc and protoc-gen-grpc-web for later use
|
## protoc and protoc-gen-grpc-web for later use
|
||||||
#######################
|
#######################
|
||||||
FROM ${BUILDARCH}-base
|
FROM ${BUILDARCH}-base
|
||||||
ARG PROTOC_VERSION=3.18.0
|
ARG PROTOC_VERSION=22.3
|
||||||
ARG PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip
|
ARG PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip
|
||||||
ARG GRPC_WEB_VERSION=1.3.0
|
ARG GRPC_WEB_VERSION=1.3.0
|
||||||
ARG GATEWAY_VERSION=2.15.1
|
ARG GATEWAY_VERSION=2.15.2
|
||||||
ARG VALIDATOR_VERSION=0.6.2
|
ARG VALIDATOR_VERSION=0.10.1
|
||||||
# no arm specific version available and x86 works fine at the moment:
|
# no arm specific version available and x86 works fine at the moment:
|
||||||
ARG GRPC_WEB=protoc-gen-grpc-web-${GRPC_WEB_VERSION}-linux-x86_64
|
ARG GRPC_WEB=protoc-gen-grpc-web-${GRPC_WEB_VERSION}-linux-x86_64
|
||||||
|
|
||||||
|
@ -73,7 +73,6 @@ COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/statik/statik.go
|
|||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/pkg/grpc pkg/grpc
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/pkg/grpc pkg/grpc
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/v2/zitadel openapi/v2/zitadel
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/v2/zitadel openapi/v2/zitadel
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/statik/statik.go openapi/statik/statik.go
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/statik/statik.go openapi/statik/statik.go
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/templates.gen.go internal/protoc/protoc-gen-authoption/templates.gen.go
|
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption/options.pb.go internal/protoc/protoc-gen-authoption/authoption/options.pb.go
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption/options.pb.go internal/protoc/protoc-gen-authoption/authoption/options.pb.go
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/proto docs/docs/apis/proto
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/proto docs/docs/apis/proto
|
||||||
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/assets docs/docs/apis/assets
|
COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/assets docs/docs/apis/assets
|
||||||
|
@ -15,17 +15,11 @@ protoc \
|
|||||||
-I=/proto/include/ \
|
-I=/proto/include/ \
|
||||||
--go_out $GOPATH/src \
|
--go_out $GOPATH/src \
|
||||||
--go-grpc_out $GOPATH/src \
|
--go-grpc_out $GOPATH/src \
|
||||||
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
$(find ${PROTO_PATH} -iname *.proto)
|
$(find ${PROTO_PATH} -iname *.proto)
|
||||||
|
|
||||||
# generate authoptions code from templates
|
|
||||||
go-bindata \
|
|
||||||
-pkg main \
|
|
||||||
-prefix internal/protoc/protoc-gen-authoption \
|
|
||||||
-o ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption/templates.gen.go \
|
|
||||||
${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption/templates
|
|
||||||
|
|
||||||
# install authoption proto compiler
|
# install authoption proto compiler
|
||||||
go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption
|
go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-auth
|
||||||
|
|
||||||
# output folder for openapi v2
|
# output folder for openapi v2
|
||||||
mkdir -p ${OPENAPI_PATH}
|
mkdir -p ${OPENAPI_PATH}
|
||||||
@ -39,28 +33,20 @@ protoc \
|
|||||||
--grpc-gateway_opt logtostderr=true \
|
--grpc-gateway_opt logtostderr=true \
|
||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--authoption_out ${GRPC_PATH}/system \
|
--auth_out ${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/system.proto
|
${PROTO_PATH}/system.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
mv ${ZITADEL_PATH}/pkg/grpc/system/zitadel/* ${ZITADEL_PATH}/pkg/grpc/system
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/system/zitadel
|
|
||||||
|
|
||||||
protoc \
|
protoc \
|
||||||
-I=/proto/include \
|
-I=/proto/include \
|
||||||
--grpc-gateway_out ${GOPATH}/src \
|
--grpc-gateway_out ${GOPATH}/src \
|
||||||
--grpc-gateway_opt logtostderr=true \
|
--grpc-gateway_opt logtostderr=true \
|
||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--authoption_out ${GRPC_PATH}/admin \
|
--auth_out ${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/admin.proto
|
${PROTO_PATH}/admin.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
mv ${ZITADEL_PATH}/pkg/grpc/admin/zitadel/* ${ZITADEL_PATH}/pkg/grpc/admin
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/admin/zitadel
|
|
||||||
|
|
||||||
protoc \
|
protoc \
|
||||||
-I=/proto/include \
|
-I=/proto/include \
|
||||||
--grpc-gateway_out ${GOPATH}/src \
|
--grpc-gateway_out ${GOPATH}/src \
|
||||||
@ -69,14 +55,10 @@ protoc \
|
|||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--openapiv2_opt allow_delete_body=true \
|
--openapiv2_opt allow_delete_body=true \
|
||||||
--authoption_out ${GRPC_PATH}/management \
|
--auth_out ${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/management.proto
|
${PROTO_PATH}/management.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
mv ${ZITADEL_PATH}/pkg/grpc/management/zitadel/* ${ZITADEL_PATH}/pkg/grpc/management
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/management/zitadel
|
|
||||||
|
|
||||||
protoc \
|
protoc \
|
||||||
-I=/proto/include \
|
-I=/proto/include \
|
||||||
--grpc-gateway_out ${GOPATH}/src \
|
--grpc-gateway_out ${GOPATH}/src \
|
||||||
@ -85,14 +67,10 @@ protoc \
|
|||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--openapiv2_opt allow_delete_body=true \
|
--openapiv2_opt allow_delete_body=true \
|
||||||
--authoption_out=${GRPC_PATH}/auth \
|
--auth_out=${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/auth.proto
|
${PROTO_PATH}/auth.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
mv ${ZITADEL_PATH}/pkg/grpc/auth/zitadel/* ${ZITADEL_PATH}/pkg/grpc/auth
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/auth/zitadel
|
|
||||||
|
|
||||||
protoc \
|
protoc \
|
||||||
-I=/proto/include \
|
-I=/proto/include \
|
||||||
--grpc-gateway_out ${GOPATH}/src \
|
--grpc-gateway_out ${GOPATH}/src \
|
||||||
@ -101,14 +79,10 @@ protoc \
|
|||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--openapiv2_opt allow_delete_body=true \
|
--openapiv2_opt allow_delete_body=true \
|
||||||
--authoption_out=${GRPC_PATH}/user \
|
--auth_out=${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/user/v2alpha/user_service.proto
|
${PROTO_PATH}/user/v2alpha/user_service.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
cp -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel/* ${ZITADEL_PATH}/pkg/grpc
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel
|
|
||||||
|
|
||||||
protoc \
|
protoc \
|
||||||
-I=/proto/include \
|
-I=/proto/include \
|
||||||
--grpc-gateway_out ${GOPATH}/src \
|
--grpc-gateway_out ${GOPATH}/src \
|
||||||
@ -117,12 +91,8 @@ protoc \
|
|||||||
--openapiv2_out ${OPENAPI_PATH} \
|
--openapiv2_out ${OPENAPI_PATH} \
|
||||||
--openapiv2_opt logtostderr=true \
|
--openapiv2_opt logtostderr=true \
|
||||||
--openapiv2_opt allow_delete_body=true \
|
--openapiv2_opt allow_delete_body=true \
|
||||||
--authoption_out=${GRPC_PATH}/session \
|
--auth_out=${GOPATH}/src \
|
||||||
--validate_out=lang=go:${GOPATH}/src \
|
--validate_out=lang=go:${GOPATH}/src \
|
||||||
${PROTO_PATH}/session/v2alpha/session_service.proto
|
${PROTO_PATH}/session/v2alpha/session_service.proto
|
||||||
|
|
||||||
# authoptions are generated into the wrong folder
|
|
||||||
cp -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel/* ${ZITADEL_PATH}/pkg/grpc
|
|
||||||
rm -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel
|
|
||||||
|
|
||||||
echo "done generating grpc"
|
echo "done generating grpc"
|
||||||
|
@ -321,6 +321,8 @@ SystemDefaults:
|
|||||||
ApplicationKeySize: 2048
|
ApplicationKeySize: 2048
|
||||||
Multifactors:
|
Multifactors:
|
||||||
OTP:
|
OTP:
|
||||||
|
# If this is empty, the issuer is the requested domain
|
||||||
|
# This is helpful in scenarios with multiple ZITADEL environments or virtual instances
|
||||||
Issuer: "ZITADEL"
|
Issuer: "ZITADEL"
|
||||||
DomainVerification:
|
DomainVerification:
|
||||||
VerificationGenerator:
|
VerificationGenerator:
|
||||||
|
@ -76,6 +76,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2,48 +2,62 @@ package setup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cockroachdb/cockroach-go/v2/crdb"
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed 10.sql
|
//go:embed 10_create_temp_table.sql
|
||||||
correctCreationDate10 string
|
correctCreationDate10CreateTable string
|
||||||
|
//go:embed 10_fill_table.sql
|
||||||
|
correctCreationDate10FillTable string
|
||||||
|
//go:embed 10_update.sql
|
||||||
|
correctCreationDate10Update string
|
||||||
)
|
)
|
||||||
|
|
||||||
type CorrectCreationDate struct {
|
type CorrectCreationDate struct {
|
||||||
dbClient *database.DB
|
dbClient *database.DB
|
||||||
|
FailAfter time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mig *CorrectCreationDate) Execute(ctx context.Context) (err error) {
|
func (mig *CorrectCreationDate) Execute(ctx context.Context) (err error) {
|
||||||
tx, err := mig.dbClient.Begin()
|
ctx, cancel := context.WithTimeout(ctx, mig.FailAfter)
|
||||||
if err != nil {
|
defer cancel()
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mig.dbClient.Type() == "cockroach" {
|
|
||||||
if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
logging.OnError(tx.Rollback()).Debug("rollback failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = tx.Commit()
|
|
||||||
}()
|
|
||||||
for {
|
for {
|
||||||
res, err := tx.ExecContext(ctx, correctCreationDate10)
|
var affected int64
|
||||||
if err != nil {
|
err = crdb.ExecuteTx(ctx, mig.dbClient.DB, nil, func(tx *sql.Tx) error {
|
||||||
return err
|
if mig.dbClient.Type() == "cockroach" {
|
||||||
}
|
if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil {
|
||||||
affected, _ := res.RowsAffected()
|
return err
|
||||||
logging.WithFields("count", affected).Info("creation dates changed")
|
}
|
||||||
if affected == 0 {
|
}
|
||||||
|
_, err := tx.ExecContext(ctx, correctCreationDate10CreateTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, correctCreationDate10FillTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := tx.ExecContext(ctx, correctCreationDate10Update)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, _ = res.RowsAffected()
|
||||||
|
logging.WithFields("count", affected).Info("creation dates changed")
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
|
if affected == 0 || err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
cmd/setup/10_create_temp_table.sql
Normal file
6
cmd/setup/10_create_temp_table.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE temporary TABLE IF NOT EXISTS wrong_events (
|
||||||
|
instance_id TEXT
|
||||||
|
, event_sequence BIGINT
|
||||||
|
, current_cd TIMESTAMPTZ
|
||||||
|
, next_cd TIMESTAMPTZ
|
||||||
|
);
|
@ -1,10 +1,3 @@
|
|||||||
CREATE temporary TABLE IF NOT EXISTS wrong_events (
|
|
||||||
instance_id TEXT
|
|
||||||
, event_sequence BIGINT
|
|
||||||
, current_cd TIMESTAMPTZ
|
|
||||||
, next_cd TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
TRUNCATE wrong_events;
|
TRUNCATE wrong_events;
|
||||||
|
|
||||||
INSERT INTO wrong_events (
|
INSERT INTO wrong_events (
|
||||||
@ -24,5 +17,3 @@ INSERT INTO wrong_events (
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
event_sequence DESC
|
event_sequence DESC
|
||||||
);
|
);
|
||||||
|
|
||||||
UPDATE eventstore.events e SET creation_date = we.next_cd FROM wrong_events we WHERE e.event_sequence = we.event_sequence and e.instance_id = we.instance_id;
|
|
1
cmd/setup/10_update.sql
Normal file
1
cmd/setup/10_update.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
UPDATE eventstore.events e SET creation_date = we.next_cd FROM wrong_events we WHERE e.event_sequence = we.event_sequence and e.instance_id = we.instance_id;
|
@ -56,16 +56,16 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Steps struct {
|
type Steps struct {
|
||||||
s1ProjectionTable *ProjectionTable
|
s1ProjectionTable *ProjectionTable
|
||||||
s2AssetsTable *AssetTable
|
s2AssetsTable *AssetTable
|
||||||
FirstInstance *FirstInstance
|
FirstInstance *FirstInstance
|
||||||
s4EventstoreIndexes *EventstoreIndexesNew
|
s4EventstoreIndexes *EventstoreIndexesNew
|
||||||
s5LastFailed *LastFailed
|
s5LastFailed *LastFailed
|
||||||
s6OwnerRemoveColumns *OwnerRemoveColumns
|
s6OwnerRemoveColumns *OwnerRemoveColumns
|
||||||
s7LogstoreTables *LogstoreTables
|
s7LogstoreTables *LogstoreTables
|
||||||
s8AuthTokens *AuthTokenIndexes
|
s8AuthTokens *AuthTokenIndexes
|
||||||
s9EventstoreIndexes2 *EventstoreIndexesNew
|
s9EventstoreIndexes2 *EventstoreIndexesNew
|
||||||
s10EventstoreCreationDate *CorrectCreationDate
|
CorrectCreationDate *CorrectCreationDate
|
||||||
}
|
}
|
||||||
|
|
||||||
type encryptionKeyConfig struct {
|
type encryptionKeyConfig struct {
|
||||||
|
@ -33,7 +33,8 @@ func (mig *externalConfigChange) Check() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||||
cmd, err := command.StartCommands(mig.es,
|
cmd, err := command.StartCommands(
|
||||||
|
mig.es,
|
||||||
systemdefaults.SystemDefaults{},
|
systemdefaults.SystemDefaults{},
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
@ -50,6 +51,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -88,7 +88,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
|||||||
steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username(), dbType: config.Database.Type()}
|
steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username(), dbType: config.Database.Type()}
|
||||||
steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient}
|
steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient}
|
||||||
steps.s9EventstoreIndexes2 = New09(dbClient)
|
steps.s9EventstoreIndexes2 = New09(dbClient)
|
||||||
steps.s10EventstoreCreationDate = &CorrectCreationDate{dbClient: dbClient}
|
steps.CorrectCreationDate.dbClient = dbClient
|
||||||
|
|
||||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
|
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
|
||||||
logging.OnError(err).Fatal("unable to start projections")
|
logging.OnError(err).Fatal("unable to start projections")
|
||||||
@ -124,7 +124,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
|||||||
logging.OnError(err).Fatal("unable to migrate step 8")
|
logging.OnError(err).Fatal("unable to migrate step 8")
|
||||||
err = migration.Migrate(ctx, eventstoreClient, steps.s9EventstoreIndexes2)
|
err = migration.Migrate(ctx, eventstoreClient, steps.s9EventstoreIndexes2)
|
||||||
logging.OnError(err).Fatal("unable to migrate step 9")
|
logging.OnError(err).Fatal("unable to migrate step 9")
|
||||||
err = migration.Migrate(ctx, eventstoreClient, steps.s10EventstoreCreationDate)
|
err = migration.Migrate(ctx, eventstoreClient, steps.CorrectCreationDate)
|
||||||
logging.OnError(err).Fatal("unable to migrate step 10")
|
logging.OnError(err).Fatal("unable to migrate step 10")
|
||||||
|
|
||||||
for _, repeatableStep := range repeatableSteps {
|
for _, repeatableStep := range repeatableSteps {
|
||||||
|
@ -30,3 +30,5 @@ FirstInstance:
|
|||||||
MachineKey:
|
MachineKey:
|
||||||
ExpirationDate:
|
ExpirationDate:
|
||||||
Type:
|
Type:
|
||||||
|
CorrectCreationDate:
|
||||||
|
FailAfter: 5m
|
||||||
|
@ -163,6 +163,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
|||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
&http.Client{},
|
&http.Client{},
|
||||||
|
authZRepo,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start commands: %w", err)
|
return fmt.Errorf("cannot start commands: %w", err)
|
||||||
@ -288,7 +289,7 @@ func startAPIs(
|
|||||||
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
|
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries)); err != nil {
|
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
|
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
|
||||||
|
@ -437,6 +437,7 @@
|
|||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[disabled]="false"
|
[disabled]="false"
|
||||||
[(ngModel)]="redirectUris"
|
[(ngModel)]="redirectUris"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
@ -447,6 +448,7 @@
|
|||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[disabled]="false"
|
[disabled]="false"
|
||||||
[(ngModel)]="postLogoutUrisList"
|
[(ngModel)]="postLogoutUrisList"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Atlassian through SAML 2.0
|
title: Connect with Atlassian through SAML 2.0
|
||||||
|
sidebar_label: Atlassian
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on Atlassian.
|
This guide shows how to enable login with ZITADEL on Atlassian.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Auth0 through OIDC
|
title: Connect with Auth0 through OIDC
|
||||||
|
sidebar_label: Auth0 (OIDC)
|
||||||
---
|
---
|
||||||
|
|
||||||
import CreateApp from "../application/_application.mdx";
|
import CreateApp from "../application/_application.mdx";
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Auth0 through SAML 2.0
|
title: Connect with Auth0 through SAML 2.0
|
||||||
|
sidebar_label: Auth0 (SAML)
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on Auth0.
|
This guide shows how to enable login with ZITADEL on Auth0.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with AWS through SAML 2.0
|
title: Connect with AWS through SAML 2.0
|
||||||
|
sidebar_label: Amazon Web Services
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on AWS SSO.
|
This guide shows how to enable login with ZITADEL on AWS SSO.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Gitlab through SAML 2.0
|
title: Connect with Gitlab through SAML 2.0
|
||||||
|
sidebar_label: Gitlab
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on Gitlab.
|
This guide shows how to enable login with ZITADEL on Gitlab.
|
||||||
|
132
docs/docs/guides/integrate/services/google-cloud.mdx
Normal file
132
docs/docs/guides/integrate/services/google-cloud.mdx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: Google Cloud with Workforce Identity Federation (OIDC)
|
||||||
|
sidebar_label: Google Cloud
|
||||||
|
---
|
||||||
|
|
||||||
|
import CreateApp from "../application/_application.mdx";
|
||||||
|
|
||||||
|
This guide shows how to login users and assign roles with [Workforce Identity Federation to Google Cloud](https://cloud.google.com/iam/docs/workforce-identity-federation).
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure your application in ZITADEL
|
||||||
|
- configure an Action to transform claims
|
||||||
|
- create and configure the connection to Google Cloud with Workforce Identity Federation using OpenID Connect (OIDC)
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](/guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](/guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](/guides/manage/console/projects)
|
||||||
|
- prerequisites on Google Cloud side [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation).
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and a Google Cloud. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if
|
||||||
|
> the headings start with "Google Cloud" please refer to the configuration guide on Google Cloud.
|
||||||
|
|
||||||
|
## **Google Cloud**: Configure
|
||||||
|
|
||||||
|
Follow the steps **Before you begin**, **Required roles**, and **create a workforce identity pool** (OIDC) in the [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation).
|
||||||
|
|
||||||
|
Before you create the workforce identity pool provider you should create your application in ZITADEL.
|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
First of all we create the application in your project.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Google Cloud requires just an ID Token as JWT including the [described required and optional scopes](https://cloud.google.com/iam/docs/workforce-identity-federation#attribute-mappings).
|
||||||
|
:::
|
||||||
|
|
||||||
|
Create a new application and click on "I'm a pro. Skip this wizard."
|
||||||
|
|
||||||
|
- **Application Type**: Web
|
||||||
|
- **Grant Types**: Implicit
|
||||||
|
- **Response Type**: ID Token
|
||||||
|
- **Authentication Method**: None
|
||||||
|
|
||||||
|
:::info
|
||||||
|
You need to add the redirect URL and configure token settings after creating the application.
|
||||||
|
:::
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## **ZITADEL**: Redirect url
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
After creating, go to the application settings "Redirect settings" and add the redirect url from Googles configuration guide.
|
||||||
|
It looks something like `https://auth.cloud.google/signin-callback/locations/global/workforcePools/WORKFORCE_POOL_ID/providers/WORKFORCE_PROVIDER_ID`.
|
||||||
|
|
||||||
|
Save the settings.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
Make sure to replace the `WORKFORCE_POOL_ID` and `WORKFORCE_PROVIDER_ID` with your values in the redirect url
|
||||||
|
:::
|
||||||
|
|
||||||
|
## **ZITADEL**: Token settings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
After creating, go to the application settings "Token settings" and configure as follows:
|
||||||
|
|
||||||
|
- **Auth Token Type**: JWT
|
||||||
|
- **Add user roles to the access token**: disabled (optional)
|
||||||
|
- **User roles inside ID Token**: enabled
|
||||||
|
- **User Info inside ID Token**: enabled
|
||||||
|
|
||||||
|
Save the settings.
|
||||||
|
|
||||||
|
## **ZITADEL**: Custom claims
|
||||||
|
|
||||||
|
Go to your project and create roles according to the Groups in Google Cloud.
|
||||||
|
Authorize a test user by assigning roles in ZITADEL.
|
||||||
|
|
||||||
|
Google Cloud expects some claims, including groups, in a specific format as [described here](https://cloud.google.com/iam/docs/workforce-identity-federation#attribute-mappings).
|
||||||
|
Claims can be transformed in ZITADEL with [Actions](/apis/actions/introduction).
|
||||||
|
|
||||||
|
Create an Action with the following code to flatten the roles and include the claim for the users' display name.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If you want to configure a special attribute mapping in the workforce identity pool provider, then adjust the claims accordingly.
|
||||||
|
:::
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function googleGroups(ctx, api) {
|
||||||
|
if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let grants = [];
|
||||||
|
ctx.v1.user.grants.grants.forEach(claim => {
|
||||||
|
claim.roles.forEach(role => {
|
||||||
|
grants.push(claim.projectId+':'+role)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
api.v1.claims.setClaim('google.groups', grants)
|
||||||
|
api.v1.claims.setClaim('google.display_name', ctx.v1.getUser().human.displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
Make sure that the name of the action matches the name of the function.
|
||||||
|
:::
|
||||||
|
|
||||||
|
And add the the Action Script to the following Flow and Trigger:
|
||||||
|
|
||||||
|
- **Flow Type**: Complement Token
|
||||||
|
- **Trigger Type**: Pre access token creation
|
||||||
|
- **Actions**: googleGroups
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## **Google Cloud**: Create a WIP provider
|
||||||
|
|
||||||
|
Complete the steps in the [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) with the `ISSUER_URI` and `CLIENT_ID` from ZITADEL.
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Ping Identity through SAML 2.0
|
title: Connect with Ping Identity through SAML 2.0
|
||||||
|
sidebar_label: Ping Identity
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on Auth0.
|
This guide shows how to enable login with ZITADEL on Auth0.
|
||||||
|
@ -7,6 +7,7 @@ Migrating users from Auth0 to ZITADEL requires the following steps:
|
|||||||
|
|
||||||
- Request and download hashed passwords
|
- Request and download hashed passwords
|
||||||
- Export all user data
|
- Export all user data
|
||||||
|
- Run migration tool to merge Auth0 users and passwords
|
||||||
- Import users and password hashes to ZITADEL
|
- Import users and password hashes to ZITADEL
|
||||||
|
|
||||||
## Export hashed passwords
|
## Export hashed passwords
|
||||||
@ -34,8 +35,37 @@ See this [community post](https://community.auth0.com/t/password-hashes-export-d
|
|||||||
Create a [bulk user export](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports) from the Auth0 Management API.
|
Create a [bulk user export](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports) from the Auth0 Management API.
|
||||||
You will receive a newline-delimited JSON with the requested user data.
|
You will receive a newline-delimited JSON with the requested user data.
|
||||||
|
|
||||||
|
This is an example request, we have included the user id, the email and the name of the user. Make sure to export the users in a json format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url $AUTH0_DOMAIN/api/v2/jobs/users-exports \
|
||||||
|
--header 'authorization: Bearer $TOKEN' \
|
||||||
|
--header 'content-type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"connection_id": "$CONNECTION_ID",
|
||||||
|
"format": "json",
|
||||||
|
"fields": [
|
||||||
|
{"name": "user_id"},
|
||||||
|
{"name": "email"},
|
||||||
|
{"name": "name"},
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Migration Tool
|
||||||
|
|
||||||
|
We have developed a tool that combines your exported user data with their corresponding passwords to generate the import request body for ZITADEL.
|
||||||
|
|
||||||
|
1. Download the latest release of [github.com/zitadel/zitadel-tools](https://github.com/zitadel/zitadel-tools/releases)
|
||||||
|
2. Execute the binary with the following flags:
|
||||||
|
```bash
|
||||||
|
./zitadel-tools migrate auth0 --org=<organisation id> --users=./users.json --passwords=./passwords.json --output=./importBody.json
|
||||||
|
```
|
||||||
|
Use the Organization ID from your ZITADEL instance where you like to add the users.
|
||||||
|
3. You will now get a new file importBody.json which contains the body for the request to the import of ZITADEL
|
||||||
|
|
||||||
## Import users and password hashes to ZITADEL
|
## Import users and password hashes to ZITADEL
|
||||||
|
|
||||||
You will need to merge the received password hashes with the user bulk export.
|
Copy the content from the importBody.json file created in the last step.
|
||||||
|
You can now follow the instructions described in the [Migrate Users](../users) guide to import users to ZITADEL.
|
||||||
After you successfully merged the datasets, you can follow the instructions described in the [Migrate Users](../users) guide to import users to ZITADEL.
|
|
||||||
|
@ -259,6 +259,13 @@ module.exports = {
|
|||||||
sidebarOptions: {
|
sidebarOptions: {
|
||||||
groupPathsBy: "tag",
|
groupPathsBy: "tag",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
specPath: ".artifacts/openapi/zitadel/user/v2alpha/user_service.swagger.json",
|
||||||
|
outputDir: "docs/apis/user_service",
|
||||||
|
sidebarOptions: {
|
||||||
|
groupPathsBy: "tag",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -211,6 +211,7 @@ module.exports = {
|
|||||||
items: [
|
items: [
|
||||||
"guides/integrate/services/gitlab-self-hosted",
|
"guides/integrate/services/gitlab-self-hosted",
|
||||||
"guides/integrate/services/aws-saml",
|
"guides/integrate/services/aws-saml",
|
||||||
|
"guides/integrate/services/google-cloud",
|
||||||
"guides/integrate/services/atlassian-saml",
|
"guides/integrate/services/atlassian-saml",
|
||||||
"guides/integrate/services/gitlab-saml",
|
"guides/integrate/services/gitlab-saml",
|
||||||
"guides/integrate/services/auth0-oidc",
|
"guides/integrate/services/auth0-oidc",
|
||||||
@ -374,6 +375,20 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
items: require("./docs/apis/system/sidebar.js"),
|
items: require("./docs/apis/system/sidebar.js"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "User Lifecycle (Alpha)",
|
||||||
|
link: {
|
||||||
|
type: "generated-index",
|
||||||
|
title: "User Service API (Alpha)",
|
||||||
|
slug: "/apis/user_service",
|
||||||
|
description:
|
||||||
|
"This API is intended to manage users in a ZITADEL instance.\n"+
|
||||||
|
"\n"+
|
||||||
|
"This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.",
|
||||||
|
},
|
||||||
|
items: require("./docs/apis/user_service/sidebar.js"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Assets",
|
label: "Assets",
|
||||||
@ -508,4 +523,4 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
BIN
docs/static/img/guides/integrate/services/google-cloud-action-code.png
vendored
Normal file
BIN
docs/static/img/guides/integrate/services/google-cloud-action-code.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-action-flow.png
vendored
Normal file
BIN
docs/static/img/guides/integrate/services/google-cloud-action-flow.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-create-app.png
vendored
Normal file
BIN
docs/static/img/guides/integrate/services/google-cloud-create-app.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 219 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-redirect-url.png
vendored
Normal file
BIN
docs/static/img/guides/integrate/services/google-cloud-redirect-url.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 159 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-token-settings.png
vendored
Normal file
BIN
docs/static/img/guides/integrate/services/google-cloud-token-settings.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 217 KiB |
@ -14,11 +14,11 @@ const (
|
|||||||
authenticated = "authenticated"
|
authenticated = "authenticated"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
||||||
ctx, span := tracing.NewServerInterceptorSpan(ctx)
|
ctx, span := tracing.NewServerInterceptorSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, verifier, method)
|
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedPermissions, allPermissions, err := getUserMethodPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig, ctxData)
|
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -110,18 +110,6 @@ func HasGlobalPermission(perms []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasGlobalExplicitPermission(perms []string, permToCheck string) bool {
|
|
||||||
for _, perm := range perms {
|
|
||||||
p, ctxID := SplitPermission(perm)
|
|
||||||
if p == permToCheck {
|
|
||||||
if ctxID == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAllPermissionCtxIDs(perms []string) []string {
|
func GetAllPermissionCtxIDs(perms []string) []string {
|
||||||
ctxIDs := make([]string, 0)
|
ctxIDs := make([]string, 0)
|
||||||
for _, perm := range perms {
|
for _, perm := range perms {
|
||||||
@ -132,16 +120,3 @@ func GetAllPermissionCtxIDs(perms []string) []string {
|
|||||||
}
|
}
|
||||||
return ctxIDs
|
return ctxIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetExplicitPermissionCtxIDs(perms []string, searchPerm string) []string {
|
|
||||||
ctxIDs := make([]string, 0)
|
|
||||||
for _, perm := range perms {
|
|
||||||
p, ctxID := SplitPermission(perm)
|
|
||||||
if p == searchPerm {
|
|
||||||
if ctxID != "" {
|
|
||||||
ctxIDs = append(ctxIDs, ctxID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctxIDs
|
|
||||||
}
|
|
||||||
|
@ -14,11 +14,11 @@ type MethodMapping map[string]Option
|
|||||||
type Option struct {
|
type Option struct {
|
||||||
Permission string
|
Permission string
|
||||||
CheckParam string
|
CheckParam string
|
||||||
Feature string
|
AllowSelf bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Config) getPermissionsFromRole(role string) []string {
|
func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string {
|
||||||
for _, roleMap := range a.RolePermissionMappings {
|
for _, roleMap := range rolePermissionMappings {
|
||||||
if roleMap.Role == role {
|
if roleMap.Role == role {
|
||||||
return roleMap.Permissions
|
return roleMap.Permissions
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,28 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPerm string, authConfig Config, ctxData CtxData) (requestedPermissions, allPermissions []string, err error) {
|
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||||
|
ctxData := GetCtxData(ctx)
|
||||||
|
if allowSelf && ctxData.UserID == resourceID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions")
|
||||||
|
err = checkUserResourcePermissions(requestedPermissions, resourceID)
|
||||||
|
userPermissionSpan.EndWithError(err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level),
|
||||||
|
// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately.
|
||||||
|
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
@ -16,13 +37,13 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, dataKey, ctxData)
|
ctx = context.WithValue(ctx, dataKey, ctxData)
|
||||||
memberships, err := t.SearchMyMemberships(ctx)
|
memberships, err := resolver.SearchMyMemberships(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if len(memberships) == 0 {
|
if len(memberships) == 0 {
|
||||||
err = retry(func() error {
|
err = retry(func() error {
|
||||||
memberships, err = t.SearchMyMemberships(ctx)
|
memberships, err = resolver.SearchMyMemberships(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -35,24 +56,56 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
|||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, authConfig)
|
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings)
|
||||||
return requestedPermissions, allPermissions, nil
|
return requestedPermissions, allPermissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, authConfig Config) (requestPermissions, allPermissions []string) {
|
// checkUserResourcePermissions checks that if a user i granted either the requested permission globally (project.write)
|
||||||
|
// or the specific resource (project.write:123)
|
||||||
|
func checkUserResourcePermissions(userPerms []string, resourceID string) error {
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return errors.ThrowPermissionDenied(nil, "AUTH-AWfge", "No matching permissions found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if HasGlobalPermission(userPerms) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasContextResourcePermission(userPerms, resourceID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.ThrowPermissionDenied(nil, "AUTH-Swrgg2", "No matching permissions found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasContextResourcePermission(permissions []string, resourceID string) bool {
|
||||||
|
for _, perm := range permissions {
|
||||||
|
_, ctxID := SplitPermission(perm)
|
||||||
|
if resourceID == ctxID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, roleMappings []RoleMapping) (requestPermissions, allPermissions []string) {
|
||||||
requestPermissions = make([]string, 0)
|
requestPermissions = make([]string, 0)
|
||||||
allPermissions = make([]string, 0)
|
allPermissions = make([]string, 0)
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, authConfig, requestPermissions, allPermissions)
|
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, requestPermissions, allPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestPermissions, allPermissions
|
return requestPermissions, allPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapMembershipToPerm(requiredPerm string, membership *Membership, authConfig Config, requestPermissions, allPermissions []string) ([]string, []string) {
|
func mapMembershipToPerm(requiredPerm string, membership *Membership, roleMappings []RoleMapping, requestPermissions, allPermissions []string) ([]string, []string) {
|
||||||
roleNames, roleContextID := roleWithContext(membership)
|
roleNames, roleContextID := roleWithContext(membership)
|
||||||
for _, roleName := range roleNames {
|
for _, roleName := range roleNames {
|
||||||
perms := authConfig.getPermissionsFromRole(roleName)
|
perms := getPermissionsFromRole(roleMappings, roleName)
|
||||||
|
|
||||||
for _, p := range perms {
|
for _, p := range perms {
|
||||||
permWithCtx := addRoleContextIDToPerm(p, roleContextID)
|
permWithCtx := addRoleContextIDToPerm(p, roleContextID)
|
||||||
|
@ -18,7 +18,7 @@ type testVerifier struct {
|
|||||||
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
||||||
return "userID", "agentID", "clientID", "de", "orgID", nil
|
return "userID", "agentID", "clientID", "de", "orgID", nil
|
||||||
}
|
}
|
||||||
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*Membership, error) {
|
func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) {
|
||||||
return v.memberships, nil
|
return v.memberships, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ func equalStringArray(a, b []string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_GetUserMethodPermissions(t *testing.T) {
|
func Test_GetUserPermissions(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
ctxData CtxData
|
ctxData CtxData
|
||||||
verifier *TokenVerifier
|
verifier *TokenVerifier
|
||||||
@ -139,7 +139,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
_, perms, err := getUserMethodPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig, tt.args.ctxData)
|
_, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
|
||||||
|
|
||||||
if tt.wantErr && err == nil {
|
if tt.wantErr && err == nil {
|
||||||
t.Errorf("got wrong result, should get err: actual: %v ", err)
|
t.Errorf("got wrong result, should get err: actual: %v ", err)
|
||||||
@ -295,7 +295,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig)
|
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings)
|
||||||
if !equalStringArray(requestPerms, tt.requestPerms) {
|
if !equalStringArray(requestPerms, tt.requestPerms) {
|
||||||
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
||||||
}
|
}
|
||||||
@ -435,7 +435,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig, tt.args.requestPerms, tt.args.allPerms)
|
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings, tt.args.requestPerms, tt.args.allPerms)
|
||||||
if !equalStringArray(requestPerms, tt.requestPerms) {
|
if !equalStringArray(requestPerms, tt.requestPerms) {
|
||||||
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
||||||
}
|
}
|
||||||
@ -519,3 +519,109 @@ func Test_ExistisPerm(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CheckUserResourcePermissions(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
perms []string
|
||||||
|
resourceID string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no permissions",
|
||||||
|
args: args{
|
||||||
|
perms: []string{},
|
||||||
|
resourceID: "",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has permission and no context requested",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"project.read"},
|
||||||
|
resourceID: "",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "context requested and has global permission",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"project.read", "project.read:1"},
|
||||||
|
resourceID: "Test",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "context requested and has specific permission",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"project.read:Test"},
|
||||||
|
resourceID: "Test",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "context requested and has no permission",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"project.read:Test"},
|
||||||
|
resourceID: "Hodor",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := checkUserResourcePermissions(tt.args.perms, tt.args.resourceID)
|
||||||
|
if tt.wantErr && err == nil {
|
||||||
|
t.Errorf("got wrong result, should get err: actual: %v ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr && err != nil {
|
||||||
|
t.Errorf("shouldn't get err: %v ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr && !caos_errs.IsPermissionDenied(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_HasContextResourcePermission(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
perms []string
|
||||||
|
resourceID string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "existing context permission",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"test:wrong", "test:right"},
|
||||||
|
resourceID: "right",
|
||||||
|
},
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not existing context permission",
|
||||||
|
args: args{
|
||||||
|
perms: []string{"test:wrong", "test:wrong2"},
|
||||||
|
resourceID: "test",
|
||||||
|
},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := hasContextResourcePermission(tt.args.perms, tt.args.resourceID)
|
||||||
|
if result != tt.result {
|
||||||
|
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,10 +27,14 @@ type TokenVerifier struct {
|
|||||||
systemJWTProfile op.JWTProfileVerifier
|
systemJWTProfile op.JWTProfileVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MembershipsResolver interface {
|
||||||
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||||
|
}
|
||||||
|
|
||||||
type authZRepo interface {
|
type authZRepo interface {
|
||||||
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
|
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
|
||||||
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
||||||
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
||||||
ExistsOrg(ctx context.Context, orgID string) error
|
ExistsOrg(ctx context.Context, orgID string) error
|
||||||
}
|
}
|
||||||
@ -127,10 +131,10 @@ func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings Me
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context) (_ []*Membership, err error) {
|
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string) (_ []*Membership, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
return v.authZRepo.SearchMyMemberships(ctx)
|
return v.authZRepo.SearchMyMemberships(ctx, orgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
|
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
|
||||||
|
@ -71,7 +71,7 @@ func (s *Server) AppName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MethodPrefix() string {
|
func (s *Server) MethodPrefix() string {
|
||||||
return admin.AdminService_MethodPrefix
|
return admin.AdminService_ServiceDesc.ServiceName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||||
|
@ -69,7 +69,7 @@ func (s *Server) AppName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MethodPrefix() string {
|
func (s *Server) MethodPrefix() string {
|
||||||
return auth.AuthService_MethodPrefix
|
return auth.AuthService_ServiceDesc.ServiceName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||||
|
@ -63,7 +63,7 @@ func (s *Server) AppName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MethodPrefix() string {
|
func (s *Server) MethodPrefix() string {
|
||||||
return management.ManagementService_MethodPrefix
|
return management.ManagementService_ServiceDesc.ServiceName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||||
|
@ -210,17 +210,14 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
||||||
details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req))
|
human := AddHumanUserRequestToAddHuman(req)
|
||||||
|
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mgmt_pb.AddHumanUserResponse{
|
return &mgmt_pb.AddHumanUserResponse{
|
||||||
UserId: details.ID,
|
UserId: human.ID,
|
||||||
Details: obj_grpc.AddToDetailsPb(
|
Details: obj_grpc.DomainToAddDetailsPb(human.Details),
|
||||||
details.Sequence,
|
|
||||||
details.EventDate,
|
|
||||||
details.ResourceOwner,
|
|
||||||
),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
internal/api/grpc/object/v2/converter.go
Normal file
19
internal/api/grpc/object/v2/converter.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||||
|
details := &object.Details{
|
||||||
|
Sequence: objectDetail.Sequence,
|
||||||
|
ResourceOwner: objectDetail.ResourceOwner,
|
||||||
|
}
|
||||||
|
if !objectDetail.EventDate.IsZero() {
|
||||||
|
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
|
||||||
|
}
|
||||||
|
return details
|
||||||
|
}
|
@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
||||||
|
if o, ok := req.(AuthContext); ok {
|
||||||
|
orgID = o.AuthContext()
|
||||||
|
}
|
||||||
|
|
||||||
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
|
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
|||||||
span.End()
|
span.End()
|
||||||
return handler(ctxSetter(ctx), req)
|
return handler(ctxSetter(ctx), req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthContext interface {
|
||||||
|
AuthContext() string
|
||||||
|
}
|
||||||
|
@ -24,7 +24,7 @@ type verifierMock struct{}
|
|||||||
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
||||||
return "", "", "", "", "", nil
|
return "", "", "", "", "", nil
|
||||||
}
|
}
|
||||||
func (v *verifierMock) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
|
func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +50,13 @@ func CreateServer(
|
|||||||
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
|
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
|
||||||
middleware.NoCacheInterceptor(),
|
middleware.NoCacheInterceptor(),
|
||||||
middleware.ErrorHandler(),
|
middleware.ErrorHandler(),
|
||||||
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_MethodPrefix, healthpb.Health_ServiceDesc.ServiceName),
|
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
|
||||||
middleware.AccessStorageInterceptor(accessSvc),
|
middleware.AccessStorageInterceptor(accessSvc),
|
||||||
middleware.AuthorizationInterceptor(verifier, authConfig),
|
middleware.AuthorizationInterceptor(verifier, authConfig),
|
||||||
middleware.TranslationHandler(),
|
middleware.TranslationHandler(),
|
||||||
middleware.ValidationHandler(),
|
middleware.ValidationHandler(),
|
||||||
middleware.ServiceHandler(),
|
middleware.ServiceHandler(),
|
||||||
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_MethodPrefix),
|
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/admin/repository"
|
"github.com/zitadel/zitadel/internal/admin/repository"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
@ -60,7 +59,7 @@ func (s *Server) AppName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MethodPrefix() string {
|
func (s *Server) MethodPrefix() string {
|
||||||
return system.SystemService_MethodPrefix
|
return system.SystemService_ServiceDesc.ServiceName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||||
|
65
internal/api/grpc/user/v2/email.go
Normal file
65
internal/api/grpc/user/v2/email.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||||
|
var resourceOwner string // TODO: check if still needed
|
||||||
|
var email *domain.Email
|
||||||
|
|
||||||
|
switch v := req.GetVerification().(type) {
|
||||||
|
case *user.SetEmailRequest_SendCode:
|
||||||
|
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
|
||||||
|
case *user.SetEmailRequest_ReturnCode:
|
||||||
|
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||||
|
case *user.SetEmailRequest_IsVerified:
|
||||||
|
if v.IsVerified {
|
||||||
|
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
|
||||||
|
} else {
|
||||||
|
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||||
|
default:
|
||||||
|
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user.SetEmailResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: email.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(email.ChangeDate),
|
||||||
|
ResourceOwner: email.ResourceOwner,
|
||||||
|
},
|
||||||
|
VerificationCode: email.PlainCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
|
||||||
|
details, err := s.command.VerifyUserEmail(ctx,
|
||||||
|
req.GetUserId(),
|
||||||
|
"", // TODO: check if still needed
|
||||||
|
req.GetVerificationCode(),
|
||||||
|
s.userCodeAlg,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.VerifyEmailResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: details.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(details.EventDate),
|
||||||
|
ResourceOwner: details.ResourceOwner,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
@ -6,27 +6,27 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ user.UserServiceServer = (*Server)(nil)
|
var _ user.UserServiceServer = (*Server)(nil)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
user.UnimplementedUserServiceServer
|
user.UnimplementedUserServiceServer
|
||||||
command *command.Commands
|
command *command.Commands
|
||||||
query *query.Queries
|
query *query.Queries
|
||||||
|
userCodeAlg crypto.EncryptionAlgorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct{}
|
type Config struct{}
|
||||||
|
|
||||||
func CreateServer(
|
func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server {
|
||||||
command *command.Commands,
|
|
||||||
query *query.Queries,
|
|
||||||
) *Server {
|
|
||||||
return &Server{
|
return &Server{
|
||||||
command: command,
|
command: command,
|
||||||
query: query,
|
query: query,
|
||||||
|
userCodeAlg: userCodeAlg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) TestGet(ctx context.Context, req *user.TestGetRequest) (*user.TestGetResponse, error) {
|
|
||||||
return &user.TestGetResponse{
|
|
||||||
Ctx: req.Ctx.String(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) TestPost(ctx context.Context, req *user.TestPostRequest) (*user.TestPostResponse, error) {
|
|
||||||
return &user.TestPostResponse{
|
|
||||||
Ctx: req.Ctx.String(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) TestAuth(ctx context.Context, req *user.TestAuthRequest) (*user.TestAuthResponse, error) {
|
|
||||||
reqCtx, err := authDemo(ctx, req.Ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user.TestAuthResponse{
|
|
||||||
User: &user.User{Id: authz.GetCtxData(ctx).UserID},
|
|
||||||
Ctx: reqCtx,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func authDemo(ctx context.Context, reqCtx *user.Context) (*user.Context, error) {
|
|
||||||
ro := authz.GetCtxData(ctx).ResourceOwner
|
|
||||||
if reqCtx == nil {
|
|
||||||
return &user.Context{Ctx: &user.Context_OrgId{OrgId: ro}}, nil
|
|
||||||
}
|
|
||||||
switch c := reqCtx.Ctx.(type) {
|
|
||||||
case *user.Context_OrgId:
|
|
||||||
if c.OrgId == ro {
|
|
||||||
return reqCtx, nil
|
|
||||||
}
|
|
||||||
return nil, errors.ThrowPermissionDenied(nil, "USER-dg4g", "Errors.User.NotAllowedOrg")
|
|
||||||
case *user.Context_OrgDomain:
|
|
||||||
if c.OrgDomain == "forbidden.com" {
|
|
||||||
return nil, errors.ThrowPermissionDenied(nil, "USER-SDg4g", "Errors.User.NotAllowedOrg")
|
|
||||||
}
|
|
||||||
return reqCtx, nil
|
|
||||||
case *user.Context_Instance:
|
|
||||||
return reqCtx, nil
|
|
||||||
default:
|
|
||||||
return reqCtx, nil
|
|
||||||
}
|
|
||||||
}
|
|
112
internal/api/grpc/user/v2/user.go
Normal file
112
internal/api/grpc/user/v2/user.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||||
|
human, err := addUserRequestToAddHuman(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orgID := req.GetOrganisation().GetOrgId()
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = authz.GetCtxData(ctx).OrgID
|
||||||
|
}
|
||||||
|
err = s.command.AddHuman(ctx, orgID, human, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.AddHumanUserResponse{
|
||||||
|
UserId: human.ID,
|
||||||
|
Details: object.DomainToDetailsPb(human.Details),
|
||||||
|
EmailCode: human.EmailCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||||
|
username := req.GetUsername()
|
||||||
|
if username == "" {
|
||||||
|
username = req.GetEmail().GetEmail()
|
||||||
|
}
|
||||||
|
var urlTemplate string
|
||||||
|
if req.GetEmail().GetSendCode() != nil {
|
||||||
|
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
|
||||||
|
// test the template execution so the async notification will not fail because of it and the user won't realize
|
||||||
|
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
|
||||||
|
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
|
||||||
|
for i, metadataEntry := range req.Metadata {
|
||||||
|
metadata[i] = &command.AddMetadataEntry{
|
||||||
|
Key: metadataEntry.GetKey(),
|
||||||
|
Value: metadataEntry.GetValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &command.AddHuman{
|
||||||
|
ID: req.GetUserId(),
|
||||||
|
Username: username,
|
||||||
|
FirstName: req.GetProfile().GetFirstName(),
|
||||||
|
LastName: req.GetProfile().GetLastName(),
|
||||||
|
NickName: req.GetProfile().GetNickName(),
|
||||||
|
DisplayName: req.GetProfile().GetDisplayName(),
|
||||||
|
Email: command.Email{
|
||||||
|
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
|
||||||
|
Verified: req.GetEmail().GetIsVerified(),
|
||||||
|
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
||||||
|
URLTemplate: urlTemplate,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
||||||
|
Gender: genderToDomain(req.GetProfile().GetGender()),
|
||||||
|
Phone: command.Phone{}, // TODO: add as soon as possible
|
||||||
|
Password: req.GetPassword().GetPassword(),
|
||||||
|
BcryptedPassword: bcryptedPassword,
|
||||||
|
PasswordChangeRequired: passwordChangeRequired,
|
||||||
|
Passwordless: false,
|
||||||
|
ExternalIDP: false,
|
||||||
|
Register: false,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genderToDomain(gender user.Gender) domain.Gender {
|
||||||
|
switch gender {
|
||||||
|
case user.Gender_GENDER_UNSPECIFIED:
|
||||||
|
return domain.GenderUnspecified
|
||||||
|
case user.Gender_GENDER_FEMALE:
|
||||||
|
return domain.GenderFemale
|
||||||
|
case user.Gender_GENDER_MALE:
|
||||||
|
return domain.GenderMale
|
||||||
|
case user.Gender_GENDER_DIVERSE:
|
||||||
|
return domain.GenderDiverse
|
||||||
|
default:
|
||||||
|
return domain.GenderUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
|
||||||
|
if hashed == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// we currently only handle bcrypt
|
||||||
|
if hashed.GetAlgorithm() != "bcrypt" {
|
||||||
|
return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
|
||||||
|
}
|
||||||
|
return hashed.GetHash(), nil
|
||||||
|
}
|
80
internal/api/grpc/user/v2/user_test.go
Normal file
80
internal/api/grpc/user/v2/user_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_hashedPasswordToCommand(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
hashed *user.HashedPassword
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want string
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not hashed",
|
||||||
|
args{
|
||||||
|
hashed: nil,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hashed, not bcrypt",
|
||||||
|
args{
|
||||||
|
hashed: &user.HashedPassword{
|
||||||
|
Hash: "hash",
|
||||||
|
Algorithm: "custom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"",
|
||||||
|
func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hashed, bcrypt",
|
||||||
|
args{
|
||||||
|
hashed: &user.HashedPassword{
|
||||||
|
Hash: "hash",
|
||||||
|
Algorithm: "bcrypt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"hash",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := hashedPasswordToCommand(tt.args.hashed)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -176,7 +176,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm
|
|||||||
if errors.IsErrorAlreadyExists(err) {
|
if errors.IsErrorAlreadyExists(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logging.OnError(err).Warn("initial lock failed")
|
logging.OnError(err).Debug("initial lock failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
|
|||||||
if errors.IsErrorAlreadyExists(err) {
|
if errors.IsErrorAlreadyExists(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logging.OnError(err).Warn("initial lock failed")
|
logging.OnError(err).Debug("initial lock failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,17 +12,17 @@ type UserMembershipRepo struct {
|
|||||||
Queries *query.Queries
|
Queries *query.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context) (_ []*authz.Membership, err error) {
|
func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context, orgID string) (_ []*authz.Membership, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
memberships, err := repo.searchUserMemberships(ctx)
|
memberships, err := repo.searchUserMemberships(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return userMembershipsToMemberships(memberships), nil
|
return userMembershipsToMemberships(memberships), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []*query.Membership, err error) {
|
func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID string) (_ []*query.Membership, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
ctxData := authz.GetCtxData(ctx)
|
ctxData := authz.GetCtxData(ctx)
|
||||||
@ -30,11 +30,11 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(ctxData.OrgID, authz.GetInstance(ctx).InstanceID())
|
orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(orgID, authz.GetInstance(ctx).InstanceID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(ctxData.OrgID)
|
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserMembershipRepository interface {
|
type UserMembershipRepository interface {
|
||||||
SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error)
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,9 @@ import (
|
|||||||
type Commands struct {
|
type Commands struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
|
checkPermission permissionCheck
|
||||||
|
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||||
|
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
static static.Storage
|
static static.Storage
|
||||||
idGenerator id.Generator
|
idGenerator id.Generator
|
||||||
@ -59,7 +62,8 @@ type Commands struct {
|
|||||||
certificateLifetime time.Duration
|
certificateLifetime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartCommands(es *eventstore.Eventstore,
|
func StartCommands(
|
||||||
|
es *eventstore.Eventstore,
|
||||||
defaults sd.SystemDefaults,
|
defaults sd.SystemDefaults,
|
||||||
zitadelRoles []authz.RoleMapping,
|
zitadelRoles []authz.RoleMapping,
|
||||||
staticStore static.Storage,
|
staticStore static.Storage,
|
||||||
@ -76,6 +80,7 @@ func StartCommands(es *eventstore.Eventstore,
|
|||||||
oidcEncryption,
|
oidcEncryption,
|
||||||
samlEncryption crypto.EncryptionAlgorithm,
|
samlEncryption crypto.EncryptionAlgorithm,
|
||||||
httpClient *http.Client,
|
httpClient *http.Client,
|
||||||
|
membershipsResolver authz.MembershipsResolver,
|
||||||
) (repo *Commands, err error) {
|
) (repo *Commands, err error) {
|
||||||
if externalDomain == "" {
|
if externalDomain == "" {
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||||
@ -102,6 +107,10 @@ func StartCommands(es *eventstore.Eventstore,
|
|||||||
certificateAlgorithm: samlEncryption,
|
certificateAlgorithm: samlEncryption,
|
||||||
webauthnConfig: webAuthN,
|
webauthnConfig: webAuthN,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
|
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||||
|
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
||||||
|
},
|
||||||
|
newEmailCode: newEmailCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||||
|
@ -10,24 +10,33 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
type CryptoCodeWithExpiry struct {
|
||||||
|
Crypted *crypto.CryptoValue
|
||||||
|
Plain string
|
||||||
|
Expiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
|
||||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code := new(CryptoCodeWithExpiry)
|
||||||
switch a := alg.(type) {
|
switch a := alg.(type) {
|
||||||
case crypto.HashAlgorithm:
|
case crypto.HashAlgorithm:
|
||||||
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||||
case crypto.EncryptionAlgorithm:
|
case crypto.EncryptionAlgorithm:
|
||||||
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||||
default:
|
default:
|
||||||
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return value, config.Expiry, nil
|
|
||||||
|
code.Expiry = config.Expiry
|
||||||
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
||||||
|
@ -2,7 +2,6 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
@ -12,12 +11,18 @@ import (
|
|||||||
type Email struct {
|
type Email struct {
|
||||||
Address domain.EmailAddress
|
Address domain.EmailAddress
|
||||||
Verified bool
|
Verified bool
|
||||||
|
|
||||||
|
// ReturnCode is used if the Verified field is false
|
||||||
|
ReturnCode bool
|
||||||
|
|
||||||
|
// URLTemplate can be used to specify a custom link to be sent in the mail verification
|
||||||
|
URLTemplate string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Email) Validate() error {
|
func (e *Email) Validate() error {
|
||||||
return e.Address.Validate()
|
return e.Address.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
||||||
}
|
}
|
||||||
|
@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
||||||
}
|
}
|
||||||
} else if setup.Org.Human != nil {
|
} else if setup.Org.Human != nil {
|
||||||
|
setup.Org.Human.ID = userID
|
||||||
validations = append(validations,
|
validations = append(validations,
|
||||||
AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
|
c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordAlg, c.userEncryption, true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||||
@ -248,3 +249,15 @@ func (m *mockInstance) RequestedHost() string {
|
|||||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newMockPermissionCheckAllowed() permissionCheck {
|
||||||
|
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockPermissionCheckNotAllowed() permissionCheck {
|
||||||
|
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||||
|
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
"github.com/zitadel/zitadel/internal/repository/project"
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrgSetup struct {
|
type OrgSetup struct {
|
||||||
@ -22,22 +22,13 @@ type OrgSetup struct {
|
|||||||
Roles []string
|
Roles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) SetUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) {
|
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, userIDs ...string) (userID string, token string, machineKey *MachineKey, details *domain.ObjectDetails, err error) {
|
||||||
existingOrg, err := c.getOrgWriteModelByID(ctx, orgID)
|
userID, err = c.idGenerator.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", "", nil, nil, err
|
||||||
}
|
}
|
||||||
if existingOrg != nil {
|
|
||||||
return "", nil, errors.ThrowPreconditionFailed(nil, "COMMAND-poaj2", "Errors.Org.AlreadyExisting")
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...)
|
|
||||||
return userID, details, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, string, *MachineKey, *domain.ObjectDetails, error) {
|
|
||||||
orgAgg := org.NewAggregate(orgID)
|
orgAgg := org.NewAggregate(orgID)
|
||||||
userAgg := user_repo.NewAggregate(userID, orgID)
|
userAgg := user.NewAggregate(userID, orgID)
|
||||||
|
|
||||||
roles := []string{domain.RoleOrgOwner}
|
roles := []string{domain.RoleOrgOwner}
|
||||||
if len(o.Roles) > 0 {
|
if len(o.Roles) > 0 {
|
||||||
@ -49,9 +40,9 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pat *PersonalAccessToken
|
var pat *PersonalAccessToken
|
||||||
var machineKey *MachineKey
|
|
||||||
if o.Human != nil {
|
if o.Human != nil {
|
||||||
validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption))
|
o.Human.ID = userID
|
||||||
|
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordAlg, c.userEncryption, true))
|
||||||
} else if o.Machine != nil {
|
} else if o.Machine != nil {
|
||||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
||||||
if o.Machine.Pat != nil {
|
if o.Machine.Pat != nil {
|
||||||
@ -89,7 +80,6 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
|
|||||||
return "", "", nil, nil, err
|
return "", "", nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var token string
|
|
||||||
if pat != nil {
|
if pat != nil {
|
||||||
token = pat.Token
|
token = pat.Token
|
||||||
}
|
}
|
||||||
@ -107,12 +97,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string)
|
|||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := c.idGenerator.Next()
|
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userIDs...)
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...)
|
|
||||||
return userID, details, err
|
return userID, details, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,9 +350,9 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
|||||||
ResourceOwner(orgID).
|
ResourceOwner(orgID).
|
||||||
OrderAsc().
|
OrderAsc().
|
||||||
AddQuery().
|
AddQuery().
|
||||||
AggregateTypes(user_repo.AggregateType).
|
AggregateTypes(user.AggregateType).
|
||||||
EventTypes(
|
EventTypes(
|
||||||
user_repo.UserIDPLinkAddedType, user_repo.UserIDPLinkRemovedType, user_repo.UserIDPLinkCascadeRemovedType,
|
user.UserIDPLinkAddedType, user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType,
|
||||||
).Builder())
|
).Builder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -375,13 +360,13 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
|||||||
links := make([]*domain.UserIDPLink, 0)
|
links := make([]*domain.UserIDPLink, 0)
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
switch eventTyped := event.(type) {
|
switch eventTyped := event.(type) {
|
||||||
case *user_repo.UserIDPLinkAddedEvent:
|
case *user.UserIDPLinkAddedEvent:
|
||||||
links = append(links, &domain.UserIDPLink{
|
links = append(links, &domain.UserIDPLink{
|
||||||
IDPConfigID: eventTyped.IDPConfigID,
|
IDPConfigID: eventTyped.IDPConfigID,
|
||||||
ExternalUserID: eventTyped.ExternalUserID,
|
ExternalUserID: eventTyped.ExternalUserID,
|
||||||
DisplayName: eventTyped.DisplayName,
|
DisplayName: eventTyped.DisplayName,
|
||||||
})
|
})
|
||||||
case *user_repo.UserIDPLinkRemovedEvent:
|
case *user.UserIDPLinkRemovedEvent:
|
||||||
for i := range links {
|
for i := range links {
|
||||||
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
||||||
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
||||||
@ -392,7 +377,7 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case *user_repo.UserIDPLinkCascadeRemovedEvent:
|
case *user.UserIDPLinkCascadeRemovedEvent:
|
||||||
for i := range links {
|
for i := range links {
|
||||||
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
||||||
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
||||||
@ -495,14 +480,14 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI
|
|||||||
ResourceOwner(orgID).
|
ResourceOwner(orgID).
|
||||||
OrderAsc().
|
OrderAsc().
|
||||||
AddQuery().
|
AddQuery().
|
||||||
AggregateTypes(user_repo.AggregateType).
|
AggregateTypes(user.AggregateType).
|
||||||
EventTypes(
|
EventTypes(
|
||||||
user_repo.HumanAddedType,
|
user.HumanAddedType,
|
||||||
user_repo.MachineAddedEventType,
|
user.MachineAddedEventType,
|
||||||
user_repo.HumanRegisteredType,
|
user.HumanRegisteredType,
|
||||||
user_repo.UserDomainClaimedType,
|
user.UserDomainClaimedType,
|
||||||
user_repo.UserUserNameChangedType,
|
user.UserUserNameChangedType,
|
||||||
user_repo.UserRemovedType,
|
user.UserRemovedType,
|
||||||
).Builder())
|
).Builder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -511,25 +496,25 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI
|
|||||||
users := make([]userIDName, 0)
|
users := make([]userIDName, 0)
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
switch eventTyped := event.(type) {
|
switch eventTyped := event.(type) {
|
||||||
case *user_repo.HumanAddedEvent:
|
case *user.HumanAddedEvent:
|
||||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||||
case *user_repo.MachineAddedEvent:
|
case *user.MachineAddedEvent:
|
||||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||||
case *user_repo.HumanRegisteredEvent:
|
case *user.HumanRegisteredEvent:
|
||||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||||
case *user_repo.DomainClaimedEvent:
|
case *user.DomainClaimedEvent:
|
||||||
for i := range users {
|
for i := range users {
|
||||||
if users[i].id == eventTyped.Aggregate().ID {
|
if users[i].id == eventTyped.Aggregate().ID {
|
||||||
users[i].name = eventTyped.UserName
|
users[i].name = eventTyped.UserName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case *user_repo.UsernameChangedEvent:
|
case *user.UsernameChangedEvent:
|
||||||
for i := range users {
|
for i := range users {
|
||||||
if users[i].id == eventTyped.Aggregate().ID {
|
if users[i].id == eventTyped.Aggregate().ID {
|
||||||
users[i].name = eventTyped.UserName
|
users[i].name = eventTyped.UserName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case *user_repo.UserRemovedEvent:
|
case *user.UserRemovedEvent:
|
||||||
for i := range users {
|
for i := range users {
|
||||||
if users[i].id == eventTyped.Aggregate().ID {
|
if users[i].id == eventTyped.Aggregate().ID {
|
||||||
users[i] = users[len(users)-1]
|
users[i] = users[len(users)-1]
|
||||||
|
11
internal/command/permission.go
Normal file
11
internal/command/permission.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type permissionCheck func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error)
|
||||||
|
|
||||||
|
const (
|
||||||
|
permissionUserWrite = "user.write"
|
||||||
|
)
|
@ -2,7 +2,6 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
@ -14,6 +13,6 @@ type Phone struct {
|
|||||||
Verified bool
|
Verified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
|
||||||
}
|
}
|
||||||
|
@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
|
|||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddHuman struct {
|
type AddHuman struct {
|
||||||
|
// ID is optional, if empty it will be generated
|
||||||
|
ID string
|
||||||
// Username is required
|
// Username is required
|
||||||
Username string
|
Username string
|
||||||
// FirstName is required
|
// FirstName is required
|
||||||
@ -43,63 +45,98 @@ type AddHuman struct {
|
|||||||
PreferredLanguage language.Tag
|
PreferredLanguage language.Tag
|
||||||
// Gender is required
|
// Gender is required
|
||||||
Gender domain.Gender
|
Gender domain.Gender
|
||||||
//Phone represents an international phone number
|
// Phone represents an international phone number
|
||||||
Phone Phone
|
Phone Phone
|
||||||
//Password is optional
|
// Password is optional
|
||||||
Password string
|
Password string
|
||||||
//BcryptedPassword is optional
|
// BcryptedPassword is optional
|
||||||
BcryptedPassword string
|
BcryptedPassword string
|
||||||
//PasswordChangeRequired is used if the `Password`-field is set
|
// PasswordChangeRequired is used if the `Password`-field is set
|
||||||
PasswordChangeRequired bool
|
PasswordChangeRequired bool
|
||||||
Passwordless bool
|
Passwordless bool
|
||||||
ExternalIDP bool
|
ExternalIDP bool
|
||||||
Register bool
|
Register bool
|
||||||
|
Metadata []*AddMetadataEntry
|
||||||
|
|
||||||
|
// Details are set after a successful execution of the command
|
||||||
|
Details *domain.ObjectDetails
|
||||||
|
|
||||||
|
// EmailCode is set by the command
|
||||||
|
EmailCode *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
func (h *AddHuman) Validate() (err error) {
|
||||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner)
|
if err := h.Email.Validate(); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if isUserStateExists(existingHuman.UserState) {
|
if h.Username = strings.TrimSpace(h.Username); h.Username == "" {
|
||||||
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
return errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
||||||
|
}
|
||||||
|
if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
||||||
|
}
|
||||||
|
h.ensureDisplayName()
|
||||||
|
|
||||||
|
if h.Phone.Number != "" {
|
||||||
|
if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, metadataEntry := range h.Metadata {
|
||||||
|
if err := metadataEntry.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
type AddMetadataEntry struct {
|
||||||
agg := user.NewAggregate(userID, resourceOwner)
|
Key string
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption))
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AddMetadataEntry) Valid() error {
|
||||||
|
if m.Key = strings.TrimSpace(m.Key); m.Key == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty")
|
||||||
|
}
|
||||||
|
if len(m.Value) == 0 {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
|
||||||
|
if resourceOwner == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
||||||
|
}
|
||||||
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||||
|
c.AddHumanCommand(
|
||||||
|
human,
|
||||||
|
resourceOwner,
|
||||||
|
c.userPasswordAlg,
|
||||||
|
c.userEncryption,
|
||||||
|
allowInitMail,
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := c.eventstore.Push(ctx, cmds...)
|
events, err := c.eventstore.Push(ctx, cmds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
human.Details = &domain.ObjectDetails{
|
||||||
|
Sequence: events[len(events)-1].Sequence(),
|
||||||
|
EventDate: events[len(events)-1].CreationDate(),
|
||||||
|
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &domain.HumanDetails{
|
return nil
|
||||||
ID: userID,
|
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
Sequence: events[len(events)-1].Sequence(),
|
|
||||||
EventDate: events[len(events)-1].CreationDate(),
|
|
||||||
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
|
|
||||||
if resourceOwner == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
|
||||||
}
|
|
||||||
userID, err := c.idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type humanCreationCommand interface {
|
type humanCreationCommand interface {
|
||||||
@ -108,30 +145,18 @@ type humanCreationCommand interface {
|
|||||||
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation {
|
func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
|
||||||
return func() (_ preparation.CreateCommands, err error) {
|
return func() (_ preparation.CreateCommands, err error) {
|
||||||
if err := human.Email.Validate(); err != nil {
|
if err := human.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
|
||||||
}
|
|
||||||
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
|
||||||
}
|
|
||||||
human.ensureDisplayName()
|
|
||||||
|
|
||||||
if human.Phone.Number != "" {
|
|
||||||
if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
|
if err := c.addHumanCommandCheckID(ctx, filter, human, orgID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a := user.NewAggregate(human.ID, orgID)
|
||||||
|
|
||||||
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
|
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -176,55 +201,30 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
|||||||
createCmd.AddPhoneData(human.Phone.Number)
|
createCmd.AddPhoneData(human.Phone.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
if human.Password != "" {
|
if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
|
||||||
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
if human.BcryptedPassword != "" {
|
|
||||||
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds := make([]eventstore.Command, 0, 3)
|
cmds := make([]eventstore.Command, 0, 3)
|
||||||
cmds = append(cmds, createCmd)
|
cmds = append(cmds, createCmd)
|
||||||
|
|
||||||
if human.Email.Verified {
|
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail)
|
||||||
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
//add init code if
|
|
||||||
// email not verified or
|
|
||||||
// user not registered and password set
|
|
||||||
if human.shouldAddInitCode() {
|
|
||||||
value, expiry, err := newUserInitCode(ctx, filter, codeAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
|
||||||
} else {
|
|
||||||
if !human.Email.Verified {
|
|
||||||
value, expiry, err := newEmailCode(ctx, filter, codeAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if human.Phone.Verified {
|
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, a, human, codeAlg)
|
||||||
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
|
if err != nil {
|
||||||
} else if human.Phone.Number != "" {
|
return nil, err
|
||||||
value, expiry, err := newPhoneCode(ctx, filter, codeAlg)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
for _, metadataEntry := range human.Metadata {
|
||||||
}
|
cmds = append(cmds, user.NewMetadataSetEvent(
|
||||||
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
ctx,
|
||||||
|
&a.Aggregate,
|
||||||
|
metadataEntry.Key,
|
||||||
|
metadataEntry.Value,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmds, nil
|
return cmds, nil
|
||||||
@ -232,6 +232,85 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) ([]eventstore.Command, error) {
|
||||||
|
if human.Email.Verified {
|
||||||
|
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
|
||||||
|
// add init code if
|
||||||
|
// email not verified or
|
||||||
|
// user not registered and password set
|
||||||
|
if allowInitMail && human.shouldAddInitCode() {
|
||||||
|
initCode, err := newUserInitCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), nil
|
||||||
|
}
|
||||||
|
if !human.Email.Verified {
|
||||||
|
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if human.Email.ReturnCode {
|
||||||
|
human.EmailCode = &emailCode.Plain
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode)), nil
|
||||||
|
}
|
||||||
|
return cmds, nil
|
||||||
|
}
|
||||||
|
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
|
||||||
|
if human.Phone.Number == "" {
|
||||||
|
return cmds, nil
|
||||||
|
}
|
||||||
|
if human.Phone.Verified {
|
||||||
|
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil
|
||||||
|
}
|
||||||
|
phoneCode, err := newPhoneCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
|
||||||
|
if human.ID == "" {
|
||||||
|
human.ID, err = c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isUserStateExists(existingHuman.UserState) {
|
||||||
|
return errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
|
||||||
|
if human.Password != "" {
|
||||||
|
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if human.BcryptedPassword != "" {
|
||||||
|
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
|
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
|
||||||
if mustBeDomain {
|
if mustBeDomain {
|
||||||
return nil
|
return nil
|
||||||
@ -507,7 +586,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
|
|||||||
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
|
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
|
||||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||||
} else {
|
} else {
|
||||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -651,3 +730,17 @@ func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceo
|
|||||||
}
|
}
|
||||||
return humanWriteModel, nil
|
return humanWriteModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceowner string) (*HumanWriteModel, error) {
|
||||||
|
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
|
||||||
|
events, err := filter(ctx, humanWriteModel.Query())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(events) == 0 {
|
||||||
|
return humanWriteModel, nil
|
||||||
|
}
|
||||||
|
humanWriteModel.AppendEvents(events...)
|
||||||
|
err = humanWriteModel.Reduce()
|
||||||
|
return humanWriteModel, err
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
@ -41,7 +42,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em
|
|||||||
if email.IsEmailVerified {
|
if email.IsEmailVerified {
|
||||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||||
} else {
|
} else {
|
||||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -113,7 +114,7 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID,
|
|||||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified")
|
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified")
|
||||||
}
|
}
|
||||||
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
||||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
@ -71,11 +72,14 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
|||||||
if accountName == "" {
|
if accountName == "" {
|
||||||
accountName = string(human.EmailAddress)
|
accountName = string(human.EmailAddress)
|
||||||
}
|
}
|
||||||
key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
issuer := c.multifactors.OTP.Issuer
|
||||||
|
if issuer == "" {
|
||||||
|
issuer = authz.GetInstance(ctx).RequestedDomain()
|
||||||
|
}
|
||||||
|
key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret))
|
_, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -2,17 +2,19 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
@ -29,16 +31,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
idGenerator id.Generator
|
idGenerator id.Generator
|
||||||
userPasswordAlg crypto.HashAlgorithm
|
userPasswordAlg crypto.HashAlgorithm
|
||||||
codeAlg crypto.EncryptionAlgorithm
|
codeAlg crypto.EncryptionAlgorithm
|
||||||
|
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
orgID string
|
orgID string
|
||||||
human *AddHuman
|
human *AddHuman
|
||||||
secretGenerator crypto.Generator
|
secretGenerator crypto.Generator
|
||||||
|
allowInitMail bool
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
want *domain.HumanDetails
|
want *domain.ObjectDetails
|
||||||
err func(error) bool
|
wantID string
|
||||||
|
wantEmailCode string
|
||||||
|
err func(error) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
userAgg := user.NewAggregate("user1", "org1")
|
userAgg := user.NewAggregate("user1", "org1")
|
||||||
@ -68,9 +74,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
Address: "email@test.ch",
|
Address: "email@test.ch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user invalid, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
},
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with id, already exists, precondition error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", true, ""),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
ID: "user1",
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,6 +145,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
expectFilter(),
|
expectFilter(),
|
||||||
expectFilter(),
|
expectFilter(),
|
||||||
|
expectFilter(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
@ -95,9 +160,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsInternal,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -106,6 +174,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -134,36 +203,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsInternal,
|
err: func(err error) bool {
|
||||||
},
|
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal"))
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user invalid, invalid argument error",
|
|
||||||
fields: fields{
|
|
||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
|
||||||
eventstore: eventstoreExpect(
|
|
||||||
t,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
orgID: "org1",
|
|
||||||
human: &AddHuman{
|
|
||||||
Username: "username",
|
|
||||||
FirstName: "firstname",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
|
||||||
err: errors.IsErrorInvalidArgument,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "add human (with initial code), ok",
|
name: "add human (with initial code), ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -237,16 +290,15 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
Sequence: 0,
|
||||||
ObjectDetails: domain.ObjectDetails{
|
EventDate: time.Time{},
|
||||||
Sequence: 0,
|
ResourceOwner: "org1",
|
||||||
EventDate: time.Time{},
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -254,6 +306,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -330,14 +383,174 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human (with password and email code custom template), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("emailCode"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: false,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human (with password and return email code), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("emailCode"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
ReturnCode: true,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: false,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
wantEmailCode: "emailCode",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -345,6 +558,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -400,14 +614,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -415,6 +628,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -470,14 +684,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -485,6 +698,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -540,14 +754,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -555,6 +768,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -594,9 +808,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -604,6 +821,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -687,15 +905,14 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -703,6 +920,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -787,14 +1005,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -802,6 +1019,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
|
expectFilter(),
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
@ -875,14 +1093,105 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
},
|
||||||
ResourceOwner: "org1",
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human with metadata, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
instance.NewSecretGeneratorAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&instanceAgg.Aggregate,
|
||||||
|
domain.SecretGeneratorTypeInitCode,
|
||||||
|
0,
|
||||||
|
1*time.Hour,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanInitialCodeAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte(""),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMetadataSetEvent(
|
||||||
|
context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
"testKey",
|
||||||
|
[]byte("testValue"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
Metadata: []*AddMetadataEntry{
|
||||||
|
{
|
||||||
|
Key: "testKey",
|
||||||
|
Value: []byte("testValue"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -893,8 +1202,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||||
userEncryption: tt.fields.codeAlg,
|
userEncryption: tt.fields.codeAlg,
|
||||||
idGenerator: tt.fields.idGenerator,
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
newEmailCode: tt.fields.newEmailCode,
|
||||||
}
|
}
|
||||||
got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
|
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@ -904,7 +1214,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
t.Errorf("got wrong err: %v ", err)
|
t.Errorf("got wrong err: %v ", err)
|
||||||
}
|
}
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.Equal(t, tt.res.want, got)
|
assert.Equal(t, tt.res.want, tt.args.human.Details)
|
||||||
|
assert.Equal(t, tt.res.wantID, tt.args.human.ID)
|
||||||
|
assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -958,7 +1270,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -985,7 +1297,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1022,7 +1334,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1065,7 +1377,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1869,7 +2181,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1899,7 +2211,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1939,7 +2251,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1987,7 +2299,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2056,7 +2368,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2125,7 +2437,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2211,7 +2523,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3147,7 +3459,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
|||||||
userID: "",
|
userID: "",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3164,7 +3476,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
|||||||
userID: "user1",
|
userID: "user1",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsNotFound,
|
err: caos_errs.IsNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3261,7 +3573,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
|||||||
userIDs: []string{"user1"},
|
userIDs: []string{"user1"},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3277,7 +3589,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
|||||||
userIDs: []string{},
|
userIDs: []string{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3479,37 +3791,41 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddHumanCommand(t *testing.T) {
|
func TestAddHumanCommand(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
idGenerator id.Generator
|
||||||
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
a *user.Aggregate
|
human *AddHuman
|
||||||
human *AddHuman
|
orgID string
|
||||||
passwordAlg crypto.HashAlgorithm
|
passwordAlg crypto.HashAlgorithm
|
||||||
filter preparation.FilterToQueryReducer
|
filter preparation.FilterToQueryReducer
|
||||||
codeAlg crypto.EncryptionAlgorithm
|
codeAlg crypto.EncryptionAlgorithm
|
||||||
|
allowInitMail bool
|
||||||
}
|
}
|
||||||
agg := user.NewAggregate("id", "ro")
|
agg := user.NewAggregate("id", "ro")
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
fields fields
|
||||||
want Want
|
args args
|
||||||
|
want Want
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid email",
|
name: "invalid email",
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
Email: Email{
|
Email: Email{
|
||||||
Address: "invalid",
|
Address: "invalid",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orgID: "ro",
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid first name",
|
name: "invalid first name",
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
Username: "username",
|
Username: "username",
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
@ -3517,30 +3833,33 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
Address: "support@zitadel.com",
|
Address: "support@zitadel.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orgID: "ro",
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid last name",
|
name: "invalid last name",
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
Username: "username",
|
Username: "username",
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
FirstName: "hurst",
|
FirstName: "hurst",
|
||||||
Email: Email{Address: "support@zitadel.com"},
|
Email: Email{Address: "support@zitadel.com"},
|
||||||
},
|
},
|
||||||
|
orgID: "ro",
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid password",
|
name: "invalid password",
|
||||||
|
fields: fields{
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
Email: Email{Address: "support@zitadel.com"},
|
Email: Email{Address: "support@zitadel.com"},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
@ -3549,23 +3868,28 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
Password: "short",
|
Password: "short",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
},
|
},
|
||||||
|
orgID: "ro",
|
||||||
filter: NewMultiFilter().Append(
|
filter: NewMultiFilter().Append(
|
||||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
return []eventstore.Event{
|
return []eventstore.Event{}, nil
|
||||||
org.NewDomainPolicyAddedEvent(
|
|
||||||
context.Background(),
|
|
||||||
&org.NewAggregate("id").Aggregate,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
}).
|
}).
|
||||||
|
Append(
|
||||||
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
|
return []eventstore.Event{
|
||||||
|
org.NewDomainPolicyAddedEvent(
|
||||||
|
ctx,
|
||||||
|
&org.NewAggregate("id").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}).
|
||||||
Append(
|
Append(
|
||||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
return []eventstore.Event{
|
return []eventstore.Event{
|
||||||
org.NewPasswordComplexityPolicyAddedEvent(
|
org.NewPasswordComplexityPolicyAddedEvent(
|
||||||
context.Background(),
|
ctx,
|
||||||
&org.NewAggregate("id").Aggregate,
|
&org.NewAggregate("id").Aggregate,
|
||||||
8,
|
8,
|
||||||
true,
|
true,
|
||||||
@ -3578,13 +3902,15 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
Filter(),
|
Filter(),
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "correct",
|
name: "correct",
|
||||||
|
fields: fields{
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
Email: Email{Address: "support@zitadel.com", Verified: true},
|
Email: Email{Address: "support@zitadel.com", Verified: true},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
@ -3593,25 +3919,30 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
Password: "password",
|
Password: "password",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
},
|
},
|
||||||
|
orgID: "ro",
|
||||||
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
filter: NewMultiFilter().Append(
|
filter: NewMultiFilter().Append(
|
||||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
return []eventstore.Event{
|
return []eventstore.Event{}, nil
|
||||||
org.NewDomainPolicyAddedEvent(
|
|
||||||
context.Background(),
|
|
||||||
&org.NewAggregate("id").Aggregate,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
}).
|
}).
|
||||||
|
Append(
|
||||||
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
|
return []eventstore.Event{
|
||||||
|
org.NewDomainPolicyAddedEvent(
|
||||||
|
ctx,
|
||||||
|
&org.NewAggregate("id").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}).
|
||||||
Append(
|
Append(
|
||||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||||
return []eventstore.Event{
|
return []eventstore.Event{
|
||||||
org.NewPasswordComplexityPolicyAddedEvent(
|
org.NewPasswordComplexityPolicyAddedEvent(
|
||||||
context.Background(),
|
ctx,
|
||||||
&org.NewAggregate("id").Aggregate,
|
&org.NewAggregate("id").Aggregate,
|
||||||
2,
|
2,
|
||||||
false,
|
false,
|
||||||
@ -3654,7 +3985,25 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
AssertValidation(t, context.Background(), AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg), tt.args.filter, tt.want)
|
c := &Commands{
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
}
|
||||||
|
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
|
return &CryptoCodeWithExpiry{
|
||||||
|
Crypted: &crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte(code),
|
||||||
|
},
|
||||||
|
Plain: code,
|
||||||
|
Expiry: exp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
208
internal/command/user_v2_email.go
Normal file
208
internal/command/user_v2_email.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeUserEmail sets a user's email address, generates a code
|
||||||
|
// and triggers a notification e-mail with the default confirmation URL format.
|
||||||
|
func (c *Commands) ChangeUserEmail(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
|
||||||
|
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserEmailURLTemplate sets a user's email address, generates a code
|
||||||
|
// and triggers a notification e-mail with the confirmation URL rendered from the passed urlTmpl.
|
||||||
|
// urlTmpl must be a valid [tmpl.Template].
|
||||||
|
func (c *Commands) ChangeUserEmailURLTemplate(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, urlTmpl string) (*domain.Email, error) {
|
||||||
|
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTmpl, userID, "code", "orgID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, urlTmpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserEmailReturnCode sets a user's email address, generates a code and does not send a notification email.
|
||||||
|
// The generated plain text code will be set in the returned Email object.
|
||||||
|
func (c *Commands) ChangeUserEmailReturnCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
|
||||||
|
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, true, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserEmailVerified sets a user's email address and marks it is verified.
|
||||||
|
// No code is generated and no confirmation e-mail is send.
|
||||||
|
func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resourceOwner, email string) (*domain.Email, error) {
|
||||||
|
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd.SetVerified(ctx)
|
||||||
|
return cmd.Push(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) {
|
||||||
|
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gen := crypto.NewEncryptionGenerator(*config, alg)
|
||||||
|
return c.changeUserEmailWithGenerator(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeUserEmailWithGenerator set a user's email address.
|
||||||
|
// returnCode controls if the plain text version of the code will be set in the return object.
|
||||||
|
// When the plain text code is returned, no notification e-mail will be send to the user.
|
||||||
|
// urlTmpl allows changing the target URL that is used by the e-mail and should be a validated Go template, if used.
|
||||||
|
func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) {
|
||||||
|
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd.Push(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) VerifyUserEmail(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) {
|
||||||
|
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gen := crypto.NewEncryptionGenerator(*config, alg)
|
||||||
|
return c.verifyUserEmailWithGenerator(ctx, userID, resourceOwner, code, gen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) verifyUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) {
|
||||||
|
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = cmd.VerifyCode(ctx, code, gen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = cmd.Push(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserEmailEvents allows step-by-step additions of events,
|
||||||
|
// operating on the Human Email Model.
|
||||||
|
type UserEmailEvents struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
aggregate *eventstore.Aggregate
|
||||||
|
events []eventstore.Command
|
||||||
|
model *HumanEmailWriteModel
|
||||||
|
|
||||||
|
plainCode *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserEmailEvents constructs a UserEmailEvents with a Human Email Write Model,
|
||||||
|
// filtered by userID and resourceOwner.
|
||||||
|
// If a model cannot be found, or it's state is invalid and error is returned.
|
||||||
|
func (c *Commands) NewUserEmailEvents(ctx context.Context, userID, resourceOwner string) (*UserEmailEvents, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing")
|
||||||
|
}
|
||||||
|
|
||||||
|
model, err := c.emailWriteModel(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if model.UserState == domain.UserStateUnspecified || model.UserState == domain.UserStateDeleted {
|
||||||
|
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Email.NotFound")
|
||||||
|
}
|
||||||
|
if model.UserState == domain.UserStateInitial {
|
||||||
|
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised")
|
||||||
|
}
|
||||||
|
return &UserEmailEvents{
|
||||||
|
eventstore: c.eventstore,
|
||||||
|
aggregate: UserAggregateFromWriteModel(&model.WriteModel),
|
||||||
|
model: model,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change sets a new email address.
|
||||||
|
// The generated event unsets any previously generated code and verified flag.
|
||||||
|
func (c *UserEmailEvents) Change(ctx context.Context, email domain.EmailAddress) error {
|
||||||
|
if err := email.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
event, hasChanged := c.model.NewChangedEvent(ctx, c.aggregate, email)
|
||||||
|
if !hasChanged {
|
||||||
|
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged")
|
||||||
|
}
|
||||||
|
c.events = append(c.events, event)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVerified sets the email address to verified.
|
||||||
|
func (c *UserEmailEvents) SetVerified(ctx context.Context) {
|
||||||
|
c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddGeneratedCode generates a new encrypted code and sets it to the email address.
|
||||||
|
// When returnCode a plain text of the code will be returned from Push.
|
||||||
|
func (c *UserEmailEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, urlTmpl string, returnCode bool) error {
|
||||||
|
value, plain, err := crypto.NewCode(gen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), urlTmpl, returnCode))
|
||||||
|
if returnCode {
|
||||||
|
c.plainCode = &plain
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserEmailEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error {
|
||||||
|
if code == "" {
|
||||||
|
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen)
|
||||||
|
if err == nil {
|
||||||
|
c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, c.aggregate))
|
||||||
|
logging.WithFields("id", "COMMAND-Zoo6b", "userID", c.aggregate.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
|
||||||
|
return caos_errs.ThrowInvalidArgument(err, "COMMAND-eis9R", "Errors.User.Code.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push all events to the eventstore and Reduce them into the Model.
|
||||||
|
func (c *UserEmailEvents) Push(ctx context.Context) (*domain.Email, error) {
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx, c.events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(c.model, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
email := writeModelToEmail(c.model)
|
||||||
|
email.PlainCode = c.plainCode
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
1315
internal/command/user_v2_email_test.go
Normal file
1315
internal/command/user_v2_email_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,6 @@ import (
|
|||||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HumanDetails struct {
|
|
||||||
ID string
|
|
||||||
ObjectDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
type Human struct {
|
type Human struct {
|
||||||
es_models.ObjectRoot
|
es_models.ObjectRoot
|
||||||
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,6 +38,8 @@ type Email struct {
|
|||||||
|
|
||||||
EmailAddress EmailAddress
|
EmailAddress EmailAddress
|
||||||
IsEmailVerified bool
|
IsEmailVerified bool
|
||||||
|
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||||
|
PlainCode *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailCode struct {
|
type EmailCode struct {
|
||||||
@ -51,13 +56,36 @@ func (e *Email) Validate() error {
|
|||||||
return e.EmailAddress.Validate()
|
return e.EmailAddress.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) {
|
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, string, error) {
|
||||||
emailCodeCrypto, _, err := crypto.NewCode(emailGenerator)
|
emailCodeCrypto, code, err := crypto.NewCode(emailGenerator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return &EmailCode{
|
return &EmailCode{
|
||||||
Code: emailCodeCrypto,
|
Code: emailCodeCrypto,
|
||||||
Expiry: emailGenerator.Expiry(),
|
Expiry: emailGenerator.Expiry(),
|
||||||
}, nil
|
}, code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmURLData struct {
|
||||||
|
UserID string
|
||||||
|
Code string
|
||||||
|
OrgID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderConfirmURLTemplate parses and renders tmplStr.
|
||||||
|
// userID, code and orgID are passed into the [ConfirmURLData].
|
||||||
|
// "%s%s?userID=%s&code=%s&orgID=%s"
|
||||||
|
func RenderConfirmURLTemplate(w io.Writer, tmplStr, userID, code, orgID string) error {
|
||||||
|
tmpl, err := template.New("").Parse(tmplStr)
|
||||||
|
if err != nil {
|
||||||
|
return caos_errs.ThrowInvalidArgument(err, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &ConfirmURLData{userID, code, orgID}
|
||||||
|
if err = tmpl.Execute(w, data); err != nil {
|
||||||
|
return caos_errs.ThrowInvalidArgument(err, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmailValid(t *testing.T) {
|
func TestEmailValid(t *testing.T) {
|
||||||
@ -72,3 +78,57 @@ func TestEmailValid(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderConfirmURLTemplate(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
tmplStr string
|
||||||
|
userID string
|
||||||
|
code string
|
||||||
|
orgID string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid template",
|
||||||
|
args: args{
|
||||||
|
tmplStr: "{{",
|
||||||
|
userID: "user1",
|
||||||
|
code: "123",
|
||||||
|
orgID: "org1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "execution error",
|
||||||
|
args: args{
|
||||||
|
tmplStr: "{{.Foo}}",
|
||||||
|
userID: "user1",
|
||||||
|
code: "123",
|
||||||
|
orgID: "org1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
tmplStr: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||||
|
userID: "user1",
|
||||||
|
code: "123",
|
||||||
|
orgID: "org1",
|
||||||
|
},
|
||||||
|
want: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var w strings.Builder
|
||||||
|
err := RenderConfirmURLTemplate(&w, tt.args.tmplStr, tt.args.userID, tt.args.code, tt.args.orgID)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.want, w.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -223,7 +223,7 @@ func (h *ProjectionHandler) schedule(ctx context.Context) {
|
|||||||
errs := h.lock(lockCtx, h.requeueAfter, "system")
|
errs := h.lock(lockCtx, h.requeueAfter, "system")
|
||||||
if err, ok := <-errs; err != nil || !ok {
|
if err, ok := <-errs; err != nil || !ok {
|
||||||
cancelLock()
|
cancelLock()
|
||||||
logging.WithFields("projection", h.ProjectionName).OnError(err).Warn("initial lock failed for first schedule")
|
logging.WithFields("projection", h.ProjectionName).OnError(err).Debug("initial lock failed for first schedule")
|
||||||
h.triggerProjection.Reset(h.requeueAfter)
|
h.triggerProjection.Reset(h.requeueAfter)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -253,7 +253,7 @@ func (h *ProjectionHandler) schedule(ctx context.Context) {
|
|||||||
//wait until projection is locked
|
//wait until projection is locked
|
||||||
if err, ok := <-errs; err != nil || !ok {
|
if err, ok := <-errs; err != nil || !ok {
|
||||||
cancelInstanceLock()
|
cancelInstanceLock()
|
||||||
logging.WithFields("projection", h.ProjectionName).OnError(err).Warn("initial lock failed")
|
logging.WithFields("projection", h.ProjectionName).OnError(err).Debug("initial lock failed")
|
||||||
failed = true
|
failed = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ func (s *spooledHandler) load(workerID string) {
|
|||||||
var err error
|
var err error
|
||||||
s.succeededOnce, err = s.hasSucceededOnce(ctx)
|
s.succeededOnce, err = s.hasSucceededOnce(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.WithFields("view", s.ViewModel()).OnError(err).Warn("initial lock failed for first schedule")
|
logging.WithFields("view", s.ViewModel()).OnError(err).Debug("initial lock failed for first schedule")
|
||||||
errs <- err
|
errs <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -182,6 +182,9 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
||||||
}
|
}
|
||||||
|
if e.CodeReturned {
|
||||||
|
return crdb.NewNoOpStatement(e), nil
|
||||||
|
}
|
||||||
ctx := HandlerContext(event.Aggregate())
|
ctx := HandlerContext(event.Aggregate())
|
||||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||||
@ -232,7 +235,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
|||||||
e,
|
e,
|
||||||
u.metricSuccessfulDeliveriesEmail,
|
u.metricSuccessfulDeliveriesEmail,
|
||||||
u.metricFailedDeliveriesEmail,
|
u.metricFailedDeliveriesEmail,
|
||||||
).SendEmailVerificationCode(notifyUser, origin, code)
|
).SendEmailVerificationCode(notifyUser, origin, code, e.URLTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error {
|
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string, urlTmpl string) error {
|
||||||
url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner)
|
var url string
|
||||||
|
if urlTmpl == "" {
|
||||||
|
url = login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner)
|
||||||
|
} else {
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
url = buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
args := make(map[string]interface{})
|
args := make(map[string]interface{})
|
||||||
args["Code"] = code
|
args["Code"] = code
|
||||||
return notify(url, args, domain.VerifyEmailMessageType, true)
|
return notify(url, args, domain.VerifyEmailMessageType, true)
|
||||||
|
107
internal/notification/types/email_verification_code_test.go
Normal file
107
internal/notification/types/email_verification_code_test.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||||
|
type res struct {
|
||||||
|
url string
|
||||||
|
args map[string]interface{}
|
||||||
|
messageType string
|
||||||
|
allowUnverifiedNotificationChannel bool
|
||||||
|
}
|
||||||
|
notify := func(dst *res) Notify {
|
||||||
|
return func(
|
||||||
|
url string,
|
||||||
|
args map[string]interface{},
|
||||||
|
messageType string,
|
||||||
|
allowUnverifiedNotificationChannel bool,
|
||||||
|
) error {
|
||||||
|
dst.url = url
|
||||||
|
dst.args = args
|
||||||
|
dst.messageType = messageType
|
||||||
|
dst.allowUnverifiedNotificationChannel = allowUnverifiedNotificationChannel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
user *query.NotifyUser
|
||||||
|
origin string
|
||||||
|
code string
|
||||||
|
urlTmpl string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *res
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default URL",
|
||||||
|
args: args{
|
||||||
|
user: &query.NotifyUser{
|
||||||
|
ID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
origin: "https://example.com",
|
||||||
|
code: "123",
|
||||||
|
urlTmpl: "",
|
||||||
|
},
|
||||||
|
want: &res{
|
||||||
|
url: "https://example.com/ui/login/mail/verification?userID=user1&code=123&orgID=org1",
|
||||||
|
args: map[string]interface{}{"Code": "123"},
|
||||||
|
messageType: domain.VerifyEmailMessageType,
|
||||||
|
allowUnverifiedNotificationChannel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "template error",
|
||||||
|
args: args{
|
||||||
|
user: &query.NotifyUser{
|
||||||
|
ID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
origin: "https://example.com",
|
||||||
|
code: "123",
|
||||||
|
urlTmpl: "{{",
|
||||||
|
},
|
||||||
|
want: &res{},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "template success",
|
||||||
|
args: args{
|
||||||
|
user: &query.NotifyUser{
|
||||||
|
ID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
origin: "https://example.com",
|
||||||
|
code: "123",
|
||||||
|
urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||||
|
},
|
||||||
|
want: &res{
|
||||||
|
url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||||
|
args: map[string]interface{}{"Code": "123"},
|
||||||
|
messageType: domain.VerifyEmailMessageType,
|
||||||
|
allowUnverifiedNotificationChannel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := new(res)
|
||||||
|
err := notify(got).SendEmailVerificationCode(tt.args.user, tt.args.origin, tt.args.code, tt.args.urlTmpl)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl
Normal file
16
internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Code generated by protoc-gen-auth. DO NOT EDIT.
|
||||||
|
|
||||||
|
package {{.GoPackageName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
)
|
||||||
|
|
||||||
|
var {{.ServiceName}}_AuthMethods = authz.MethodMapping {
|
||||||
|
{{ range $m := .AuthOptions}}
|
||||||
|
{{$.ServiceName}}_{{$m.Name}}_FullMethodName: authz.Option{
|
||||||
|
Permission: "{{$m.Permission}}",
|
||||||
|
CheckParam: "{{$m.CheckFieldName}}",
|
||||||
|
},
|
||||||
|
{{ end}}
|
||||||
|
}
|
97
internal/protoc/protoc-gen-auth/main.go
Normal file
97
internal/protoc/protoc-gen-auth/main.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/compiler/protogen"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/descriptorpb"
|
||||||
|
"google.golang.org/protobuf/types/pluginpb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed auth_method_mapping.go.tmpl
|
||||||
|
authTemplate []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
type authMethods struct {
|
||||||
|
GoPackageName string
|
||||||
|
ProtoPackageName string
|
||||||
|
ServiceName string
|
||||||
|
AuthOptions []authOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type authOption struct {
|
||||||
|
Name string
|
||||||
|
Permission string
|
||||||
|
CheckFieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
input, _ := io.ReadAll(os.Stdin)
|
||||||
|
var req pluginpb.CodeGeneratorRequest
|
||||||
|
err := proto.Unmarshal(input, &req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := protogen.Options{}
|
||||||
|
plugin, err := opts.New(&req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
|
||||||
|
|
||||||
|
authTemp := loadTemplate(authTemplate)
|
||||||
|
|
||||||
|
for _, file := range plugin.Files {
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
var methods authMethods
|
||||||
|
for _, service := range file.Services {
|
||||||
|
methods.ServiceName = service.GoName
|
||||||
|
methods.GoPackageName = string(file.GoPackageName)
|
||||||
|
methods.ProtoPackageName = *file.Proto.Package
|
||||||
|
for _, method := range service.Methods {
|
||||||
|
if options := method.Desc.Options().(*descriptorpb.MethodOptions); options != nil {
|
||||||
|
ext := proto.GetExtension(options, authoption.E_AuthOption).(*authoption.AuthOption)
|
||||||
|
if ext != nil {
|
||||||
|
methods.AuthOptions = append(methods.AuthOptions, authOption{Name: string(method.Desc.Name()), Permission: ext.Permission, CheckFieldName: ext.CheckFieldName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(methods.AuthOptions) > 0 {
|
||||||
|
authTemp.Execute(&buf, &methods)
|
||||||
|
|
||||||
|
filename := file.GeneratedFilenamePrefix + ".pb.authoptions.go"
|
||||||
|
file := plugin.NewGeneratedFile(filename, ".")
|
||||||
|
|
||||||
|
file.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a response from our plugin and marshall as protobuf
|
||||||
|
stdout := plugin.Response()
|
||||||
|
out, err := proto.Marshal(stdout)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the response to stdout, to be picked up by protoc
|
||||||
|
fmt.Fprintf(os.Stdout, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplate(templateData []byte) *template.Template {
|
||||||
|
return template.Must(template.New("").
|
||||||
|
Parse(string(templateData)))
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
# protoc-gen-authoption
|
|
||||||
|
|
||||||
Proto options to annotate auth methods in protos
|
|
||||||
|
|
||||||
## Generate protos/templates
|
|
||||||
protos: `go generate authoption/generate.go`
|
|
||||||
templates/install: `go generate generate.go`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
```
|
|
||||||
// proto file
|
|
||||||
import "authoption/options.proto";
|
|
||||||
|
|
||||||
service MyService {
|
|
||||||
|
|
||||||
rpc Hello(Hello) returns (google.protobuf.Empty) {
|
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/hello"
|
|
||||||
};
|
|
||||||
|
|
||||||
option (caos.zitadel.utils.v1.auth_option) = {
|
|
||||||
zitadel_permission: "hello.read"
|
|
||||||
zitadel_check_param: "id"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
message Hello {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Caos Auth Option is used for granting groups
|
|
||||||
On each zitadel role is specified which auth methods are allowed to call
|
|
||||||
|
|
||||||
Get protoc-get-authoption: ``go get github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption``
|
|
||||||
|
|
||||||
Protc-Flag: ``--authoption_out=.``
|
|
@ -1,4 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
//go:generate go-bindata -pkg main -o templates.gen.go templates
|
|
||||||
//go:generate go install
|
|
@ -1,15 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
base "github.com/zitadel/zitadel/internal/protoc/protoc-base"
|
|
||||||
"github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fileName = "%v.pb.authoptions.go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
base.RegisterExtension(authoption.E_AuthOption)
|
|
||||||
base.RunWithBaseTemplate(fileName, base.LoadTemplate(templatesAuth_method_mappingGoTmplBytes()))
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
// Code generated by protoc-gen-authmethod. DO NOT EDIT.
|
|
||||||
|
|
||||||
package {{.File.GoPkg.Name}}
|
|
||||||
|
|
||||||
|
|
||||||
import (
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
{{ range $s := .File.Services }}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {{$s.Name}}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {{$s.Name}}_MethodPrefix = "{{$.File.Package}}.{{$s.Name}}"
|
|
||||||
|
|
||||||
var {{$s.Name}}_AuthMethods = authz.MethodMapping {
|
|
||||||
{{ range $m := $s.Method}}
|
|
||||||
{{ $mAuthOpt := option $m.Options "zitadel.v1.auth_option" }}
|
|
||||||
{{ if and $mAuthOpt $mAuthOpt.Permission }}
|
|
||||||
"/{{$.File.Package}}.{{$s.Name}}/{{.Name}}": authz.Option{
|
|
||||||
Permission: "{{$mAuthOpt.Permission}}",
|
|
||||||
CheckParam: "{{$mAuthOpt.CheckFieldName}}",
|
|
||||||
},
|
|
||||||
{{end}}
|
|
||||||
{{ end}}
|
|
||||||
}
|
|
||||||
|
|
||||||
{{ end }}
|
|
@ -19,6 +19,7 @@ const (
|
|||||||
HumanEmailVerificationFailedType = emailEventPrefix + "verification.failed"
|
HumanEmailVerificationFailedType = emailEventPrefix + "verification.failed"
|
||||||
HumanEmailCodeAddedType = emailEventPrefix + "code.added"
|
HumanEmailCodeAddedType = emailEventPrefix + "code.added"
|
||||||
HumanEmailCodeSentType = emailEventPrefix + "code.sent"
|
HumanEmailCodeSentType = emailEventPrefix + "code.sent"
|
||||||
|
HumanEmailConfirmURLAddedType = emailEventPrefix + "confirm_url.added"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HumanEmailChangedEvent struct {
|
type HumanEmailChangedEvent struct {
|
||||||
@ -121,8 +122,10 @@ func HumanEmailVerificationFailedEventMapper(event *repository.Event) (eventstor
|
|||||||
type HumanEmailCodeAddedEvent struct {
|
type HumanEmailCodeAddedEvent struct {
|
||||||
eventstore.BaseEvent `json:"-"`
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||||
Expiry time.Duration `json:"expiry,omitempty"`
|
Expiry time.Duration `json:"expiry,omitempty"`
|
||||||
|
URLTemplate string `json:"url_template,omitempty"`
|
||||||
|
CodeReturned bool `json:"code_returned,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *HumanEmailCodeAddedEvent) Data() interface{} {
|
func (e *HumanEmailCodeAddedEvent) Data() interface{} {
|
||||||
@ -137,15 +140,29 @@ func NewHumanEmailCodeAddedEvent(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
code *crypto.CryptoValue,
|
code *crypto.CryptoValue,
|
||||||
expiry time.Duration) *HumanEmailCodeAddedEvent {
|
expiry time.Duration,
|
||||||
|
) *HumanEmailCodeAddedEvent {
|
||||||
|
return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, "", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHumanEmailCodeAddedEventV2(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
code *crypto.CryptoValue,
|
||||||
|
expiry time.Duration,
|
||||||
|
urlTemplate string,
|
||||||
|
codeReturned bool,
|
||||||
|
) *HumanEmailCodeAddedEvent {
|
||||||
return &HumanEmailCodeAddedEvent{
|
return &HumanEmailCodeAddedEvent{
|
||||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
ctx,
|
ctx,
|
||||||
aggregate,
|
aggregate,
|
||||||
HumanEmailCodeAddedType,
|
HumanEmailCodeAddedType,
|
||||||
),
|
),
|
||||||
Code: code,
|
Code: code,
|
||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
|
URLTemplate: urlTemplate,
|
||||||
|
CodeReturned: codeReturned,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: Email wurde nicht geändert
|
NotChanged: Email wurde nicht geändert
|
||||||
Empty: Email ist leer
|
Empty: Email ist leer
|
||||||
IDMissing: Email ID fehlt
|
IDMissing: Email ID fehlt
|
||||||
|
InvalidURLTemplate: URL Template ist ungültig
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: Telefonnummer nicht gefunden
|
NotFound: Telefonnummer nicht gefunden
|
||||||
Invalid: Telefonnummer ist ungültig
|
Invalid: Telefonnummer ist ungültig
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: Email not changed
|
NotChanged: Email not changed
|
||||||
Empty: Email is empty
|
Empty: Email is empty
|
||||||
IDMissing: Email ID is missing
|
IDMissing: Email ID is missing
|
||||||
|
InvalidURLTemplate: URL Template is invalid
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: Phone not found
|
NotFound: Phone not found
|
||||||
Invalid: Phone is invalid
|
Invalid: Phone is invalid
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: El email no ha cambiado
|
NotChanged: El email no ha cambiado
|
||||||
Empty: El email no está vacío
|
Empty: El email no está vacío
|
||||||
IDMissing: Falta el ID del email
|
IDMissing: Falta el ID del email
|
||||||
|
InvalidURLTemplate: La plantilla URL no es válida
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: Teléfono no encontrado
|
NotFound: Teléfono no encontrado
|
||||||
Invalid: El teléfono no es válido
|
Invalid: El teléfono no es válido
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: L'adresse électronique n'a pas changé
|
NotChanged: L'adresse électronique n'a pas changé
|
||||||
Empty: Email est vide
|
Empty: Email est vide
|
||||||
IDMissing: Email ID manquant
|
IDMissing: Email ID manquant
|
||||||
|
InvalidURLTemplate: Le modèle d'URL n'est pas valide
|
||||||
Phone:
|
Phone:
|
||||||
Notfound: Téléphone non trouvé
|
Notfound: Téléphone non trouvé
|
||||||
Invalid: Le téléphone n'est pas valide
|
Invalid: Le téléphone n'est pas valide
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: Email non cambiata
|
NotChanged: Email non cambiata
|
||||||
Empty: Email è vuota
|
Empty: Email è vuota
|
||||||
IDMissing: Email ID mancante
|
IDMissing: Email ID mancante
|
||||||
|
InvalidURLTemplate: Il modello di URL non è valido
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: Telefono non trovato
|
NotFound: Telefono non trovato
|
||||||
Invalid: Il telefono non è valido
|
Invalid: Il telefono non è valido
|
||||||
|
@ -76,6 +76,7 @@ Errors:
|
|||||||
Invalid: 無効なメールアドレスです
|
Invalid: 無効なメールアドレスです
|
||||||
AlreadyVerified: メールアドレスはすでに検証済みです
|
AlreadyVerified: メールアドレスはすでに検証済みです
|
||||||
NotChanged: メールアドレスが変更されていません
|
NotChanged: メールアドレスが変更されていません
|
||||||
|
InvalidURLTemplate: URLテンプレートが無効です
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: 電話番号が見つかりません
|
NotFound: 電話番号が見つかりません
|
||||||
Invalid: 無効な電話番号です
|
Invalid: 無効な電話番号です
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: Adres e-mail nie zmieniony
|
NotChanged: Adres e-mail nie zmieniony
|
||||||
Empty: Adres e-mail jest pusty
|
Empty: Adres e-mail jest pusty
|
||||||
IDMissing: Adres e-mail ID brakuje
|
IDMissing: Adres e-mail ID brakuje
|
||||||
|
InvalidURLTemplate: Szablon URL jest nieprawidłowy
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: Numer telefonu nie znaleziony
|
NotFound: Numer telefonu nie znaleziony
|
||||||
Invalid: Numer telefonu jest nieprawidłowy
|
Invalid: Numer telefonu jest nieprawidłowy
|
||||||
|
@ -81,6 +81,7 @@ Errors:
|
|||||||
NotChanged: 电子邮件未更改
|
NotChanged: 电子邮件未更改
|
||||||
Empty: 电子邮件是空的
|
Empty: 电子邮件是空的
|
||||||
IDMissing: 电子邮件ID丢失
|
IDMissing: 电子邮件ID丢失
|
||||||
|
InvalidURLTemplate: URL模板无效
|
||||||
Phone:
|
Phone:
|
||||||
NotFound: 手机号码未找到
|
NotFound: 手机号码未找到
|
||||||
Invalid: 手机号码无效
|
Invalid: 手机号码无效
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
Exporter *prometheus.Exporter
|
Provider metric.MeterProvider
|
||||||
Meter metric.Meter
|
Meter metric.Meter
|
||||||
Counters sync.Map
|
Counters sync.Map
|
||||||
UpDownSumObserver sync.Map
|
UpDownSumObserver sync.Map
|
||||||
@ -34,12 +34,13 @@ func NewMetrics(meterName string) (metrics.Metrics, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &Metrics{}, err
|
return &Metrics{}, err
|
||||||
}
|
}
|
||||||
|
meterProvider := sdk_metric.NewMeterProvider(
|
||||||
|
sdk_metric.WithReader(exporter),
|
||||||
|
sdk_metric.WithResource(resource),
|
||||||
|
)
|
||||||
return &Metrics{
|
return &Metrics{
|
||||||
Exporter: exporter,
|
Provider: meterProvider,
|
||||||
Meter: sdk_metric.NewMeterProvider(
|
Meter: meterProvider.Meter(meterName),
|
||||||
sdk_metric.WithReader(exporter),
|
|
||||||
sdk_metric.WithResource(resource),
|
|
||||||
).Meter(meterName),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ func (m *Metrics) GetExporter() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metrics) GetMetricsProvider() metric.MeterProvider {
|
func (m *Metrics) GetMetricsProvider() metric.MeterProvider {
|
||||||
return sdk_metric.NewMeterProvider(sdk_metric.WithReader(m.Exporter))
|
return m.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metrics) RegisterCounter(name, description string) error {
|
func (m *Metrics) RegisterCounter(name, description string) error {
|
||||||
|
5
pkg/grpc/user/v2alpha/user.go
Normal file
5
pkg/grpc/user/v2alpha/user.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
func (r *AddHumanUserRequest) AuthContext() string {
|
||||||
|
return r.GetOrganisation().GetOrgId()
|
||||||
|
}
|
@ -2801,7 +2801,7 @@ service AdminService {
|
|||||||
|
|
||||||
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
delete: "/text/message/verifyemail/{language}"
|
delete: "/text/message/passwordreset/{language}"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
|
@ -5588,7 +5588,7 @@ service ManagementService {
|
|||||||
|
|
||||||
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
delete: "/text/message/verifyemail/{language}"
|
delete: "/text/message/passwordreset/{language}"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
|
40
proto/zitadel/object/v2alpha/object.proto
Normal file
40
proto/zitadel/object/v2alpha/object.proto
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.object.v2alpha;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha;object";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message Organisation {
|
||||||
|
oneof org {
|
||||||
|
string org_id = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Details {
|
||||||
|
//sequence represents the order of events. It's always counting
|
||||||
|
//
|
||||||
|
// on read: the sequence of the last event reduced by the projection
|
||||||
|
//
|
||||||
|
// on manipulation: the timestamp of the event(s) added by the manipulation
|
||||||
|
uint64 sequence = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
//change_date is the timestamp when the object was changed
|
||||||
|
//
|
||||||
|
// on read: the timestamp of the last event reduced by the projection
|
||||||
|
//
|
||||||
|
// on manipulation: the timestamp of the event(s) added by the manipulation
|
||||||
|
google.protobuf.Timestamp change_date = 2;
|
||||||
|
//resource_owner is the organization or instance_id an object belongs to
|
||||||
|
string resource_owner = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
44
proto/zitadel/user/v2alpha/email.proto
Normal file
44
proto/zitadel/user/v2alpha/email.proto
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.user.v2alpha;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message SetHumanEmail {
|
||||||
|
string email = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200, email: true},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"mini@mouse.com\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// if no verification is specified, an email is sent with the default url
|
||||||
|
oneof verification {
|
||||||
|
SendEmailVerificationCode send_code = 2;
|
||||||
|
ReturnEmailVerificationCode return_code = 3;
|
||||||
|
bool is_verified = 4 [(validate.rules).bool.const = true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendEmailVerificationCode {
|
||||||
|
optional string url_template = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"";
|
||||||
|
description: "\"Optionally set a url_template, which will be used in the verification mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReturnEmailVerificationCode {}
|
||||||
|
|
53
proto/zitadel/user/v2alpha/password.proto
Normal file
53
proto/zitadel/user/v2alpha/password.proto
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.user.v2alpha;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message SetUserPassword {
|
||||||
|
oneof type {
|
||||||
|
Password password = 1;
|
||||||
|
HashedPassword hashed_password = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Password {
|
||||||
|
string password = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"Secr3tP4ssw0rd!\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
bool change_required = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HashedPassword {
|
||||||
|
string hash = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"$2a$12$lJ08fqVr8bFJilRVnDT9QeULI7YW.nT3iwUv6dyg0aCrfm3UY8XR2\"";
|
||||||
|
description: "\"hashed password\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
string algorithm = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200, const: "bcrypt"},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"bcrypt\"";
|
||||||
|
description: "\"algorithm used for the hash. currently only bcrypt is supported\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
bool change_required = 3;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user