mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat: user v2alpha email API (#5708)
* chore(proto): update versions
* change protoc plugin
* some cleanups
* define api for setting emails in new api
* implement user.SetEmail
* move SetEmail buisiness logic into command
* resuse newCryptoCode
* command: add ChangeEmail unit tests
Not complete, was not able to mock the generator.
* Revert "resuse newCryptoCode"
This reverts commit c89e90ae35
.
* undo change to crypto code generators
* command: use a generator so we can test properly
* command: reorganise ChangeEmail
improve test coverage
* implement VerifyEmail
including unit tests
* add URL template tests
* proto: change context to object
* remove old auth option
* remove old auth option
* fix linting errors
run gci on modified files
* add permission checks and fix some errors
* comments
* comments
---------
Co-authored-by: Livio Spring <livio.a@gmail.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
parent
2a79e77c7b
commit
095ec21678
@ -16,11 +16,11 @@ ENV PROTOC_ARCH x86_64
|
||||
## protoc and protoc-gen-grpc-web for later use
|
||||
#######################
|
||||
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 GRPC_WEB_VERSION=1.3.0
|
||||
ARG GATEWAY_VERSION=2.15.1
|
||||
ARG VALIDATOR_VERSION=0.6.2
|
||||
ARG GATEWAY_VERSION=2.15.2
|
||||
ARG VALIDATOR_VERSION=0.10.1
|
||||
# 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
|
||||
|
||||
|
@ -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/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/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/docs/apis/proto docs/docs/apis/proto
|
||||
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/ \
|
||||
--go_out $GOPATH/src \
|
||||
--go-grpc_out $GOPATH/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
$(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
|
||||
go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption
|
||||
go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-auth
|
||||
|
||||
# output folder for openapi v2
|
||||
mkdir -p ${OPENAPI_PATH}
|
||||
@ -39,28 +33,20 @@ protoc \
|
||||
--grpc-gateway_opt logtostderr=true \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--authoption_out ${GRPC_PATH}/system \
|
||||
--auth_out ${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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 \
|
||||
-I=/proto/include \
|
||||
--grpc-gateway_out ${GOPATH}/src \
|
||||
--grpc-gateway_opt logtostderr=true \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--authoption_out ${GRPC_PATH}/admin \
|
||||
--auth_out ${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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 \
|
||||
-I=/proto/include \
|
||||
--grpc-gateway_out ${GOPATH}/src \
|
||||
@ -69,14 +55,10 @@ protoc \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--openapiv2_opt allow_delete_body=true \
|
||||
--authoption_out ${GRPC_PATH}/management \
|
||||
--auth_out ${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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 \
|
||||
-I=/proto/include \
|
||||
--grpc-gateway_out ${GOPATH}/src \
|
||||
@ -85,14 +67,10 @@ protoc \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--openapiv2_opt allow_delete_body=true \
|
||||
--authoption_out=${GRPC_PATH}/auth \
|
||||
--auth_out=${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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 \
|
||||
-I=/proto/include \
|
||||
--grpc-gateway_out ${GOPATH}/src \
|
||||
@ -101,14 +79,10 @@ protoc \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--openapiv2_opt allow_delete_body=true \
|
||||
--authoption_out=${GRPC_PATH}/user \
|
||||
--auth_out=${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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 \
|
||||
-I=/proto/include \
|
||||
--grpc-gateway_out ${GOPATH}/src \
|
||||
@ -117,12 +91,8 @@ protoc \
|
||||
--openapiv2_out ${OPENAPI_PATH} \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--openapiv2_opt allow_delete_body=true \
|
||||
--authoption_out=${GRPC_PATH}/session \
|
||||
--auth_out=${GOPATH}/src \
|
||||
--validate_out=lang=go:${GOPATH}/src \
|
||||
${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"
|
||||
|
@ -76,6 +76,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -33,7 +33,8 @@ func (mig *externalConfigChange) Check() bool {
|
||||
}
|
||||
|
||||
func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||
cmd, err := command.StartCommands(mig.es,
|
||||
cmd, err := command.StartCommands(
|
||||
mig.es,
|
||||
systemdefaults.SystemDefaults{},
|
||||
nil,
|
||||
nil,
|
||||
@ -50,6 +51,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -146,6 +146,7 @@ func startZitadel(config *Config, masterKey string) error {
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
&http.Client{},
|
||||
authZRepo,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
@ -248,7 +249,7 @@ func startAPIs(
|
||||
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
|
||||
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
|
||||
}
|
||||
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
|
||||
|
@ -14,11 +14,11 @@ const (
|
||||
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)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, verifier, method)
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -29,7 +29,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
||||
}, 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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -110,18 +110,6 @@ func HasGlobalPermission(perms []string) bool {
|
||||
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 {
|
||||
ctxIDs := make([]string, 0)
|
||||
for _, perm := range perms {
|
||||
@ -132,16 +120,3 @@ func GetAllPermissionCtxIDs(perms []string) []string {
|
||||
}
|
||||
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 {
|
||||
Permission string
|
||||
CheckParam string
|
||||
Feature string
|
||||
AllowSelf bool
|
||||
}
|
||||
|
||||
func (a *Config) getPermissionsFromRole(role string) []string {
|
||||
for _, roleMap := range a.RolePermissionMappings {
|
||||
func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string {
|
||||
for _, roleMap := range rolePermissionMappings {
|
||||
if roleMap.Role == role {
|
||||
return roleMap.Permissions
|
||||
}
|
||||
|
@ -7,7 +7,28 @@ import (
|
||||
"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)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@ -16,13 +37,13 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, dataKey, ctxData)
|
||||
memberships, err := t.SearchMyMemberships(ctx)
|
||||
memberships, err := resolver.SearchMyMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(memberships) == 0 {
|
||||
err = retry(func() error {
|
||||
memberships, err = t.SearchMyMemberships(ctx)
|
||||
memberships, err = resolver.SearchMyMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -35,24 +56,56 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, authConfig)
|
||||
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings)
|
||||
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)
|
||||
allPermissions = make([]string, 0)
|
||||
for _, membership := range memberships {
|
||||
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, authConfig, requestPermissions, allPermissions)
|
||||
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, 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)
|
||||
for _, roleName := range roleNames {
|
||||
perms := authConfig.getPermissionsFromRole(roleName)
|
||||
perms := getPermissionsFromRole(roleMappings, roleName)
|
||||
|
||||
for _, p := range perms {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ func equalStringArray(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
func Test_GetUserPermissions(t *testing.T) {
|
||||
type args struct {
|
||||
ctxData CtxData
|
||||
verifier *TokenVerifier
|
||||
@ -139,7 +139,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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 {
|
||||
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 {
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
type MembershipsResolver interface {
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||
}
|
||||
|
||||
type authZRepo interface {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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) {
|
||||
|
@ -71,7 +71,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return admin.AdminService_MethodPrefix
|
||||
return admin.AdminService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
@ -69,7 +69,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return auth.AuthService_MethodPrefix
|
||||
return auth.AuthService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
@ -63,7 +63,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return management.ManagementService_MethodPrefix
|
||||
return management.ManagementService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
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
|
||||
}
|
@ -24,7 +24,7 @@ type verifierMock struct{}
|
||||
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -50,13 +50,13 @@ func CreateServer(
|
||||
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
|
||||
middleware.NoCacheInterceptor(),
|
||||
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.AuthorizationInterceptor(verifier, authConfig),
|
||||
middleware.TranslationHandler(),
|
||||
middleware.ValidationHandler(),
|
||||
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"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/admin/repository"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
@ -60,7 +59,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return system.SystemService_MethodPrefix
|
||||
return system.SystemService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
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) {
|
||||
resourceOwner := "" // 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/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"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)
|
||||
|
||||
type Server struct {
|
||||
user.UnimplementedUserServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
command: command,
|
||||
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
|
||||
}
|
||||
}
|
@ -12,17 +12,17 @@ type UserMembershipRepo struct {
|
||||
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)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
memberships, err := repo.searchUserMemberships(ctx)
|
||||
memberships, err := repo.searchUserMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
@ -30,11 +30,11 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(ctxData.OrgID)
|
||||
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -7,5 +7,5 @@ import (
|
||||
)
|
||||
|
||||
type UserMembershipRepository interface {
|
||||
SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error)
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error)
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ import (
|
||||
type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
checkPermission permissionCheck
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
static static.Storage
|
||||
idGenerator id.Generator
|
||||
@ -59,7 +61,8 @@ type Commands struct {
|
||||
certificateLifetime time.Duration
|
||||
}
|
||||
|
||||
func StartCommands(es *eventstore.Eventstore,
|
||||
func StartCommands(
|
||||
es *eventstore.Eventstore,
|
||||
defaults sd.SystemDefaults,
|
||||
zitadelRoles []authz.RoleMapping,
|
||||
staticStore static.Storage,
|
||||
@ -76,6 +79,7 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
oidcEncryption,
|
||||
samlEncryption crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
membershipsResolver authz.MembershipsResolver,
|
||||
) (repo *Commands, err error) {
|
||||
if externalDomain == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||
@ -102,6 +106,9 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
certificateAlgorithm: samlEncryption,
|
||||
webauthnConfig: webAuthN,
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
@ -248,3 +249,15 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
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
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"
|
||||
)
|
@ -507,7 +507,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
|
||||
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
|
||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
} else {
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
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 {
|
||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
} else {
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
|
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
@ -1,12 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
@ -35,6 +38,8 @@ type Email struct {
|
||||
|
||||
EmailAddress EmailAddress
|
||||
IsEmailVerified bool
|
||||
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||
PlainCode *string
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
@ -51,13 +56,36 @@ func (e *Email) Validate() error {
|
||||
return e.EmailAddress.Validate()
|
||||
}
|
||||
|
||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) {
|
||||
emailCodeCrypto, _, err := crypto.NewCode(emailGenerator)
|
||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, string, error) {
|
||||
emailCodeCrypto, code, err := crypto.NewCode(emailGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return &EmailCode{
|
||||
Code: emailCodeCrypto,
|
||||
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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +182,10 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
||||
if !ok {
|
||||
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())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||
@ -232,7 +236,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
||||
e,
|
||||
u.metricSuccessfulDeliveriesEmail,
|
||||
u.metricFailedDeliveriesEmail,
|
||||
).SendEmailVerificationCode(notifyUser, origin, code)
|
||||
).SendEmailVerificationCode(notifyUser, origin, code, e.URLTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,13 +1,25 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error {
|
||||
url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner)
|
||||
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string, urlTmpl string) error {
|
||||
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["Code"] = code
|
||||
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"
|
||||
HumanEmailCodeAddedType = emailEventPrefix + "code.added"
|
||||
HumanEmailCodeSentType = emailEventPrefix + "code.sent"
|
||||
HumanEmailConfirmURLAddedType = emailEventPrefix + "confirm_url.added"
|
||||
)
|
||||
|
||||
type HumanEmailChangedEvent struct {
|
||||
@ -121,8 +122,10 @@ func HumanEmailVerificationFailedEventMapper(event *repository.Event) (eventstor
|
||||
type HumanEmailCodeAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
URLTemplate string `json:"url_template,omitempty"`
|
||||
CodeReturned bool `json:"code_returned,omitempty"`
|
||||
}
|
||||
|
||||
func (e *HumanEmailCodeAddedEvent) Data() interface{} {
|
||||
@ -137,15 +140,29 @@ func NewHumanEmailCodeAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
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{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanEmailCodeAddedType,
|
||||
),
|
||||
Code: code,
|
||||
Expiry: expiry,
|
||||
Code: code,
|
||||
Expiry: expiry,
|
||||
URLTemplate: urlTemplate,
|
||||
CodeReturned: codeReturned,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: Email wurde nicht geändert
|
||||
Empty: Email ist leer
|
||||
IDMissing: Email ID fehlt
|
||||
InvalidURLTemplate: URL Template ist ungültig
|
||||
Phone:
|
||||
NotFound: Telefonnummer nicht gefunden
|
||||
Invalid: Telefonnummer ist ungültig
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: Email not changed
|
||||
Empty: Email is empty
|
||||
IDMissing: Email ID is missing
|
||||
InvalidURLTemplate: URL Template is invalid
|
||||
Phone:
|
||||
NotFound: Phone not found
|
||||
Invalid: Phone is invalid
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: El email no ha cambiado
|
||||
Empty: El email no está vacío
|
||||
IDMissing: Falta el ID del email
|
||||
InvalidURLTemplate: La plantilla URL no es válida
|
||||
Phone:
|
||||
NotFound: Teléfono no encontrado
|
||||
Invalid: El teléfono no es válido
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: L'adresse électronique n'a pas changé
|
||||
Empty: Email est vide
|
||||
IDMissing: Email ID manquant
|
||||
InvalidURLTemplate: Le modèle d'URL n'est pas valide
|
||||
Phone:
|
||||
Notfound: Téléphone non trouvé
|
||||
Invalid: Le téléphone n'est pas valide
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: Email non cambiata
|
||||
Empty: Email è vuota
|
||||
IDMissing: Email ID mancante
|
||||
InvalidURLTemplate: Il modello di URL non è valido
|
||||
Phone:
|
||||
NotFound: Telefono non trovato
|
||||
Invalid: Il telefono non è valido
|
||||
|
@ -76,6 +76,7 @@ Errors:
|
||||
Invalid: 無効なメールアドレスです
|
||||
AlreadyVerified: メールアドレスはすでに検証済みです
|
||||
NotChanged: メールアドレスが変更されていません
|
||||
InvalidURLTemplate: URLテンプレートが無効です
|
||||
Phone:
|
||||
NotFound: 電話番号が見つかりません
|
||||
Invalid: 無効な電話番号です
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: Adres e-mail nie zmieniony
|
||||
Empty: Adres e-mail jest pusty
|
||||
IDMissing: Adres e-mail ID brakuje
|
||||
InvalidURLTemplate: Szablon URL jest nieprawidłowy
|
||||
Phone:
|
||||
NotFound: Numer telefonu nie znaleziony
|
||||
Invalid: Numer telefonu jest nieprawidłowy
|
||||
|
@ -81,6 +81,7 @@ Errors:
|
||||
NotChanged: 电子邮件未更改
|
||||
Empty: 电子邮件是空的
|
||||
IDMissing: 电子邮件ID丢失
|
||||
InvalidURLTemplate: URL模板无效
|
||||
Phone:
|
||||
NotFound: 手机号码未找到
|
||||
Invalid: 手机号码无效
|
||||
|
@ -2801,7 +2801,7 @@ service AdminService {
|
||||
|
||||
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/text/message/verifyemail/{language}"
|
||||
delete: "/text/message/passwordreset/{language}"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
|
@ -5588,7 +5588,7 @@ service ManagementService {
|
||||
|
||||
rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/text/message/verifyemail/{language}"
|
||||
delete: "/text/message/passwordreset/{language}"
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
message OrgContext {
|
||||
oneof ctx {
|
||||
string org_id = 1;
|
||||
string org_domain = 2;
|
||||
}
|
||||
}
|
||||
|
||||
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\"";
|
||||
}
|
||||
];
|
||||
}
|
27
proto/zitadel/user/v2alpha/email.proto
Normal file
27
proto/zitadel/user/v2alpha/email.proto
Normal file
@ -0,0 +1,27 @@
|
||||
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 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 {}
|
||||
|
@ -3,39 +3,32 @@ syntax = "proto3";
|
||||
package zitadel.user.v2alpha;
|
||||
|
||||
import "zitadel/options.proto";
|
||||
import "zitadel/user/v2alpha/user.proto";
|
||||
import "zitadel/object/v2alpha/object.proto";
|
||||
import "zitadel/user/v2alpha/email.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||
|
||||
service UserService {
|
||||
|
||||
// TestGet simply demonstrates how the context (org, instance) could be handled in a GET request //
|
||||
//
|
||||
// this request is subject to change and currently used for demonstration only
|
||||
rpc TestGet (TestGetRequest) returns (TestGetResponse) {
|
||||
rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2alpha/users/test"
|
||||
};
|
||||
}
|
||||
|
||||
// TestPOST simply demonstrates how the context (org, instance) could be handled in a POST request
|
||||
//
|
||||
// this request is subject to change and currently used for demonstration only
|
||||
rpc TestPost (TestPostRequest) returns (TestPostResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2alpha/users/test"
|
||||
post: "/v2alpha/users/{user_id}/email"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
}
|
||||
|
||||
// TestAuth demonstrates how the context (org, instance) could be handled in combination of the authorized context
|
||||
//
|
||||
// this request is subject to change and currently used for demonstration only
|
||||
rpc TestAuth (TestAuthRequest) returns (TestAuthResponse) {
|
||||
rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2alpha/users/test_auth"
|
||||
post: "/v2alpha/users/{user_id}/email/_verify"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
@ -44,35 +37,61 @@ service UserService {
|
||||
}
|
||||
}
|
||||
|
||||
message TestGetRequest{
|
||||
Context ctx = 1;
|
||||
}
|
||||
|
||||
message TestGetResponse{
|
||||
string ctx = 1;
|
||||
}
|
||||
|
||||
message TestPostRequest{
|
||||
Context ctx = 1;
|
||||
}
|
||||
|
||||
message TestPostResponse{
|
||||
string ctx = 1;
|
||||
}
|
||||
|
||||
message TestAuthRequest{
|
||||
Context ctx = 1;
|
||||
}
|
||||
|
||||
message TestAuthResponse{
|
||||
User user = 1;
|
||||
Context ctx = 2;
|
||||
}
|
||||
|
||||
message Context {
|
||||
oneof ctx {
|
||||
bool instance = 1;
|
||||
string org_id = 2;
|
||||
string org_domain = 3;
|
||||
message SetEmailRequest{
|
||||
string user_id = 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: "\"69629026806489455\"";
|
||||
}
|
||||
];
|
||||
string email = 2 [
|
||||
(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 = 3;
|
||||
ReturnEmailVerificationCode return_code = 4;
|
||||
bool is_verified = 5 [(validate.rules).bool.const = true];
|
||||
}
|
||||
}
|
||||
|
||||
message SetEmailResponse{
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
// in case the verification was set to return_code, the code will be returned
|
||||
optional string verification_code = 2;
|
||||
}
|
||||
|
||||
message VerifyEmailRequest{
|
||||
string user_id = 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: "\"69629026806489455\"";
|
||||
}
|
||||
];
|
||||
string verification_code = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 20},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 20;
|
||||
example: "\"SKJd342k\"";
|
||||
description: "\"the verification code generated during the set email request\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message VerifyEmailResponse{
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
33
tools/go.mod
33
tools/go.mod
@ -3,17 +3,28 @@ module github.com/zitadel/zitadel/tools
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.1
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.1
|
||||
github.com/go-bindata/go-bindata/v3 v3.1.3
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0
|
||||
github.com/iancoleman/strcase v0.1.3 // indirect
|
||||
github.com/kisielk/errcheck v1.5.0 // indirect
|
||||
github.com/lyft/protoc-gen-star v0.5.2 // indirect
|
||||
github.com/pseudomuto/protoc-gen-doc v1.4.1
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.1.1 // indirect
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/kisielk/errcheck v1.6.3 // indirect
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.3 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mwitkow/go-proto-validators v0.3.2 // indirect
|
||||
github.com/pseudomuto/protoc-gen-doc v1.5.1
|
||||
github.com/pseudomuto/protokit v0.2.1 // indirect
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/spf13/afero v1.5.1 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
|
||||
google.golang.org/protobuf v1.26.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
)
|
||||
|
1260
tools/go.sum
1260
tools/go.sum
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user