mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 15:35:10 +00:00
feat(v3alpha): web key resource (#8262)
# Which Problems Are Solved Implement a new API service that allows management of OIDC signing web keys. This allows users to manage rotation of the instance level keys. which are currently managed based on expiry. The API accepts the generation of the following key types and parameters: - RSA keys with 2048, 3072 or 4096 bit in size and: - Signing with SHA-256 (RS256) - Signing with SHA-384 (RS384) - Signing with SHA-512 (RS512) - ECDSA keys with - P256 curve - P384 curve - P512 curve - ED25519 keys # How the Problems Are Solved Keys are serialized for storage using the JSON web key format from the `jose` library. This is the format that will be used by OIDC for signing, verification and publication. Each instance can have a number of key pairs. All existing public keys are meant to be used for token verification and publication the keys endpoint. Keys can be activated and the active private key is meant to sign new tokens. There is always exactly 1 active signing key: 1. When the first key for an instance is generated, it is automatically activated. 2. Activation of the next key automatically deactivates the previously active key. 3. Keys cannot be manually deactivated from the API 4. Active keys cannot be deleted # Additional Changes - Query methods that later will be used by the OIDC package are already implemented. Preparation for #8031 - Fix indentation in french translation for instance event - Move user_schema translations to consistent positions in all translation files # Additional Context - Closes #8030 - Part of #7809 --------- Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
parent
e2e1100124
commit
64a3bb3149
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
||||
package initialise
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
@ -17,7 +18,10 @@ type Config struct {
|
||||
func MustNewConfig(v *viper.Viper) *Config {
|
||||
config := new(Config)
|
||||
err := v.Unmarshal(config,
|
||||
viper.DecodeHook(database.DecodeHook),
|
||||
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
||||
database.DecodeHook,
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read config")
|
||||
|
||||
|
@ -74,6 +74,7 @@ func mustNewConfig(v *viper.Viper, config any) {
|
||||
database.DecodeHook,
|
||||
actions.HTTPConfigDecodeHook,
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read default config")
|
||||
|
@ -27,6 +27,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read default config")
|
||||
|
@ -74,6 +74,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read default config")
|
||||
@ -139,6 +140,7 @@ func MustNewSteps(v *viper.Viper) *Steps {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read steps")
|
||||
|
@ -100,6 +100,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read config")
|
||||
|
@ -44,6 +44,7 @@ import (
|
||||
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
|
||||
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
|
||||
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
|
||||
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||
@ -442,6 +443,9 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
|
||||
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
||||
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
||||
|
@ -340,6 +340,14 @@ module.exports = {
|
||||
categoryLinkSource: "auto",
|
||||
},
|
||||
},
|
||||
webkey_v3: {
|
||||
specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json",
|
||||
outputDir: "docs/apis/resources/webkey_service_v3",
|
||||
sidebarOptions: {
|
||||
groupPathsBy: "tag",
|
||||
categoryLinkSource: "auto",
|
||||
},
|
||||
},
|
||||
feature_v2: {
|
||||
specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json",
|
||||
outputDir: "docs/apis/resources/feature_service_v2",
|
||||
|
@ -732,6 +732,20 @@ module.exports = {
|
||||
},
|
||||
items: require("./docs/apis/resources/action_service_v3/sidebar.ts"),
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Web key Lifecycle (Preview)",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Action Service API (Preview)",
|
||||
slug: "/apis/resources/action_service_v3",
|
||||
description:
|
||||
"This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" +
|
||||
"\n" +
|
||||
"This project is in preview state. It can AND will continue breaking until a stable version is released.",
|
||||
},
|
||||
items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
|
||||
TokenExchange: req.OidcTokenExchange,
|
||||
Actions: req.Actions,
|
||||
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
||||
WebKey: req.WebKey,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
|
||||
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
||||
Actions: featureSourceToFlagPb(&f.Actions),
|
||||
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
||||
WebKey: featureSourceToFlagPb(&f.WebKey),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
OidcTokenExchange: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
WebKey: gu.Ptr(true),
|
||||
}
|
||||
want := &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
TokenExchange: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
WebKey: gu.Ptr(true),
|
||||
}
|
||||
got := instanceFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
Level: feature.LevelSystem,
|
||||
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
|
||||
},
|
||||
WebKey: query.FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
WebKey: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
}
|
||||
got := instanceFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
|
@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
|
||||
TokenExchange: req.OidcTokenExchange,
|
||||
Actions: req.Actions,
|
||||
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
||||
WebKey: req.WebKey,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
|
||||
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
||||
Actions: featureSourceToFlagPb(&f.Actions),
|
||||
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
||||
WebKey: featureSourceToFlagPb(&f.WebKey),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
OidcTokenExchange: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
WebKey: gu.Ptr(true),
|
||||
}
|
||||
want := &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
TokenExchange: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
WebKey: gu.Ptr(true),
|
||||
}
|
||||
got := instanceFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
Level: feature.LevelSystem,
|
||||
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
|
||||
},
|
||||
WebKey: query.FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
WebKey: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
}
|
||||
got := instanceFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
|
@ -188,8 +188,8 @@ func TestServer_SetExecution_Request(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -326,8 +326,8 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -506,8 +506,8 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -692,8 +692,8 @@ func TestServer_SetExecution_Event(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -791,8 +791,8 @@ func TestServer_SetExecution_Function(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
|
||||
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
@ -248,7 +248,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
defer close()
|
||||
}
|
||||
|
||||
got, err := Tester.Client.ActionV3.GetTarget(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
@ -214,7 +214,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, getErr := Tester.Client.ActionV3.GetTarget(tt.args.ctx, tt.args.req)
|
||||
got, getErr := Tester.Client.ActionV3Alpha.GetTarget(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, getErr, "Error: "+getErr.Error())
|
||||
} else {
|
||||
@ -476,7 +476,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
}
|
||||
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, listErr := Tester.Client.ActionV3.SearchTargets(tt.args.ctx, tt.args.req)
|
||||
got, listErr := Tester.Client.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(ttt, listErr, "Error: "+listErr.Error())
|
||||
} else {
|
||||
@ -864,7 +864,7 @@ func TestServer_SearchExecutions(t *testing.T) {
|
||||
}
|
||||
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, listErr := Tester.Client.ActionV3.SearchExecutions(tt.args.ctx, tt.args.req)
|
||||
got, listErr := Tester.Client.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, listErr, "Error: "+listErr.Error())
|
||||
} else {
|
||||
|
@ -197,7 +197,7 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Tester.Client.ActionV3.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
|
||||
got, err := Tester.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -382,8 +382,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
err := tt.prepare(tt.args.req)
|
||||
require.NoError(t, err)
|
||||
// We want to have the same response no matter how often we call the function
|
||||
Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
|
||||
got, err := Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
|
||||
Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@ -438,7 +438,7 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Tester.Client.ActionV3.DeleteTarget(tt.ctx, tt.req)
|
||||
got, err := Tester.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
47
internal/api/grpc/resources/webkey/v3/server.go
Normal file
47
internal/api/grpc/resources/webkey/v3/server.go
Normal file
@ -0,0 +1,47 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"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/query"
|
||||
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
webkey.UnimplementedZITADELWebKeysServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
webkey.RegisterZITADELWebKeysServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return webkey.ZITADELWebKeys_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return webkey.RegisterZITADELWebKeysHandler
|
||||
}
|
87
internal/api/grpc/resources/webkey/v3/webkey.go
Normal file
87
internal/api/grpc/resources/webkey/v3/webkey.go
Normal file
@ -0,0 +1,87 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
)
|
||||
|
||||
func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err = checkWebKeyFeature(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webkey.CreateWebKeyResponse{
|
||||
Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyRequest) (_ *webkey.ActivateWebKeyResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err = checkWebKeyFeature(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details, err := s.command.ActivateWebKey(ctx, req.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webkey.ActivateWebKeyResponse{
|
||||
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyRequest) (_ *webkey.DeleteWebKeyResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err = checkWebKeyFeature(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details, err := s.command.DeleteWebKey(ctx, req.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webkey.DeleteWebKeyResponse{
|
||||
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) (_ *webkey.ListWebKeysResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err = checkWebKeyFeature(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := s.query.ListWebKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webkey.ListWebKeysResponse{
|
||||
WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func checkWebKeyFeature(ctx context.Context) error {
|
||||
if !authz.GetFeatures(ctx).WebKey {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "WEBKEY-Ohx6E", "Errors.WebKey.FeatureDisabled")
|
||||
}
|
||||
return nil
|
||||
}
|
173
internal/api/grpc/resources/webkey/v3/webkey_converter.go
Normal file
173
internal/api/grpc/resources/webkey/v3/webkey_converter.go
Normal file
@ -0,0 +1,173 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
)
|
||||
|
||||
func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
|
||||
switch config := req.GetKey().GetConfig().(type) {
|
||||
case *webkey.WebKey_Rsa:
|
||||
return webKeyRSAConfigToCrypto(config.Rsa)
|
||||
case *webkey.WebKey_Ecdsa:
|
||||
return webKeyECDSAConfigToCrypto(config.Ecdsa)
|
||||
case *webkey.WebKey_Ed25519:
|
||||
return new(crypto.WebKeyED25519Config)
|
||||
default:
|
||||
return webKeyRSAConfigToCrypto(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig {
|
||||
out := new(crypto.WebKeyRSAConfig)
|
||||
|
||||
switch config.GetBits() {
|
||||
case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED:
|
||||
out.Bits = crypto.RSABits2048
|
||||
case webkey.WebKeyRSAConfig_RSA_BITS_2048:
|
||||
out.Bits = crypto.RSABits2048
|
||||
case webkey.WebKeyRSAConfig_RSA_BITS_3072:
|
||||
out.Bits = crypto.RSABits3072
|
||||
case webkey.WebKeyRSAConfig_RSA_BITS_4096:
|
||||
out.Bits = crypto.RSABits4096
|
||||
default:
|
||||
out.Bits = crypto.RSABits2048
|
||||
}
|
||||
|
||||
switch config.GetHasher() {
|
||||
case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED:
|
||||
out.Hasher = crypto.RSAHasherSHA256
|
||||
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256:
|
||||
out.Hasher = crypto.RSAHasherSHA256
|
||||
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384:
|
||||
out.Hasher = crypto.RSAHasherSHA384
|
||||
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512:
|
||||
out.Hasher = crypto.RSAHasherSHA512
|
||||
default:
|
||||
out.Hasher = crypto.RSAHasherSHA256
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig {
|
||||
out := new(crypto.WebKeyECDSAConfig)
|
||||
|
||||
switch config.GetCurve() {
|
||||
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED:
|
||||
out.Curve = crypto.EllipticCurveP256
|
||||
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256:
|
||||
out.Curve = crypto.EllipticCurveP256
|
||||
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384:
|
||||
out.Curve = crypto.EllipticCurveP384
|
||||
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512:
|
||||
out.Curve = crypto.EllipticCurveP512
|
||||
default:
|
||||
out.Curve = crypto.EllipticCurveP256
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey {
|
||||
out := make([]*webkey.GetWebKey, len(list))
|
||||
for i := range list {
|
||||
out[i] = webKeyDetailsToPb(&list[i], instanceID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey {
|
||||
out := &webkey.GetWebKey{
|
||||
Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{
|
||||
ID: details.KeyID,
|
||||
CreationDate: details.CreationDate,
|
||||
EventDate: details.ChangeDate,
|
||||
}, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
|
||||
State: webKeyStateToPb(details.State),
|
||||
Config: &webkey.WebKey{},
|
||||
}
|
||||
|
||||
switch config := details.Config.(type) {
|
||||
case *crypto.WebKeyRSAConfig:
|
||||
out.Config.Config = &webkey.WebKey_Rsa{
|
||||
Rsa: webKeyRSAConfigToPb(config),
|
||||
}
|
||||
case *crypto.WebKeyECDSAConfig:
|
||||
out.Config.Config = &webkey.WebKey_Ecdsa{
|
||||
Ecdsa: webKeyECDSAConfigToPb(config),
|
||||
}
|
||||
case *crypto.WebKeyED25519Config:
|
||||
out.Config.Config = &webkey.WebKey_Ed25519{
|
||||
Ed25519: new(webkey.WebKeyED25519Config),
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState {
|
||||
switch state {
|
||||
case domain.WebKeyStateUnspecified:
|
||||
return webkey.WebKeyState_STATE_UNSPECIFIED
|
||||
case domain.WebKeyStateInitial:
|
||||
return webkey.WebKeyState_STATE_INITIAL
|
||||
case domain.WebKeyStateActive:
|
||||
return webkey.WebKeyState_STATE_ACTIVE
|
||||
case domain.WebKeyStateInactive:
|
||||
return webkey.WebKeyState_STATE_INACTIVE
|
||||
case domain.WebKeyStateRemoved:
|
||||
return webkey.WebKeyState_STATE_REMOVED
|
||||
default:
|
||||
return webkey.WebKeyState_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig {
|
||||
out := new(webkey.WebKeyRSAConfig)
|
||||
|
||||
switch config.Bits {
|
||||
case crypto.RSABitsUnspecified:
|
||||
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED
|
||||
case crypto.RSABits2048:
|
||||
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048
|
||||
case crypto.RSABits3072:
|
||||
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072
|
||||
case crypto.RSABits4096:
|
||||
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096
|
||||
}
|
||||
|
||||
switch config.Hasher {
|
||||
case crypto.RSAHasherUnspecified:
|
||||
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED
|
||||
case crypto.RSAHasherSHA256:
|
||||
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256
|
||||
case crypto.RSAHasherSHA384:
|
||||
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384
|
||||
case crypto.RSAHasherSHA512:
|
||||
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig {
|
||||
out := new(webkey.WebKeyECDSAConfig)
|
||||
|
||||
switch config.Curve {
|
||||
case crypto.EllipticCurveUnspecified:
|
||||
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED
|
||||
case crypto.EllipticCurveP256:
|
||||
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256
|
||||
case crypto.EllipticCurveP384:
|
||||
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384
|
||||
case crypto.EllipticCurveP512:
|
||||
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
529
internal/api/grpc/resources/webkey/v3/webkey_converter_test.go
Normal file
529
internal/api/grpc/resources/webkey/v3/webkey_converter_test.go
Normal file
@ -0,0 +1,529 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
)
|
||||
|
||||
func Test_createWebKeyRequestToConfig(t *testing.T) {
|
||||
type args struct {
|
||||
req *webkey.CreateWebKeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want crypto.WebKeyConfig
|
||||
}{
|
||||
{
|
||||
name: "RSA",
|
||||
args: args{&webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits3072,
|
||||
Hasher: crypto.RSAHasherSHA384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ECDSA",
|
||||
args: args{&webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Ecdsa{
|
||||
Ecdsa: &webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ED25519",
|
||||
args: args{&webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Ed25519{
|
||||
Ed25519: &webkey.WebKeyED25519Config{},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: &crypto.WebKeyED25519Config{},
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
args: args{&webkey.CreateWebKeyRequest{}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := createWebKeyRequestToConfig(tt.args.req)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyRSAConfigToCrypto(t *testing.T) {
|
||||
type args struct {
|
||||
config *webkey.WebKeyRSAConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *crypto.WebKeyRSAConfig
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
args: args{&webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED,
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2048, RSA256",
|
||||
args: args{&webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3072, RSA384",
|
||||
args: args{&webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits3072,
|
||||
Hasher: crypto.RSAHasherSHA384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "4096, RSA512",
|
||||
args: args{&webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits4096,
|
||||
Hasher: crypto.RSAHasherSHA512,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{&webkey.WebKeyRSAConfig{
|
||||
Bits: 99,
|
||||
Hasher: 99,
|
||||
}},
|
||||
want: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyRSAConfigToCrypto(tt.args.config)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
|
||||
type args struct {
|
||||
config *webkey.WebKeyECDSAConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *crypto.WebKeyECDSAConfig
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
args: args{&webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED,
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "P256",
|
||||
args: args{&webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "P384",
|
||||
args: args{&webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "P512",
|
||||
args: args{&webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP512,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{&webkey.WebKeyECDSAConfig{
|
||||
Curve: 99,
|
||||
}},
|
||||
want: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP256,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyECDSAConfigToCrypto(tt.args.config)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyDetailsListToPb(t *testing.T) {
|
||||
instanceID := "ownerid"
|
||||
list := []query.WebKeyDetails{
|
||||
{
|
||||
KeyID: "key1",
|
||||
CreationDate: time.Unix(123, 456),
|
||||
ChangeDate: time.Unix(789, 0),
|
||||
Sequence: 123,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits3072,
|
||||
Hasher: crypto.RSAHasherSHA384,
|
||||
},
|
||||
},
|
||||
{
|
||||
KeyID: "key2",
|
||||
CreationDate: time.Unix(123, 456),
|
||||
ChangeDate: time.Unix(789, 0),
|
||||
Sequence: 123,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: &crypto.WebKeyED25519Config{},
|
||||
},
|
||||
}
|
||||
want := []*webkey.GetWebKey{
|
||||
{
|
||||
Details: &resource_object.Details{
|
||||
Id: "key1",
|
||||
Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456},
|
||||
Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0},
|
||||
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
|
||||
},
|
||||
State: webkey.WebKeyState_STATE_ACTIVE,
|
||||
Config: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Details: &resource_object.Details{
|
||||
Id: "key2",
|
||||
Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456},
|
||||
Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0},
|
||||
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
|
||||
},
|
||||
State: webkey.WebKeyState_STATE_ACTIVE,
|
||||
Config: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Ed25519{
|
||||
Ed25519: &webkey.WebKeyED25519Config{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
got := webKeyDetailsListToPb(list, instanceID)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_webKeyDetailsToPb(t *testing.T) {
|
||||
instanceID := "ownerid"
|
||||
type args struct {
|
||||
details *query.WebKeyDetails
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *webkey.GetWebKey
|
||||
}{
|
||||
{
|
||||
name: "RSA",
|
||||
args: args{&query.WebKeyDetails{
|
||||
KeyID: "keyID",
|
||||
CreationDate: time.Unix(123, 456),
|
||||
ChangeDate: time.Unix(789, 0),
|
||||
Sequence: 123,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits3072,
|
||||
Hasher: crypto.RSAHasherSHA384,
|
||||
},
|
||||
}},
|
||||
want: &webkey.GetWebKey{
|
||||
Details: &resource_object.Details{
|
||||
Id: "keyID",
|
||||
Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456},
|
||||
Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0},
|
||||
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
|
||||
},
|
||||
State: webkey.WebKeyState_STATE_ACTIVE,
|
||||
Config: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ECDSA",
|
||||
args: args{&query.WebKeyDetails{
|
||||
KeyID: "keyID",
|
||||
CreationDate: time.Unix(123, 456),
|
||||
ChangeDate: time.Unix(789, 0),
|
||||
Sequence: 123,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: &crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
}},
|
||||
want: &webkey.GetWebKey{
|
||||
Details: &resource_object.Details{
|
||||
Id: "keyID",
|
||||
Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456},
|
||||
Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0},
|
||||
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
|
||||
},
|
||||
State: webkey.WebKeyState_STATE_ACTIVE,
|
||||
Config: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Ecdsa{
|
||||
Ecdsa: &webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ED25519",
|
||||
args: args{&query.WebKeyDetails{
|
||||
KeyID: "keyID",
|
||||
CreationDate: time.Unix(123, 456),
|
||||
ChangeDate: time.Unix(789, 0),
|
||||
Sequence: 123,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: &crypto.WebKeyED25519Config{},
|
||||
}},
|
||||
want: &webkey.GetWebKey{
|
||||
Details: &resource_object.Details{
|
||||
Id: "keyID",
|
||||
Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456},
|
||||
Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0},
|
||||
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
|
||||
},
|
||||
State: webkey.WebKeyState_STATE_ACTIVE,
|
||||
Config: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Ed25519{
|
||||
Ed25519: &webkey.WebKeyED25519Config{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyDetailsToPb(tt.args.details, instanceID)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyStateToPb(t *testing.T) {
|
||||
type args struct {
|
||||
state domain.WebKeyState
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want webkey.WebKeyState
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
args: args{domain.WebKeyStateUnspecified},
|
||||
want: webkey.WebKeyState_STATE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
name: "initial",
|
||||
args: args{domain.WebKeyStateInitial},
|
||||
want: webkey.WebKeyState_STATE_INITIAL,
|
||||
},
|
||||
{
|
||||
name: "active",
|
||||
args: args{domain.WebKeyStateActive},
|
||||
want: webkey.WebKeyState_STATE_ACTIVE,
|
||||
},
|
||||
{
|
||||
name: "inactive",
|
||||
args: args{domain.WebKeyStateInactive},
|
||||
want: webkey.WebKeyState_STATE_INACTIVE,
|
||||
},
|
||||
{
|
||||
name: "removed",
|
||||
args: args{domain.WebKeyStateRemoved},
|
||||
want: webkey.WebKeyState_STATE_REMOVED,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{99},
|
||||
want: webkey.WebKeyState_STATE_UNSPECIFIED,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyStateToPb(tt.args.state)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyRSAConfigToPb(t *testing.T) {
|
||||
type args struct {
|
||||
config *crypto.WebKeyRSAConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *webkey.WebKeyRSAConfig
|
||||
}{
|
||||
{
|
||||
name: "2048, RSA256",
|
||||
args: args{&crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
}},
|
||||
want: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3072, RSA384",
|
||||
args: args{&crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits3072,
|
||||
Hasher: crypto.RSAHasherSHA384,
|
||||
}},
|
||||
want: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "4096, RSA512",
|
||||
args: args{&crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits4096,
|
||||
Hasher: crypto.RSAHasherSHA512,
|
||||
}},
|
||||
want: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyRSAConfigToPb(tt.args.config)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_webKeyECDSAConfigToPb(t *testing.T) {
|
||||
type args struct {
|
||||
config *crypto.WebKeyECDSAConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *webkey.WebKeyECDSAConfig
|
||||
}{
|
||||
{
|
||||
name: "P256",
|
||||
args: args{&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP256,
|
||||
}},
|
||||
want: &webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "P384",
|
||||
args: args{&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
}},
|
||||
want: &webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "P512",
|
||||
args: args{&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP512,
|
||||
}},
|
||||
want: &webkey.WebKeyECDSAConfig{
|
||||
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := webKeyECDSAConfigToPb(tt.args.config)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
245
internal/api/grpc/resources/webkey/v3/webkey_integration_test.go
Normal file
245
internal/api/grpc/resources/webkey/v3/webkey_integration_test.go
Normal file
@ -0,0 +1,245 @@
|
||||
//go:build integration
|
||||
|
||||
package webkey_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
SystemCTX context.Context
|
||||
Tester *integration.Tester
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(time.Hour)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
|
||||
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
|
||||
CTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_Feature_Disabled(t *testing.T) {
|
||||
client, iamCTX := createInstanceAndClients(t, false)
|
||||
|
||||
t.Run("CreateWebKey", func(t *testing.T) {
|
||||
_, err := client.CreateWebKey(iamCTX, &webkey.CreateWebKeyRequest{})
|
||||
assertFeatureDisabledError(t, err)
|
||||
})
|
||||
t.Run("ActivateWebKey", func(t *testing.T) {
|
||||
_, err := client.ActivateWebKey(iamCTX, &webkey.ActivateWebKeyRequest{
|
||||
Id: "1",
|
||||
})
|
||||
assertFeatureDisabledError(t, err)
|
||||
})
|
||||
t.Run("DeleteWebKey", func(t *testing.T) {
|
||||
_, err := client.DeleteWebKey(iamCTX, &webkey.DeleteWebKeyRequest{
|
||||
Id: "1",
|
||||
})
|
||||
assertFeatureDisabledError(t, err)
|
||||
})
|
||||
t.Run("ListWebKeys", func(t *testing.T) {
|
||||
_, err := client.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
|
||||
assertFeatureDisabledError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_ListWebKeys(t *testing.T) {
|
||||
client, iamCtx := createInstanceAndClients(t, true)
|
||||
// After the feature is first enabled, we can expect 2 generated keys with the default config.
|
||||
checkWebKeyListState(iamCtx, t, client, 2, "", &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_CreateWebKey(t *testing.T) {
|
||||
client, iamCtx := createInstanceAndClients(t, true)
|
||||
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
checkWebKeyListState(iamCtx, t, client, 3, "", &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_ActivateWebKey(t *testing.T) {
|
||||
client, iamCtx := createInstanceAndClients(t, true)
|
||||
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
|
||||
Id: resp.GetDetails().GetId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
checkWebKeyListState(iamCtx, t, client, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_DeleteWebKey(t *testing.T) {
|
||||
client, iamCtx := createInstanceAndClients(t, true)
|
||||
keyIDs := make([]string, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
|
||||
Key: &webkey.WebKey{
|
||||
Config: &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
keyIDs[i] = resp.GetDetails().GetId()
|
||||
}
|
||||
_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
|
||||
Id: keyIDs[0],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ok := t.Run("cannot delete active key", func(t *testing.T) {
|
||||
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
|
||||
Id: keyIDs[0],
|
||||
})
|
||||
require.Error(t, err)
|
||||
s := status.Convert(err)
|
||||
assert.Equal(t, codes.FailedPrecondition, s.Code())
|
||||
assert.Contains(t, s.Message(), "COMMAND-Chai1")
|
||||
})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = t.Run("delete inactive key", func(t *testing.T) {
|
||||
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
|
||||
Id: keyIDs[1],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// There are 2 keys from feature setup, +2 created, -1 deleted = 3
|
||||
checkWebKeyListState(iamCtx, t, client, 3, keyIDs[0], &webkey.WebKey_Rsa{
|
||||
Rsa: &webkey.WebKeyRSAConfig{
|
||||
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
|
||||
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func createInstanceAndClients(t *testing.T, enableFeature bool) (webkey.ZITADELWebKeysClient, context.Context) {
|
||||
domain, _, _, iamCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
|
||||
cc, err := grpc.NewClient(
|
||||
net.JoinHostPort(domain, "8080"),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
if enableFeature {
|
||||
features := feature.NewFeatureServiceClient(cc)
|
||||
_, err = features.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
WebKey: proto.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
return webkey.NewZITADELWebKeysClient(cc), iamCTX
|
||||
}
|
||||
|
||||
func assertFeatureDisabledError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.Error(t, err)
|
||||
s := status.Convert(err)
|
||||
assert.Equal(t, codes.FailedPrecondition, s.Code())
|
||||
assert.Contains(t, s.Message(), "WEBKEY-Ohx6E")
|
||||
}
|
||||
|
||||
func checkWebKeyListState(ctx context.Context, t *testing.T, client webkey.ZITADELWebKeysClient, nKeys int, expectActiveKeyID string, config any) {
|
||||
resp, err := client.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
list := resp.GetWebKeys()
|
||||
require.Len(t, list, nKeys)
|
||||
|
||||
now := time.Now()
|
||||
var gotActiveKeyID string
|
||||
for _, key := range list {
|
||||
integration.AssertResourceDetails(t, &resource_object.Details{
|
||||
Created: timestamppb.Now(),
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: Tester.Instance.InstanceID(),
|
||||
},
|
||||
}, key.GetDetails())
|
||||
assert.WithinRange(t, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
|
||||
assert.NotEqual(t, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState())
|
||||
assert.NotEqual(t, webkey.WebKeyState_STATE_REMOVED, key.GetState())
|
||||
assert.Equal(t, config, key.GetConfig().GetConfig())
|
||||
|
||||
if key.GetState() == webkey.WebKeyState_STATE_ACTIVE {
|
||||
gotActiveKeyID = key.GetDetails().GetId()
|
||||
}
|
||||
}
|
||||
assert.NotEmpty(t, gotActiveKeyID)
|
||||
if expectActiveKeyID != "" {
|
||||
assert.Equal(t, expectActiveKeyID, gotActiveKeyID)
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
@ -208,7 +207,7 @@ func (k keySetMap) getKey(keyID string) (*jose.JSONWebKey, error) {
|
||||
return &jose.JSONWebKey{
|
||||
Key: pubKey,
|
||||
KeyID: keyID,
|
||||
Use: domain.KeyUsageSigning.String(),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -12,14 +12,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type publicKey struct {
|
||||
id string
|
||||
alg string
|
||||
use domain.KeyUsage
|
||||
use crypto.KeyUsage
|
||||
seq uint64
|
||||
expiry time.Time
|
||||
key any
|
||||
@ -33,7 +33,7 @@ func (k *publicKey) Algorithm() string {
|
||||
return k.alg
|
||||
}
|
||||
|
||||
func (k *publicKey) Use() domain.KeyUsage {
|
||||
func (k *publicKey) Use() crypto.KeyUsage {
|
||||
return k.use
|
||||
}
|
||||
|
||||
@ -55,21 +55,21 @@ var (
|
||||
"key1": {
|
||||
id: "key1",
|
||||
alg: "alg",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
seq: 1,
|
||||
expiry: clock.Now().Add(time.Minute),
|
||||
},
|
||||
"key2": {
|
||||
id: "key2",
|
||||
alg: "alg",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
seq: 3,
|
||||
expiry: clock.Now().Add(10 * time.Hour),
|
||||
},
|
||||
"exp1": {
|
||||
id: "key2",
|
||||
alg: "alg",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
seq: 4,
|
||||
expiry: clock.Now().Add(-time.Hour),
|
||||
},
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
@ -53,7 +52,7 @@ func (c *CertificateAndKey) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
|
||||
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
|
||||
err = retry(func() error {
|
||||
certAndKey, err = p.getCertificateAndKey(ctx, usage)
|
||||
if err != nil {
|
||||
@ -67,7 +66,7 @@ func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsag
|
||||
return certAndKey, err
|
||||
}
|
||||
|
||||
func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (*key.CertificateAndKey, error) {
|
||||
func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (*key.CertificateAndKey, error) {
|
||||
certs, err := p.query.ActiveCertificates(ctx, time.Now().Add(gracefulPeriod), usage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -87,7 +86,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsag
|
||||
|
||||
func (p *Storage) refreshCertificate(
|
||||
ctx context.Context,
|
||||
usage domain.KeyUsage,
|
||||
usage crypto.KeyUsage,
|
||||
position float64,
|
||||
) error {
|
||||
ok, err := p.ensureIsLatestCertificate(ctx, position)
|
||||
@ -112,7 +111,7 @@ func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float6
|
||||
return position >= maxSequence, nil
|
||||
}
|
||||
|
||||
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage domain.KeyUsage) error {
|
||||
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
ctx = setSAMLCtx(ctx)
|
||||
@ -128,8 +127,8 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case domain.KeyUsageSAMLMetadataSigning, domain.KeyUsageSAMLResponseSinging:
|
||||
certAndKey, err := p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||
case crypto.KeyUsageSAMLMetadataSigning, crypto.KeyUsageSAMLResponseSinging:
|
||||
certAndKey, err := p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca certificate: %w", err)
|
||||
}
|
||||
@ -138,14 +137,14 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case domain.KeyUsageSAMLMetadataSigning:
|
||||
case crypto.KeyUsageSAMLMetadataSigning:
|
||||
return p.command.GenerateSAMLMetadataCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||
case domain.KeyUsageSAMLResponseSinging:
|
||||
case crypto.KeyUsageSAMLResponseSinging:
|
||||
return p.command.GenerateSAMLResponseCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||
default:
|
||||
return fmt.Errorf("unknown usage")
|
||||
}
|
||||
case domain.KeyUsageSAMLCA:
|
||||
case crypto.KeyUsageSAMLCA:
|
||||
return p.command.GenerateSAMLCACertificate(setSAMLCtx(ctx), p.certificateAlgorithm)
|
||||
default:
|
||||
return fmt.Errorf("unknown certificate usage")
|
||||
|
@ -87,15 +87,15 @@ func (p *Storage) Health(context.Context) error {
|
||||
}
|
||||
|
||||
func (p *Storage) GetCA(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA)
|
||||
}
|
||||
|
||||
func (p *Storage) GetMetadataSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLMetadataSigning)
|
||||
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLMetadataSigning)
|
||||
}
|
||||
|
||||
func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLResponseSinging)
|
||||
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLResponseSinging)
|
||||
}
|
||||
|
||||
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@ -76,6 +77,7 @@ type Commands struct {
|
||||
defaultSecretGenerators *SecretGenerators
|
||||
|
||||
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error)
|
||||
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
|
||||
|
||||
GrpcMethodExisting func(method string) bool
|
||||
GrpcServiceExisting func(method string) bool
|
||||
@ -157,6 +159,7 @@ func StartCommands(
|
||||
defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime,
|
||||
defaultSecretGenerators: defaultSecretGenerators,
|
||||
samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.CertificateSize, defaults.KeyConfig.CertificateLifetime),
|
||||
webKeyGenerator: crypto.GenerateEncryptedWebKey,
|
||||
// always true for now until we can check with an eventlist
|
||||
EventExisting: func(event string) bool { return true },
|
||||
// always true for now until we can check with an eventlist
|
||||
|
@ -34,12 +34,20 @@ const (
|
||||
)
|
||||
|
||||
type InstanceSetup struct {
|
||||
zitadel ZitadelConfig
|
||||
InstanceName string
|
||||
CustomDomain string
|
||||
DefaultLanguage language.Tag
|
||||
Org InstanceOrgSetup
|
||||
SecretGenerators *SecretGenerators
|
||||
zitadel ZitadelConfig
|
||||
InstanceName string
|
||||
CustomDomain string
|
||||
DefaultLanguage language.Tag
|
||||
Org InstanceOrgSetup
|
||||
SecretGenerators *SecretGenerators
|
||||
WebKeys struct {
|
||||
Type crypto.WebKeyConfigType
|
||||
Config struct {
|
||||
RSABits crypto.RSABits
|
||||
RSAHasher crypto.RSAHasher
|
||||
EllipticCurve crypto.EllipticCurve
|
||||
}
|
||||
}
|
||||
PasswordComplexityPolicy struct {
|
||||
MinLength uint64
|
||||
HasLowercase bool
|
||||
@ -267,6 +275,9 @@ func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (vali
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||
if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||
setupFeatures(&validations, setup.Features, setup.zitadel.instanceID)
|
||||
setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits)
|
||||
@ -390,6 +401,29 @@ func setupFeatures(validations *[]preparation.Validation, features *InstanceFeat
|
||||
}
|
||||
}
|
||||
|
||||
func setupWebKeys(c *Commands, validations *[]preparation.Validation, instanceID string, setup *InstanceSetup) error {
|
||||
var conf crypto.WebKeyConfig
|
||||
switch setup.WebKeys.Type {
|
||||
case crypto.WebKeyConfigTypeUnspecified:
|
||||
return nil // config disabled, skip
|
||||
case crypto.WebKeyConfigTypeRSA:
|
||||
conf = &crypto.WebKeyRSAConfig{
|
||||
Bits: setup.WebKeys.Config.RSABits,
|
||||
Hasher: setup.WebKeys.Config.RSAHasher,
|
||||
}
|
||||
case crypto.WebKeyConfigTypeECDSA:
|
||||
conf = &crypto.WebKeyECDSAConfig{
|
||||
Curve: setup.WebKeys.Config.EllipticCurve,
|
||||
}
|
||||
case crypto.WebKeyConfigTypeED25519:
|
||||
conf = &crypto.WebKeyED25519Config{}
|
||||
default:
|
||||
return zerrors.ThrowInternalf(nil, "COMMAND-sieX0", "Errors.Internal unknown web key type %q", setup.WebKeys.Type)
|
||||
}
|
||||
*validations = append(*validations, c.prepareGenerateInitialWebKeys(instanceID, conf))
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
|
||||
if oidcSettings == nil {
|
||||
return
|
||||
|
@ -3,8 +3,11 @@ package command
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
@ -20,6 +23,7 @@ type InstanceFeatures struct {
|
||||
TokenExchange *bool
|
||||
Actions *bool
|
||||
ImprovedPerformance []feature.ImprovedPerformanceType
|
||||
WebKey *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
@ -30,7 +34,8 @@ func (m *InstanceFeatures) isEmpty() bool {
|
||||
m.TokenExchange == nil &&
|
||||
m.Actions == nil &&
|
||||
// nil check to allow unset improvements
|
||||
m.ImprovedPerformance == nil
|
||||
m.ImprovedPerformance == nil &&
|
||||
m.WebKey == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
@ -41,6 +46,9 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.setupWebKeyFeature(ctx, wm, f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commands := wm.setCommands(ctx, f)
|
||||
if len(commands) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
@ -61,6 +69,21 @@ func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Vali
|
||||
}
|
||||
}
|
||||
|
||||
// setupWebKeyFeature generates the initial web keys for the instance,
|
||||
// if the feature is enabled in the request and the feature wasn't enabled already in the writeModel.
|
||||
// [Commands.GenerateInitialWebKeys] checks if keys already exist and does nothing if that's the case.
|
||||
// The default config of a RSA key with 2048 and the SHA256 hasher is assumed.
|
||||
// Users can customize this after using the webkey/v3 API.
|
||||
func (c *Commands) setupWebKeyFeature(ctx context.Context, wm *InstanceFeaturesWriteModel, f *InstanceFeatures) error {
|
||||
if !gu.Value(f.WebKey) || gu.Value(wm.WebKey) {
|
||||
return nil
|
||||
}
|
||||
return c.GenerateInitialWebKeys(ctx, &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits2048,
|
||||
Hasher: crypto.RSAHasherSHA256,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
|
@ -67,6 +67,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.InstanceTokenExchangeEventType,
|
||||
feature_v2.InstanceActionsEventType,
|
||||
feature_v2.InstanceImprovedPerformanceEventType,
|
||||
feature_v2.InstanceWebKeyEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@ -100,6 +101,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
|
||||
case feature.KeyImprovedPerformance:
|
||||
v := value.([]feature.ImprovedPerformanceType)
|
||||
features.ImprovedPerformance = v
|
||||
case feature.KeyWebKey:
|
||||
v := value.(bool)
|
||||
features.WebKey = &v
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,5 +117,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType)
|
||||
cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType)
|
||||
return cmds
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
)
|
||||
|
||||
@ -32,7 +31,7 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string)
|
||||
_, err = c.eventstore.Push(ctx, keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSigning,
|
||||
crypto.KeyUsageSigning,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
privateKeyExp, publicKeyExp))
|
||||
@ -69,7 +68,7 @@ func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm stri
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLCA,
|
||||
crypto.KeyUsageSAMLCA,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after,
|
||||
@ -115,7 +114,7 @@ func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorith
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLResponseSinging,
|
||||
crypto.KeyUsageSAMLResponseSinging,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after,
|
||||
@ -160,7 +159,7 @@ func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorith
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLMetadataSigning,
|
||||
crypto.KeyUsageSAMLMetadataSigning,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after),
|
||||
|
@ -1,6 +1,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
@ -9,7 +10,7 @@ import (
|
||||
type KeyPairWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
Usage domain.KeyUsage
|
||||
Usage crypto.KeyUsage
|
||||
Algorithm string
|
||||
PrivateKey *domain.Key
|
||||
PublicKey *domain.Key
|
||||
|
188
internal/command/web_key.go
Normal file
188
internal/command/web_key.go
Normal file
@ -0,0 +1,188 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type WebKeyDetails struct {
|
||||
KeyID string
|
||||
ObjectDetails *domain.ObjectDetails
|
||||
}
|
||||
|
||||
// CreateWebKey creates one web key pair for the instance.
|
||||
// If the instance does not have an active key, the new key is activated.
|
||||
func (c *Commands) CreateWebKey(ctx context.Context, conf crypto.WebKeyConfig) (_ *WebKeyDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
_, activeID, err := c.getAllWebKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, authz.GetInstance(ctx).InstanceID(), conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commands := []eventstore.Command{addedCmd}
|
||||
if activeID == "" {
|
||||
commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate))
|
||||
}
|
||||
model := NewWebKeyWriteModel(aggregate.ID, authz.GetInstance(ctx).InstanceID())
|
||||
err = c.pushAppendAndReduce(ctx, model, commands...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WebKeyDetails{
|
||||
KeyID: aggregate.ID,
|
||||
ObjectDetails: writeModelToObjectDetails(&model.WriteModel),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateInitialWebKeys creates 2 web key pairs for the instance.
|
||||
// The first key is activated for signing use.
|
||||
// If the instance already has keys, this is noop.
|
||||
func (c *Commands) GenerateInitialWebKeys(ctx context.Context, conf crypto.WebKeyConfig) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
keys, _, err := c.getAllWebKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keys) != 0 {
|
||||
return nil
|
||||
}
|
||||
commands, err := c.generateInitialWebKeysCommands(ctx, authz.GetInstance(ctx).InstanceID(), conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, commands...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) generateInitialWebKeysCommands(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ []eventstore.Command, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
commands := make([]eventstore.Command, 0, 3)
|
||||
for i := 0; i < 2; i++ {
|
||||
addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, instanceID, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commands = append(commands, addedCmd)
|
||||
if i == 0 {
|
||||
commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate))
|
||||
}
|
||||
}
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func (c *Commands) generateWebKeyCommand(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ eventstore.Command, _ *eventstore.Aggregate, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
keyID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
encryptedPrivate, public, err := c.webKeyGenerator(keyID, c.keyAlgorithm, conf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
aggregate := webkey.NewAggregate(keyID, instanceID)
|
||||
addedCmd, err := webkey.NewAddedEvent(ctx, aggregate, encryptedPrivate, public, conf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return addedCmd, aggregate, nil
|
||||
}
|
||||
|
||||
// ActivateWebKey activates the key identified by keyID.
|
||||
// Any previously activated key on the current instance is deactivated.
|
||||
func (c *Commands) ActivateWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
keys, activeID, err := c.getAllWebKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if activeID == keyID {
|
||||
return writeModelToObjectDetails(
|
||||
&keys[activeID].WriteModel,
|
||||
), nil
|
||||
}
|
||||
nextActive, ok := keys[keyID]
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound")
|
||||
}
|
||||
|
||||
commands := make([]eventstore.Command, 0, 2)
|
||||
commands = append(commands, webkey.NewActivatedEvent(ctx,
|
||||
webkey.AggregateFromWriteModel(ctx, &nextActive.WriteModel),
|
||||
))
|
||||
if activeID != "" {
|
||||
commands = append(commands, webkey.NewDeactivatedEvent(ctx,
|
||||
webkey.AggregateFromWriteModel(ctx, &keys[activeID].WriteModel),
|
||||
))
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, nextActive, commands...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&nextActive.WriteModel), nil
|
||||
}
|
||||
|
||||
// getAllWebKeys searches for all web keys on the instance and returns a map of key IDs.
|
||||
// activeID is the id of the currently active key.
|
||||
func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWriteModel, activeID string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
models := newWebKeyWriteModels(authz.GetInstance(ctx).InstanceID())
|
||||
if err = c.eventstore.FilterToQueryReducer(ctx, models); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return models.keys, models.activeID, nil
|
||||
}
|
||||
|
||||
func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||
if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model.State == domain.WebKeyStateUnspecified {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound")
|
||||
}
|
||||
if model.State == domain.WebKeyStateActive {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx,
|
||||
webkey.AggregateFromWriteModel(ctx, &model.WriteModel),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
return c.generateInitialWebKeysCommands(ctx, instanceID, conf)
|
||||
}, nil
|
||||
}
|
||||
}
|
131
internal/command/web_key_model.go
Normal file
131
internal/command/web_key_model.go
Normal file
@ -0,0 +1,131 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
)
|
||||
|
||||
type WebKeyWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
State domain.WebKeyState
|
||||
PrivateKey *crypto.CryptoValue
|
||||
PublicKey *jose.JSONWebKey
|
||||
}
|
||||
|
||||
func NewWebKeyWriteModel(keyID, resourceOwner string) *WebKeyWriteModel {
|
||||
return &WebKeyWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: keyID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *WebKeyWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
wm.WriteModel.AppendEvents(events...)
|
||||
}
|
||||
|
||||
func (wm *WebKeyWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
if event.Aggregate().ID != wm.AggregateID {
|
||||
continue
|
||||
}
|
||||
switch e := event.(type) {
|
||||
case *webkey.AddedEvent:
|
||||
wm.State = domain.WebKeyStateInitial
|
||||
wm.PrivateKey = e.PrivateKey
|
||||
wm.PublicKey = e.PublicKey
|
||||
case *webkey.ActivatedEvent:
|
||||
wm.State = domain.WebKeyStateActive
|
||||
case *webkey.DeactivatedEvent:
|
||||
wm.State = domain.WebKeyStateInactive
|
||||
case *webkey.RemovedEvent:
|
||||
wm.State = domain.WebKeyStateRemoved
|
||||
wm.PrivateKey = nil
|
||||
wm.PublicKey = nil
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *WebKeyWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(webkey.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
webkey.AddedEventType,
|
||||
webkey.ActivatedEventType,
|
||||
webkey.DeactivatedEventType,
|
||||
webkey.RemovedEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
||||
|
||||
type webKeyWriteModels struct {
|
||||
resourceOwner string
|
||||
events []eventstore.Event
|
||||
keys map[string]*WebKeyWriteModel
|
||||
activeID string
|
||||
}
|
||||
|
||||
func newWebKeyWriteModels(resourceOwner string) *webKeyWriteModels {
|
||||
return &webKeyWriteModels{
|
||||
resourceOwner: resourceOwner,
|
||||
keys: make(map[string]*WebKeyWriteModel),
|
||||
}
|
||||
}
|
||||
|
||||
func (models *webKeyWriteModels) AppendEvents(events ...eventstore.Event) {
|
||||
models.events = append(models.events, events...)
|
||||
}
|
||||
|
||||
func (models *webKeyWriteModels) Reduce() error {
|
||||
for _, event := range models.events {
|
||||
aggregate := event.Aggregate()
|
||||
if models.keys[aggregate.ID] == nil {
|
||||
models.keys[aggregate.ID] = NewWebKeyWriteModel(aggregate.ID, aggregate.ResourceOwner)
|
||||
}
|
||||
|
||||
switch event.(type) {
|
||||
case *webkey.AddedEvent:
|
||||
break
|
||||
case *webkey.ActivatedEvent:
|
||||
models.activeID = aggregate.ID
|
||||
case *webkey.DeactivatedEvent:
|
||||
if models.activeID == aggregate.ID {
|
||||
models.activeID = ""
|
||||
}
|
||||
case *webkey.RemovedEvent:
|
||||
delete(models.keys, aggregate.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
model := models.keys[aggregate.ID]
|
||||
model.AppendEvents(event)
|
||||
if err := model.Reduce(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
models.events = models.events[0:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (models *webKeyWriteModels) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(models.resourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(webkey.AggregateType).
|
||||
EventTypes(
|
||||
webkey.AddedEventType,
|
||||
webkey.ActivatedEventType,
|
||||
webkey.DeactivatedEventType,
|
||||
webkey.RemovedEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
754
internal/command/web_key_test.go
Normal file
754
internal/command/web_key_test.go
Normal file
@ -0,0 +1,754 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_CreateWebKey(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
|
||||
}
|
||||
type args struct {
|
||||
conf crypto.WebKeyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *WebKeyDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "generate error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
|
||||
webKeyGenerator: func(string, crypto.EncryptionAlgorithm, crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
|
||||
return nil, nil, io.ErrClosedPipe
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "generate key, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key2",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key2"),
|
||||
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
|
||||
return &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
}, &jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
want: &WebKeyDetails{
|
||||
KeyID: "key2",
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generate and activate key, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
),
|
||||
webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
|
||||
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
|
||||
return &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
}, &jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
want: &WebKeyDetails{
|
||||
KeyID: "key1",
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webKeyGenerator: tt.fields.webKeyGenerator,
|
||||
}
|
||||
got, err := c.CreateWebKey(ctx, tt.args.conf)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_GenerateInitialWebKeys(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
|
||||
}
|
||||
type args struct {
|
||||
conf crypto.WebKeyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "key found, noop",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(expectFilter()),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrUnexpectedEOF),
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: io.ErrUnexpectedEOF,
|
||||
},
|
||||
{
|
||||
name: "keys generated and activated",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
),
|
||||
webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
),
|
||||
mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key2",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1", "key2"),
|
||||
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
|
||||
return &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
}, &jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webKeyGenerator: tt.fields.webKeyGenerator,
|
||||
}
|
||||
err := c.GenerateInitialWebKeys(ctx, tt.args.conf)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ActivateWebKey(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
|
||||
}
|
||||
type args struct {
|
||||
keyID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{"key2"},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key2"},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "activate next, de-activate old, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key2",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
),
|
||||
webkey.NewDeactivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key2"},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "activate next, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
webKeyGenerator: tt.fields.webKeyGenerator,
|
||||
}
|
||||
got, err := c.ActivateWebKey(ctx, tt.args.keyID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_DeleteWebKey(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
keyID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "key active error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete"),
|
||||
},
|
||||
{
|
||||
name: "delete deactivated key",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key2",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
|
||||
webkey.NewAggregate("key2", "instance1"),
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewDeactivatedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
webkey.NewRemovedEvent(ctx, webkey.NewAggregate("key1", "instance1")),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
ID: "key1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := c.DeleteWebKey(ctx, tt.args.keyID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewWebkeyAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
privateKey *crypto.CryptoValue,
|
||||
publicKey *jose.JSONWebKey,
|
||||
config crypto.WebKeyConfig) *webkey.AddedEvent {
|
||||
event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return event
|
||||
}
|
@ -68,6 +68,14 @@ func Encrypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func EncryptJSON(obj any, alg EncryptionAlgorithm) (*CryptoValue, error) {
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "CRYPT-Ei6doF", "error encrypting value")
|
||||
}
|
||||
return Encrypt(data, alg)
|
||||
}
|
||||
|
||||
func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) {
|
||||
if err := checkEncryptionAlgorithm(value, alg); err != nil {
|
||||
return nil, err
|
||||
@ -75,6 +83,17 @@ func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) {
|
||||
return alg.Decrypt(value.Crypted, value.KeyID)
|
||||
}
|
||||
|
||||
func DecryptJSON(value *CryptoValue, dst any, alg EncryptionAlgorithm) error {
|
||||
data, err := Decrypt(value, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(data, dst); err != nil {
|
||||
return zerrors.ThrowInternal(err, "CRYPT-Jaik2R", "error decrypting value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecryptString decrypts the value using the key identified by keyID.
|
||||
// When the decrypted value contains non-UTF8 characters an error is returned.
|
||||
func DecryptString(value *CryptoValue, alg EncryptionAlgorithm) (string, error) {
|
||||
|
116
internal/crypto/ellipticcurve_enumer.go
Normal file
116
internal/crypto/ellipticcurve_enumer.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Code generated by "enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment"; DO NOT EDIT.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _EllipticCurveName = "P256P384P512"
|
||||
|
||||
var _EllipticCurveIndex = [...]uint8{0, 0, 4, 8, 12}
|
||||
|
||||
const _EllipticCurveLowerName = "p256p384p512"
|
||||
|
||||
func (i EllipticCurve) String() string {
|
||||
if i < 0 || i >= EllipticCurve(len(_EllipticCurveIndex)-1) {
|
||||
return fmt.Sprintf("EllipticCurve(%d)", i)
|
||||
}
|
||||
return _EllipticCurveName[_EllipticCurveIndex[i]:_EllipticCurveIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _EllipticCurveNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[EllipticCurveUnspecified-(0)]
|
||||
_ = x[EllipticCurveP256-(1)]
|
||||
_ = x[EllipticCurveP384-(2)]
|
||||
_ = x[EllipticCurveP512-(3)]
|
||||
}
|
||||
|
||||
var _EllipticCurveValues = []EllipticCurve{EllipticCurveUnspecified, EllipticCurveP256, EllipticCurveP384, EllipticCurveP512}
|
||||
|
||||
var _EllipticCurveNameToValueMap = map[string]EllipticCurve{
|
||||
_EllipticCurveName[0:0]: EllipticCurveUnspecified,
|
||||
_EllipticCurveLowerName[0:0]: EllipticCurveUnspecified,
|
||||
_EllipticCurveName[0:4]: EllipticCurveP256,
|
||||
_EllipticCurveLowerName[0:4]: EllipticCurveP256,
|
||||
_EllipticCurveName[4:8]: EllipticCurveP384,
|
||||
_EllipticCurveLowerName[4:8]: EllipticCurveP384,
|
||||
_EllipticCurveName[8:12]: EllipticCurveP512,
|
||||
_EllipticCurveLowerName[8:12]: EllipticCurveP512,
|
||||
}
|
||||
|
||||
var _EllipticCurveNames = []string{
|
||||
_EllipticCurveName[0:0],
|
||||
_EllipticCurveName[0:4],
|
||||
_EllipticCurveName[4:8],
|
||||
_EllipticCurveName[8:12],
|
||||
}
|
||||
|
||||
// EllipticCurveString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func EllipticCurveString(s string) (EllipticCurve, error) {
|
||||
if val, ok := _EllipticCurveNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _EllipticCurveNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to EllipticCurve values", s)
|
||||
}
|
||||
|
||||
// EllipticCurveValues returns all values of the enum
|
||||
func EllipticCurveValues() []EllipticCurve {
|
||||
return _EllipticCurveValues
|
||||
}
|
||||
|
||||
// EllipticCurveStrings returns a slice of all String values of the enum
|
||||
func EllipticCurveStrings() []string {
|
||||
strs := make([]string, len(_EllipticCurveNames))
|
||||
copy(strs, _EllipticCurveNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAEllipticCurve returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i EllipticCurve) IsAEllipticCurve() bool {
|
||||
for _, v := range _EllipticCurveValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for EllipticCurve
|
||||
func (i EllipticCurve) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for EllipticCurve
|
||||
func (i *EllipticCurve) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("EllipticCurve should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = EllipticCurveString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for EllipticCurve
|
||||
func (i EllipticCurve) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for EllipticCurve
|
||||
func (i *EllipticCurve) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
*i, err = EllipticCurveString(string(text))
|
||||
return err
|
||||
}
|
136
internal/crypto/rsabits_enumer.go
Normal file
136
internal/crypto/rsabits_enumer.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Code generated by "enumer -type RSABits -trimprefix RSABits -text -json -linecomment"; DO NOT EDIT.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
_RSABitsName_0 = ""
|
||||
_RSABitsLowerName_0 = ""
|
||||
_RSABitsName_1 = "2048"
|
||||
_RSABitsLowerName_1 = "2048"
|
||||
_RSABitsName_2 = "3072"
|
||||
_RSABitsLowerName_2 = "3072"
|
||||
_RSABitsName_3 = "4096"
|
||||
_RSABitsLowerName_3 = "4096"
|
||||
)
|
||||
|
||||
var (
|
||||
_RSABitsIndex_0 = [...]uint8{0, 0}
|
||||
_RSABitsIndex_1 = [...]uint8{0, 4}
|
||||
_RSABitsIndex_2 = [...]uint8{0, 4}
|
||||
_RSABitsIndex_3 = [...]uint8{0, 4}
|
||||
)
|
||||
|
||||
func (i RSABits) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _RSABitsName_0
|
||||
case i == 2048:
|
||||
return _RSABitsName_1
|
||||
case i == 3072:
|
||||
return _RSABitsName_2
|
||||
case i == 4096:
|
||||
return _RSABitsName_3
|
||||
default:
|
||||
return fmt.Sprintf("RSABits(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _RSABitsNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[RSABitsUnspecified-(0)]
|
||||
_ = x[RSABits2048-(2048)]
|
||||
_ = x[RSABits3072-(3072)]
|
||||
_ = x[RSABits4096-(4096)]
|
||||
}
|
||||
|
||||
var _RSABitsValues = []RSABits{RSABitsUnspecified, RSABits2048, RSABits3072, RSABits4096}
|
||||
|
||||
var _RSABitsNameToValueMap = map[string]RSABits{
|
||||
_RSABitsName_0[0:0]: RSABitsUnspecified,
|
||||
_RSABitsLowerName_0[0:0]: RSABitsUnspecified,
|
||||
_RSABitsName_1[0:4]: RSABits2048,
|
||||
_RSABitsLowerName_1[0:4]: RSABits2048,
|
||||
_RSABitsName_2[0:4]: RSABits3072,
|
||||
_RSABitsLowerName_2[0:4]: RSABits3072,
|
||||
_RSABitsName_3[0:4]: RSABits4096,
|
||||
_RSABitsLowerName_3[0:4]: RSABits4096,
|
||||
}
|
||||
|
||||
var _RSABitsNames = []string{
|
||||
_RSABitsName_0[0:0],
|
||||
_RSABitsName_1[0:4],
|
||||
_RSABitsName_2[0:4],
|
||||
_RSABitsName_3[0:4],
|
||||
}
|
||||
|
||||
// RSABitsString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func RSABitsString(s string) (RSABits, error) {
|
||||
if val, ok := _RSABitsNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _RSABitsNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to RSABits values", s)
|
||||
}
|
||||
|
||||
// RSABitsValues returns all values of the enum
|
||||
func RSABitsValues() []RSABits {
|
||||
return _RSABitsValues
|
||||
}
|
||||
|
||||
// RSABitsStrings returns a slice of all String values of the enum
|
||||
func RSABitsStrings() []string {
|
||||
strs := make([]string, len(_RSABitsNames))
|
||||
copy(strs, _RSABitsNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsARSABits returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i RSABits) IsARSABits() bool {
|
||||
for _, v := range _RSABitsValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for RSABits
|
||||
func (i RSABits) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for RSABits
|
||||
func (i *RSABits) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("RSABits should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = RSABitsString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for RSABits
|
||||
func (i RSABits) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for RSABits
|
||||
func (i *RSABits) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
*i, err = RSABitsString(string(text))
|
||||
return err
|
||||
}
|
116
internal/crypto/rsahasher_enumer.go
Normal file
116
internal/crypto/rsahasher_enumer.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Code generated by "enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment"; DO NOT EDIT.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _RSAHasherName = "SHA256SHA384SHA512"
|
||||
|
||||
var _RSAHasherIndex = [...]uint8{0, 0, 6, 12, 18}
|
||||
|
||||
const _RSAHasherLowerName = "sha256sha384sha512"
|
||||
|
||||
func (i RSAHasher) String() string {
|
||||
if i < 0 || i >= RSAHasher(len(_RSAHasherIndex)-1) {
|
||||
return fmt.Sprintf("RSAHasher(%d)", i)
|
||||
}
|
||||
return _RSAHasherName[_RSAHasherIndex[i]:_RSAHasherIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _RSAHasherNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[RSAHasherUnspecified-(0)]
|
||||
_ = x[RSAHasherSHA256-(1)]
|
||||
_ = x[RSAHasherSHA384-(2)]
|
||||
_ = x[RSAHasherSHA512-(3)]
|
||||
}
|
||||
|
||||
var _RSAHasherValues = []RSAHasher{RSAHasherUnspecified, RSAHasherSHA256, RSAHasherSHA384, RSAHasherSHA512}
|
||||
|
||||
var _RSAHasherNameToValueMap = map[string]RSAHasher{
|
||||
_RSAHasherName[0:0]: RSAHasherUnspecified,
|
||||
_RSAHasherLowerName[0:0]: RSAHasherUnspecified,
|
||||
_RSAHasherName[0:6]: RSAHasherSHA256,
|
||||
_RSAHasherLowerName[0:6]: RSAHasherSHA256,
|
||||
_RSAHasherName[6:12]: RSAHasherSHA384,
|
||||
_RSAHasherLowerName[6:12]: RSAHasherSHA384,
|
||||
_RSAHasherName[12:18]: RSAHasherSHA512,
|
||||
_RSAHasherLowerName[12:18]: RSAHasherSHA512,
|
||||
}
|
||||
|
||||
var _RSAHasherNames = []string{
|
||||
_RSAHasherName[0:0],
|
||||
_RSAHasherName[0:6],
|
||||
_RSAHasherName[6:12],
|
||||
_RSAHasherName[12:18],
|
||||
}
|
||||
|
||||
// RSAHasherString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func RSAHasherString(s string) (RSAHasher, error) {
|
||||
if val, ok := _RSAHasherNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _RSAHasherNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to RSAHasher values", s)
|
||||
}
|
||||
|
||||
// RSAHasherValues returns all values of the enum
|
||||
func RSAHasherValues() []RSAHasher {
|
||||
return _RSAHasherValues
|
||||
}
|
||||
|
||||
// RSAHasherStrings returns a slice of all String values of the enum
|
||||
func RSAHasherStrings() []string {
|
||||
strs := make([]string, len(_RSAHasherNames))
|
||||
copy(strs, _RSAHasherNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsARSAHasher returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i RSAHasher) IsARSAHasher() bool {
|
||||
for _, v := range _RSAHasherValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for RSAHasher
|
||||
func (i RSAHasher) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for RSAHasher
|
||||
func (i *RSAHasher) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("RSAHasher should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = RSAHasherString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for RSAHasher
|
||||
func (i RSAHasher) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for RSAHasher
|
||||
func (i *RSAHasher) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
*i, err = RSAHasherString(string(text))
|
||||
return err
|
||||
}
|
238
internal/crypto/web_key.go
Normal file
238
internal/crypto/web_key.go
Normal file
@ -0,0 +1,238 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type KeyUsage int32
|
||||
|
||||
const (
|
||||
KeyUsageSigning KeyUsage = iota
|
||||
KeyUsageSAMLMetadataSigning
|
||||
KeyUsageSAMLResponseSinging
|
||||
KeyUsageSAMLCA
|
||||
)
|
||||
|
||||
func (u KeyUsage) String() string {
|
||||
switch u {
|
||||
case KeyUsageSigning:
|
||||
return "sig"
|
||||
case KeyUsageSAMLCA:
|
||||
return "saml_ca"
|
||||
case KeyUsageSAMLResponseSinging:
|
||||
return "saml_response_sig"
|
||||
case KeyUsageSAMLMetadataSigning:
|
||||
return "saml_metadata_sig"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//go:generate enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment
|
||||
type WebKeyConfigType int
|
||||
|
||||
const (
|
||||
WebKeyConfigTypeUnspecified WebKeyConfigType = iota //
|
||||
WebKeyConfigTypeRSA
|
||||
WebKeyConfigTypeECDSA
|
||||
WebKeyConfigTypeED25519
|
||||
)
|
||||
|
||||
//go:generate enumer -type RSABits -trimprefix RSABits -text -json -linecomment
|
||||
type RSABits int
|
||||
|
||||
const (
|
||||
RSABitsUnspecified RSABits = 0 //
|
||||
RSABits2048 RSABits = 2048
|
||||
RSABits3072 RSABits = 3072
|
||||
RSABits4096 RSABits = 4096
|
||||
)
|
||||
|
||||
type RSAHasher int
|
||||
|
||||
//go:generate enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment
|
||||
const (
|
||||
RSAHasherUnspecified RSAHasher = iota //
|
||||
RSAHasherSHA256
|
||||
RSAHasherSHA384
|
||||
RSAHasherSHA512
|
||||
)
|
||||
|
||||
type EllipticCurve int
|
||||
|
||||
//go:generate enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment
|
||||
const (
|
||||
EllipticCurveUnspecified EllipticCurve = iota //
|
||||
EllipticCurveP256
|
||||
EllipticCurveP384
|
||||
EllipticCurveP512
|
||||
)
|
||||
|
||||
type WebKeyConfig interface {
|
||||
Alg() jose.SignatureAlgorithm
|
||||
Type() WebKeyConfigType // Type is needed to make Unmarshal work
|
||||
IsValid() error
|
||||
}
|
||||
|
||||
func UnmarshalWebKeyConfig(data []byte, configType WebKeyConfigType) (config WebKeyConfig, err error) {
|
||||
switch configType {
|
||||
case WebKeyConfigTypeUnspecified:
|
||||
return nil, zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal")
|
||||
case WebKeyConfigTypeRSA:
|
||||
config = new(WebKeyRSAConfig)
|
||||
case WebKeyConfigTypeECDSA:
|
||||
config = new(WebKeyECDSAConfig)
|
||||
case WebKeyConfigTypeED25519:
|
||||
config = new(WebKeyED25519Config)
|
||||
default:
|
||||
return nil, zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal")
|
||||
}
|
||||
if err = json.Unmarshal(data, config); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "CRYPT-waeR0N", "Errors.Internal")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type WebKeyRSAConfig struct {
|
||||
Bits RSABits
|
||||
Hasher RSAHasher
|
||||
}
|
||||
|
||||
func (c WebKeyRSAConfig) Alg() jose.SignatureAlgorithm {
|
||||
switch c.Hasher {
|
||||
case RSAHasherUnspecified:
|
||||
return ""
|
||||
case RSAHasherSHA256:
|
||||
return jose.RS256
|
||||
case RSAHasherSHA384:
|
||||
return jose.RS384
|
||||
case RSAHasherSHA512:
|
||||
return jose.RS512
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (WebKeyRSAConfig) Type() WebKeyConfigType {
|
||||
return WebKeyConfigTypeRSA
|
||||
}
|
||||
|
||||
func (c WebKeyRSAConfig) IsValid() error {
|
||||
if !c.Bits.IsARSABits() || c.Bits == RSABitsUnspecified {
|
||||
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config")
|
||||
}
|
||||
if !c.Hasher.IsARSAHasher() || c.Hasher == RSAHasherUnspecified {
|
||||
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-ODie7", "Errors.WebKey.Config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WebKeyECDSAConfig struct {
|
||||
Curve EllipticCurve
|
||||
}
|
||||
|
||||
func (c WebKeyECDSAConfig) Alg() jose.SignatureAlgorithm {
|
||||
switch c.Curve {
|
||||
case EllipticCurveUnspecified:
|
||||
return ""
|
||||
case EllipticCurveP256:
|
||||
return jose.ES256
|
||||
case EllipticCurveP384:
|
||||
return jose.ES384
|
||||
case EllipticCurveP512:
|
||||
return jose.ES512
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (WebKeyECDSAConfig) Type() WebKeyConfigType {
|
||||
return WebKeyConfigTypeECDSA
|
||||
}
|
||||
|
||||
func (c WebKeyECDSAConfig) IsValid() error {
|
||||
if !c.Curve.IsAEllipticCurve() || c.Curve == EllipticCurveUnspecified {
|
||||
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-Ii2ai", "Errors.WebKey.Config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c WebKeyECDSAConfig) GetCurve() elliptic.Curve {
|
||||
switch c.Curve {
|
||||
case EllipticCurveUnspecified:
|
||||
return nil
|
||||
case EllipticCurveP256:
|
||||
return elliptic.P256()
|
||||
case EllipticCurveP384:
|
||||
return elliptic.P384()
|
||||
case EllipticCurveP512:
|
||||
return elliptic.P521()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type WebKeyED25519Config struct{}
|
||||
|
||||
func (WebKeyED25519Config) Alg() jose.SignatureAlgorithm {
|
||||
return jose.EdDSA
|
||||
}
|
||||
|
||||
func (WebKeyED25519Config) Type() WebKeyConfigType {
|
||||
return WebKeyConfigTypeED25519
|
||||
}
|
||||
|
||||
func (WebKeyED25519Config) IsValid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateEncryptedWebKey(keyID string, alg EncryptionAlgorithm, genConfig WebKeyConfig) (encryptedPrivate *CryptoValue, public *jose.JSONWebKey, err error) {
|
||||
private, public, err := generateWebKey(keyID, genConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
encryptedPrivate, err = EncryptJSON(private, alg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return encryptedPrivate, public, nil
|
||||
}
|
||||
|
||||
func generateWebKey(keyID string, genConfig WebKeyConfig) (private, public *jose.JSONWebKey, err error) {
|
||||
if err = genConfig.IsValid(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var key any
|
||||
switch conf := genConfig.(type) {
|
||||
case *WebKeyRSAConfig:
|
||||
key, err = rsa.GenerateKey(rand.Reader, int(conf.Bits))
|
||||
case *WebKeyECDSAConfig:
|
||||
key, err = ecdsa.GenerateKey(conf.GetCurve(), rand.Reader)
|
||||
case *WebKeyED25519Config:
|
||||
_, key, err = ed25519.GenerateKey(rand.Reader)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
private = newJSONWebkey(key, keyID, genConfig.Alg())
|
||||
return private, gu.Ptr(private.Public()), err
|
||||
}
|
||||
|
||||
func newJSONWebkey(key any, keyID string, algorithm jose.SignatureAlgorithm) *jose.JSONWebKey {
|
||||
return &jose.JSONWebKey{
|
||||
Key: key,
|
||||
KeyID: keyID,
|
||||
Algorithm: string(algorithm),
|
||||
Use: KeyUsageSigning.String(),
|
||||
}
|
||||
}
|
269
internal/crypto/web_key_test.go
Normal file
269
internal/crypto/web_key_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestUnmarshalWebKeyConfig(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
configType WebKeyConfigType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantConfig WebKeyConfig
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
args: args{
|
||||
[]byte(`{}`),
|
||||
WebKeyConfigTypeUnspecified,
|
||||
},
|
||||
wantErr: zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "rsa",
|
||||
args: args{
|
||||
[]byte(`{"bits":"2048", "hasher":"sha256"}`),
|
||||
WebKeyConfigTypeRSA,
|
||||
},
|
||||
wantConfig: &WebKeyRSAConfig{
|
||||
Bits: RSABits2048,
|
||||
Hasher: RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ecdsa",
|
||||
args: args{
|
||||
[]byte(`{"curve":"p256"}`),
|
||||
WebKeyConfigTypeECDSA,
|
||||
},
|
||||
wantConfig: &WebKeyECDSAConfig{
|
||||
Curve: EllipticCurveP256,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ed25519",
|
||||
args: args{
|
||||
[]byte(`{}`),
|
||||
WebKeyConfigTypeED25519,
|
||||
},
|
||||
wantConfig: &WebKeyED25519Config{},
|
||||
},
|
||||
{
|
||||
name: "unknown type error",
|
||||
args: args{
|
||||
[]byte(`{"curve":0}`),
|
||||
99,
|
||||
},
|
||||
wantErr: zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
args: args{
|
||||
[]byte(`~~`),
|
||||
WebKeyConfigTypeED25519,
|
||||
},
|
||||
wantErr: zerrors.ThrowInternal(nil, "CRYPT-waeR0N", "Errors.Internal"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotConfig, err := UnmarshalWebKeyConfig(tt.args.data, tt.args.configType)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, gotConfig, tt.wantConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebKeyECDSAConfig_Alg(t *testing.T) {
|
||||
type fields struct {
|
||||
Curve EllipticCurve
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want jose.SignatureAlgorithm
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
fields: fields{
|
||||
Curve: EllipticCurveUnspecified,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "P256",
|
||||
fields: fields{
|
||||
Curve: EllipticCurveP256,
|
||||
},
|
||||
want: jose.ES256,
|
||||
},
|
||||
{
|
||||
name: "P384",
|
||||
fields: fields{
|
||||
Curve: EllipticCurveP384,
|
||||
},
|
||||
want: jose.ES384,
|
||||
},
|
||||
{
|
||||
name: "P512",
|
||||
fields: fields{
|
||||
Curve: EllipticCurveP512,
|
||||
},
|
||||
want: jose.ES512,
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
fields: fields{
|
||||
Curve: 99,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := WebKeyECDSAConfig{
|
||||
Curve: tt.fields.Curve,
|
||||
}
|
||||
got := c.Alg()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebKeyECDSAConfig_GetCurve(t *testing.T) {
|
||||
type fields struct {
|
||||
Curve EllipticCurve
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want elliptic.Curve
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
fields: fields{EllipticCurveUnspecified},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "P256",
|
||||
fields: fields{EllipticCurveP256},
|
||||
want: elliptic.P256(),
|
||||
},
|
||||
{
|
||||
name: "P384",
|
||||
fields: fields{EllipticCurveP384},
|
||||
want: elliptic.P384(),
|
||||
},
|
||||
{
|
||||
name: "P512",
|
||||
fields: fields{EllipticCurveP512},
|
||||
want: elliptic.P521(),
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
fields: fields{99},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := WebKeyECDSAConfig{
|
||||
Curve: tt.fields.Curve,
|
||||
}
|
||||
got := c.GetCurve()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateEncryptedWebKey(t *testing.T) {
|
||||
type args struct {
|
||||
keyID string
|
||||
genConfig WebKeyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
assertPrivate func(t *testing.T, got *jose.JSONWebKey)
|
||||
assertPublic func(t *testing.T, got *jose.JSONWebKey)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
keyID: "keyID",
|
||||
genConfig: &WebKeyRSAConfig{
|
||||
Bits: RSABitsUnspecified,
|
||||
Hasher: RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config"),
|
||||
},
|
||||
{
|
||||
name: "RSA",
|
||||
args: args{
|
||||
keyID: "keyID",
|
||||
genConfig: &WebKeyRSAConfig{
|
||||
Bits: RSABits2048,
|
||||
Hasher: RSAHasherSHA256,
|
||||
},
|
||||
},
|
||||
assertPrivate: assertJSONWebKey("keyID", "RS256", "sig", false),
|
||||
assertPublic: assertJSONWebKey("keyID", "RS256", "sig", true),
|
||||
},
|
||||
{
|
||||
name: "ECDSA",
|
||||
args: args{
|
||||
keyID: "keyID",
|
||||
genConfig: &WebKeyECDSAConfig{
|
||||
Curve: EllipticCurveP256,
|
||||
},
|
||||
},
|
||||
assertPrivate: assertJSONWebKey("keyID", "ES256", "sig", false),
|
||||
assertPublic: assertJSONWebKey("keyID", "ES256", "sig", true),
|
||||
},
|
||||
{
|
||||
name: "ED25519",
|
||||
args: args{
|
||||
keyID: "keyID",
|
||||
genConfig: &WebKeyED25519Config{},
|
||||
},
|
||||
assertPrivate: assertJSONWebKey("keyID", "EdDSA", "sig", false),
|
||||
assertPublic: assertJSONWebKey("keyID", "EdDSA", "sig", true),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPrivate, gotPublic, err := generateWebKey(tt.args.keyID, tt.args.genConfig)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.assertPrivate != nil {
|
||||
tt.assertPrivate(t, gotPrivate)
|
||||
}
|
||||
if tt.assertPublic != nil {
|
||||
tt.assertPublic(t, gotPublic)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertJSONWebKey(keyID, algorithm, use string, isPublic bool) func(t *testing.T, got *jose.JSONWebKey) {
|
||||
return func(t *testing.T, got *jose.JSONWebKey) {
|
||||
assert.NotNil(t, got)
|
||||
assert.NotNil(t, got.Key, "key")
|
||||
assert.Equal(t, keyID, got.KeyID, "keyID")
|
||||
assert.Equal(t, algorithm, got.Algorithm, "algorithm")
|
||||
assert.Equal(t, use, got.Use, "user")
|
||||
assert.Equal(t, isPublic, got.IsPublic(), "isPublic")
|
||||
}
|
||||
}
|
116
internal/crypto/webkeyconfigtype_enumer.go
Normal file
116
internal/crypto/webkeyconfigtype_enumer.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Code generated by "enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment"; DO NOT EDIT.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _WebKeyConfigTypeName = "RSAECDSAED25519"
|
||||
|
||||
var _WebKeyConfigTypeIndex = [...]uint8{0, 0, 3, 8, 15}
|
||||
|
||||
const _WebKeyConfigTypeLowerName = "rsaecdsaed25519"
|
||||
|
||||
func (i WebKeyConfigType) String() string {
|
||||
if i < 0 || i >= WebKeyConfigType(len(_WebKeyConfigTypeIndex)-1) {
|
||||
return fmt.Sprintf("WebKeyConfigType(%d)", i)
|
||||
}
|
||||
return _WebKeyConfigTypeName[_WebKeyConfigTypeIndex[i]:_WebKeyConfigTypeIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _WebKeyConfigTypeNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[WebKeyConfigTypeUnspecified-(0)]
|
||||
_ = x[WebKeyConfigTypeRSA-(1)]
|
||||
_ = x[WebKeyConfigTypeECDSA-(2)]
|
||||
_ = x[WebKeyConfigTypeED25519-(3)]
|
||||
}
|
||||
|
||||
var _WebKeyConfigTypeValues = []WebKeyConfigType{WebKeyConfigTypeUnspecified, WebKeyConfigTypeRSA, WebKeyConfigTypeECDSA, WebKeyConfigTypeED25519}
|
||||
|
||||
var _WebKeyConfigTypeNameToValueMap = map[string]WebKeyConfigType{
|
||||
_WebKeyConfigTypeName[0:0]: WebKeyConfigTypeUnspecified,
|
||||
_WebKeyConfigTypeLowerName[0:0]: WebKeyConfigTypeUnspecified,
|
||||
_WebKeyConfigTypeName[0:3]: WebKeyConfigTypeRSA,
|
||||
_WebKeyConfigTypeLowerName[0:3]: WebKeyConfigTypeRSA,
|
||||
_WebKeyConfigTypeName[3:8]: WebKeyConfigTypeECDSA,
|
||||
_WebKeyConfigTypeLowerName[3:8]: WebKeyConfigTypeECDSA,
|
||||
_WebKeyConfigTypeName[8:15]: WebKeyConfigTypeED25519,
|
||||
_WebKeyConfigTypeLowerName[8:15]: WebKeyConfigTypeED25519,
|
||||
}
|
||||
|
||||
var _WebKeyConfigTypeNames = []string{
|
||||
_WebKeyConfigTypeName[0:0],
|
||||
_WebKeyConfigTypeName[0:3],
|
||||
_WebKeyConfigTypeName[3:8],
|
||||
_WebKeyConfigTypeName[8:15],
|
||||
}
|
||||
|
||||
// WebKeyConfigTypeString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func WebKeyConfigTypeString(s string) (WebKeyConfigType, error) {
|
||||
if val, ok := _WebKeyConfigTypeNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _WebKeyConfigTypeNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to WebKeyConfigType values", s)
|
||||
}
|
||||
|
||||
// WebKeyConfigTypeValues returns all values of the enum
|
||||
func WebKeyConfigTypeValues() []WebKeyConfigType {
|
||||
return _WebKeyConfigTypeValues
|
||||
}
|
||||
|
||||
// WebKeyConfigTypeStrings returns a slice of all String values of the enum
|
||||
func WebKeyConfigTypeStrings() []string {
|
||||
strs := make([]string, len(_WebKeyConfigTypeNames))
|
||||
copy(strs, _WebKeyConfigTypeNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAWebKeyConfigType returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i WebKeyConfigType) IsAWebKeyConfigType() bool {
|
||||
for _, v := range _WebKeyConfigTypeValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for WebKeyConfigType
|
||||
func (i WebKeyConfigType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for WebKeyConfigType
|
||||
func (i *WebKeyConfigType) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("WebKeyConfigType should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = WebKeyConfigTypeString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for WebKeyConfigType
|
||||
func (i WebKeyConfigType) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for WebKeyConfigType
|
||||
func (i *WebKeyConfigType) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
*i, err = WebKeyConfigTypeString(string(text))
|
||||
return err
|
||||
}
|
@ -10,36 +10,13 @@ import (
|
||||
type KeyPair struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Usage KeyUsage
|
||||
Usage crypto.KeyUsage
|
||||
Algorithm string
|
||||
PrivateKey *Key
|
||||
PublicKey *Key
|
||||
Certificate *Key
|
||||
}
|
||||
|
||||
type KeyUsage int32
|
||||
|
||||
const (
|
||||
KeyUsageSigning KeyUsage = iota
|
||||
KeyUsageSAMLMetadataSigning
|
||||
KeyUsageSAMLResponseSinging
|
||||
KeyUsageSAMLCA
|
||||
)
|
||||
|
||||
func (u KeyUsage) String() string {
|
||||
switch u {
|
||||
case KeyUsageSigning:
|
||||
return "sig"
|
||||
case KeyUsageSAMLCA:
|
||||
return "saml_ca"
|
||||
case KeyUsageSAMLResponseSinging:
|
||||
return "saml_response_sig"
|
||||
case KeyUsageSAMLMetadataSigning:
|
||||
return "saml_metadata_sig"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
Key *crypto.CryptoValue
|
||||
Expiry time.Time
|
||||
|
11
internal/domain/web_key.go
Normal file
11
internal/domain/web_key.go
Normal file
@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
type WebKeyState int
|
||||
|
||||
const (
|
||||
WebKeyStateUnspecified WebKeyState = iota
|
||||
WebKeyStateInitial
|
||||
WebKeyStateActive
|
||||
WebKeyStateInactive
|
||||
WebKeyStateRemoved
|
||||
)
|
@ -48,15 +48,25 @@ func WithInstanceID(id string) aggregateOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// AggregateFromWriteModel maps the given WriteModel to an Aggregate
|
||||
// AggregateFromWriteModel maps the given WriteModel to an Aggregate.
|
||||
// Deprecated: Creates linter errors on missing context. Use [AggregateFromWriteModelCtx] instead.
|
||||
func AggregateFromWriteModel(
|
||||
wm *WriteModel,
|
||||
typ AggregateType,
|
||||
version Version,
|
||||
) *Aggregate {
|
||||
return AggregateFromWriteModelCtx(context.Background(), wm, typ, version)
|
||||
}
|
||||
|
||||
// AggregateFromWriteModelCtx maps the given WriteModel to an Aggregate.
|
||||
func AggregateFromWriteModelCtx(
|
||||
ctx context.Context,
|
||||
wm *WriteModel,
|
||||
typ AggregateType,
|
||||
version Version,
|
||||
) *Aggregate {
|
||||
return NewAggregate(
|
||||
// TODO: the linter complains if this function is called without passing a context
|
||||
context.Background(),
|
||||
ctx,
|
||||
wm.AggregateID,
|
||||
typ,
|
||||
version,
|
||||
|
@ -166,7 +166,7 @@ func (e *mockEvent) DataAsBytes() []byte {
|
||||
}
|
||||
payload, err := json.Marshal(e.Payload())
|
||||
if err != nil {
|
||||
panic("unable to unmarshal")
|
||||
panic(err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ const (
|
||||
KeyTokenExchange
|
||||
KeyActions
|
||||
KeyImprovedPerformance
|
||||
KeyWebKey
|
||||
)
|
||||
|
||||
//go:generate enumer -type Level -transform snake -trimprefix Level
|
||||
@ -37,6 +38,7 @@ type Features struct {
|
||||
TokenExchange bool `json:"token_exchange,omitempty"`
|
||||
Actions bool `json:"actions,omitempty"`
|
||||
ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"`
|
||||
WebKey bool `json:"web_key,omitempty"`
|
||||
}
|
||||
|
||||
type ImprovedPerformanceType int32
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key"
|
||||
|
||||
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133}
|
||||
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140}
|
||||
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key"
|
||||
|
||||
func (i Key) String() string {
|
||||
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
||||
@ -32,9 +32,10 @@ func _KeyNoOp() {
|
||||
_ = x[KeyTokenExchange-(5)]
|
||||
_ = x[KeyActions-(6)]
|
||||
_ = x[KeyImprovedPerformance-(7)]
|
||||
_ = x[KeyWebKey-(8)]
|
||||
}
|
||||
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance}
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey}
|
||||
|
||||
var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyName[0:11]: KeyUnspecified,
|
||||
@ -53,6 +54,8 @@ var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyLowerName[106:113]: KeyActions,
|
||||
_KeyName[113:133]: KeyImprovedPerformance,
|
||||
_KeyLowerName[113:133]: KeyImprovedPerformance,
|
||||
_KeyName[133:140]: KeyWebKey,
|
||||
_KeyLowerName[133:140]: KeyWebKey,
|
||||
}
|
||||
|
||||
var _KeyNames = []string{
|
||||
@ -64,6 +67,7 @@ var _KeyNames = []string{
|
||||
_KeyName[92:106],
|
||||
_KeyName[106:113],
|
||||
_KeyName[113:133],
|
||||
_KeyName[133:140],
|
||||
}
|
||||
|
||||
// KeyString retrieves an enum value from the enum constants string name.
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
|
||||
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
@ -63,10 +64,11 @@ type Client struct {
|
||||
OrgV2beta org_v2beta.OrganizationServiceClient
|
||||
OrgV2 org.OrganizationServiceClient
|
||||
System system.SystemServiceClient
|
||||
ActionV3 action.ZITADELActionsClient
|
||||
ActionV3Alpha action.ZITADELActionsClient
|
||||
FeatureV2beta feature_v2beta.FeatureServiceClient
|
||||
FeatureV2 feature.FeatureServiceClient
|
||||
UserSchemaV3 schema.UserSchemaServiceClient
|
||||
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
|
||||
}
|
||||
|
||||
func newClient(cc *grpc.ClientConn) Client {
|
||||
@ -86,10 +88,11 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
|
||||
OrgV2: org.NewOrganizationServiceClient(cc),
|
||||
System: system.NewSystemServiceClient(cc),
|
||||
ActionV3: action.NewZITADELActionsClient(cc),
|
||||
ActionV3Alpha: action.NewZITADELActionsClient(cc),
|
||||
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
|
||||
FeatureV2: feature.NewFeatureServiceClient(cc),
|
||||
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
|
||||
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
|
||||
}
|
||||
}
|
||||
|
||||
@ -649,20 +652,20 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint
|
||||
RestAsync: &action.SetRESTAsync{},
|
||||
}
|
||||
}
|
||||
target, err := s.Client.ActionV3.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
|
||||
target, err := s.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
|
||||
require.NoError(t, err)
|
||||
return target
|
||||
}
|
||||
|
||||
func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
|
||||
_, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
|
||||
_, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
|
||||
Condition: cond,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
|
||||
target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
|
||||
target, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
|
||||
Condition: cond,
|
||||
Execution: &action.Execution{
|
||||
Targets: targets,
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -66,7 +65,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage domain.KeyUsage) (certs *Certificates, err error) {
|
||||
func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage crypto.KeyUsage) (certs *Certificates, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@ -109,7 +108,7 @@ func Test_CertificatePrepares(t *testing.T) {
|
||||
sequence: 20211109,
|
||||
resourceOwner: "ro",
|
||||
algorithm: "",
|
||||
use: domain.KeyUsageSAMLMetadataSigning,
|
||||
use: crypto.KeyUsageSAMLMetadataSigning,
|
||||
},
|
||||
expiry: testNow,
|
||||
certificate: []byte("privateKey"),
|
||||
|
@ -16,6 +16,7 @@ type InstanceFeatures struct {
|
||||
TokenExchange FeatureSource[bool]
|
||||
Actions FeatureSource[bool]
|
||||
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
|
||||
WebKey FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
||||
|
@ -67,6 +67,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.InstanceTokenExchangeEventType,
|
||||
feature_v2.InstanceActionsEventType,
|
||||
feature_v2.InstanceImprovedPerformanceEventType,
|
||||
feature_v2.InstanceWebKeyEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@ -115,6 +116,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
|
||||
features.Actions.set(level, event.Value)
|
||||
case feature.KeyImprovedPerformance:
|
||||
features.ImprovedPerformance.set(level, event.Value)
|
||||
case feature.KeyWebKey:
|
||||
features.WebKey.set(level, event.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
@ -22,7 +21,7 @@ import (
|
||||
type Key interface {
|
||||
ID() string
|
||||
Algorithm() string
|
||||
Use() domain.KeyUsage
|
||||
Use() crypto.KeyUsage
|
||||
Sequence() uint64
|
||||
}
|
||||
|
||||
@ -55,7 +54,7 @@ type key struct {
|
||||
sequence uint64
|
||||
resourceOwner string
|
||||
algorithm string
|
||||
use domain.KeyUsage
|
||||
use crypto.KeyUsage
|
||||
}
|
||||
|
||||
func (k *key) ID() string {
|
||||
@ -66,7 +65,7 @@ func (k *key) Algorithm() string {
|
||||
return k.algorithm
|
||||
}
|
||||
|
||||
func (k *key) Use() domain.KeyUsage {
|
||||
func (k *key) Use() crypto.KeyUsage {
|
||||
return k.use
|
||||
}
|
||||
|
||||
@ -222,7 +221,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key
|
||||
query, args, err := stmt.Where(
|
||||
sq.And{
|
||||
sq.Eq{
|
||||
KeyColUse.identifier(): domain.KeyUsageSigning,
|
||||
KeyColUse.identifier(): crypto.KeyUsageSigning,
|
||||
KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
},
|
||||
sq.Gt{KeyPrivateColExpiry.identifier(): t},
|
||||
@ -358,7 +357,7 @@ type PublicKeyReadModel struct {
|
||||
Algorithm string
|
||||
Key *crypto.CryptoValue
|
||||
Expiry time.Time
|
||||
Usage domain.KeyUsage
|
||||
Usage crypto.KeyUsage
|
||||
}
|
||||
|
||||
func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel {
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -131,7 +130,7 @@ func Test_KeyPrepares(t *testing.T) {
|
||||
sequence: 20211109,
|
||||
resourceOwner: "ro",
|
||||
algorithm: "RS256",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
},
|
||||
expiry: testNow,
|
||||
publicKey: &rsa.PublicKey{
|
||||
@ -212,7 +211,7 @@ func Test_KeyPrepares(t *testing.T) {
|
||||
sequence: 20211109,
|
||||
resourceOwner: "ro",
|
||||
algorithm: "RS256",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
},
|
||||
expiry: testNow,
|
||||
privateKey: &crypto.CryptoValue{
|
||||
@ -306,7 +305,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
|
||||
InstanceID: "instanceID",
|
||||
Version: key_repo.AggregateVersion,
|
||||
},
|
||||
domain.KeyUsageSigning, "alg",
|
||||
crypto.KeyUsageSigning, "alg",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
@ -345,7 +344,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
|
||||
InstanceID: "instanceID",
|
||||
Version: key_repo.AggregateVersion,
|
||||
},
|
||||
domain.KeyUsageSigning, "alg",
|
||||
crypto.KeyUsageSigning, "alg",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
@ -385,7 +384,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
|
||||
InstanceID: "instanceID",
|
||||
Version: key_repo.AggregateVersion,
|
||||
},
|
||||
domain.KeyUsageSigning, "alg",
|
||||
crypto.KeyUsageSigning, "alg",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
@ -416,7 +415,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
|
||||
id: "keyID",
|
||||
resourceOwner: "instanceID",
|
||||
algorithm: "alg",
|
||||
use: domain.KeyUsageSigning,
|
||||
use: crypto.KeyUsageSigning,
|
||||
},
|
||||
expiry: future,
|
||||
publicKey: func() *rsa.PublicKey {
|
||||
|
@ -88,6 +88,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
Event: feature_v2.InstanceImprovedPerformanceEventType,
|
||||
Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceWebKeyEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
@ -33,7 +32,7 @@ func TestKeyProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
keypair.AddedEventType,
|
||||
keypair.AggregateType,
|
||||
keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(time.Hour)),
|
||||
keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(time.Hour)),
|
||||
), keypair.AddedEventMapper),
|
||||
},
|
||||
reduce: (&keyProjection{encryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceKeyPairAdded,
|
||||
@ -52,7 +51,7 @@ func TestKeyProjection_reduces(t *testing.T) {
|
||||
"instance-id",
|
||||
uint64(15),
|
||||
"algorithm",
|
||||
domain.KeyUsageSigning,
|
||||
crypto.KeyUsageSigning,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -89,7 +88,7 @@ func TestKeyProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
keypair.AddedEventType,
|
||||
keypair.AggregateType,
|
||||
keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(-time.Hour)),
|
||||
keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(-time.Hour)),
|
||||
), keypair.AddedEventMapper),
|
||||
},
|
||||
reduce: (&keyProjection{}).reduceKeyPairAdded,
|
||||
@ -132,7 +131,7 @@ func TestKeyProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
keypair.AddedCertificateEventType,
|
||||
keypair.AggregateType,
|
||||
certificateAddedEventData(domain.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)),
|
||||
certificateAddedEventData(crypto.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)),
|
||||
), keypair.AddedCertificateEventMapper),
|
||||
},
|
||||
reduce: (&keyProjection{certEncryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceCertificateAdded,
|
||||
@ -170,10 +169,10 @@ func TestKeyProjection_reduces(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func keypairAddedEventData(usage domain.KeyUsage, t time.Time) []byte {
|
||||
func keypairAddedEventData(usage crypto.KeyUsage, t time.Time) []byte {
|
||||
return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "privateKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}, "publicKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHVibGljS2V5"}, "expiry": "` + t.Format(time.RFC3339) + `"}}`)
|
||||
}
|
||||
|
||||
func certificateAddedEventData(usage domain.KeyUsage, t time.Time) []byte {
|
||||
func certificateAddedEventData(usage crypto.KeyUsage, t time.Time) []byte {
|
||||
return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "certificate": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}}`)
|
||||
}
|
||||
|
@ -78,6 +78,7 @@ var (
|
||||
TargetProjection *handler.Handler
|
||||
ExecutionProjection *handler.Handler
|
||||
UserSchemaProjection *handler.Handler
|
||||
WebKeyProjection *handler.Handler
|
||||
|
||||
ProjectGrantFields *handler.FieldHandler
|
||||
OrgDomainVerifiedFields *handler.FieldHandler
|
||||
@ -163,6 +164,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
||||
TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"]))
|
||||
ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"]))
|
||||
UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"]))
|
||||
WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"]))
|
||||
|
||||
ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant]))
|
||||
OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified]))
|
||||
@ -292,5 +294,6 @@ func newProjectionsList() {
|
||||
TargetProjection,
|
||||
ExecutionProjection,
|
||||
UserSchemaProjection,
|
||||
WebKeyProjection,
|
||||
}
|
||||
}
|
||||
|
165
internal/query/projection/web_key.go
Normal file
165
internal/query/projection/web_key.go
Normal file
@ -0,0 +1,165 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
WebKeyTable = "projections.web_keys"
|
||||
|
||||
WebKeyInstanceIDCol = "instance_id"
|
||||
WebKeyKeyIDCol = "key_id"
|
||||
WebKeyCreationDateCol = "creation_date"
|
||||
WebKeyChangeDateCol = "change_date"
|
||||
WebKeySequenceCol = "sequence"
|
||||
WebKeyStateCol = "state"
|
||||
WebKeyPrivateKeyCol = "private_key"
|
||||
WebKeyPublicKeyCol = "public_key"
|
||||
WebKeyConfigCol = "config"
|
||||
WebKeyConfigTypeCol = "config_type"
|
||||
)
|
||||
|
||||
type webKeyProjection struct{}
|
||||
|
||||
func newWebKeyProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(webKeyProjection))
|
||||
}
|
||||
|
||||
func (*webKeyProjection) Name() string {
|
||||
return WebKeyTable
|
||||
}
|
||||
|
||||
func (*webKeyProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(
|
||||
handler.NewTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(WebKeyInstanceIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(WebKeyKeyIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(WebKeyCreationDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(WebKeyChangeDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(WebKeySequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(WebKeyStateCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(WebKeyPrivateKeyCol, handler.ColumnTypeJSONB),
|
||||
handler.NewColumn(WebKeyPublicKeyCol, handler.ColumnTypeJSONB),
|
||||
handler.NewColumn(WebKeyConfigCol, handler.ColumnTypeJSONB),
|
||||
handler.NewColumn(WebKeyConfigTypeCol, handler.ColumnTypeInt64),
|
||||
},
|
||||
handler.NewPrimaryKey(WebKeyInstanceIDCol, WebKeyKeyIDCol),
|
||||
|
||||
// index to find the current active private key for an instance.
|
||||
handler.WithIndex(handler.NewIndex(
|
||||
"web_key_state",
|
||||
[]string{WebKeyInstanceIDCol, WebKeyStateCol},
|
||||
handler.WithInclude(
|
||||
WebKeyPrivateKeyCol,
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *webKeyProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: webkey.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: webkey.AddedEventType,
|
||||
Reduce: p.reduceWebKeyAdded,
|
||||
},
|
||||
{
|
||||
Event: webkey.ActivatedEventType,
|
||||
Reduce: p.reduceWebKeyActivated,
|
||||
},
|
||||
{
|
||||
Event: webkey.DeactivatedEventType,
|
||||
Reduce: p.reduceWebKeyDeactivated,
|
||||
},
|
||||
{
|
||||
Event: webkey.RemovedEventType,
|
||||
Reduce: p.reduceWebKeyRemoved,
|
||||
},
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(WebKeyInstanceIDCol),
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (p *webKeyProjection) reduceWebKeyAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*webkey.AddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-jei2K", "reduce.wrong.event.type %s", webkey.AddedEventType)
|
||||
}
|
||||
return handler.NewCreateStatement(e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(WebKeyInstanceIDCol, e.Agg.InstanceID),
|
||||
handler.NewCol(WebKeyKeyIDCol, e.Agg.ID),
|
||||
handler.NewCol(WebKeyCreationDateCol, e.CreationDate()),
|
||||
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(WebKeySequenceCol, e.Sequence()),
|
||||
handler.NewCol(WebKeyStateCol, domain.WebKeyStateInitial),
|
||||
handler.NewCol(WebKeyPrivateKeyCol, e.PrivateKey),
|
||||
handler.NewCol(WebKeyPublicKeyCol, e.PublicKey),
|
||||
handler.NewCol(WebKeyConfigCol, e.Config),
|
||||
handler.NewCol(WebKeyConfigTypeCol, e.ConfigType),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *webKeyProjection) reduceWebKeyActivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*webkey.ActivatedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-iiQu2", "reduce.wrong.event.type %s", webkey.ActivatedEventType)
|
||||
}
|
||||
return handler.NewUpdateStatement(e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(WebKeySequenceCol, e.Sequence()),
|
||||
handler.NewCol(WebKeyStateCol, domain.WebKeyStateActive),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
|
||||
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *webKeyProjection) reduceWebKeyDeactivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*webkey.DeactivatedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-zei3E", "reduce.wrong.event.type %s", webkey.DeactivatedEventType)
|
||||
}
|
||||
return handler.NewUpdateStatement(e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(WebKeySequenceCol, e.Sequence()),
|
||||
handler.NewCol(WebKeyStateCol, domain.WebKeyStateInactive),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
|
||||
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *webKeyProjection) reduceWebKeyRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*webkey.RemovedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Zei6f", "reduce.wrong.event.type %s", webkey.RemovedEventType)
|
||||
}
|
||||
return handler.NewDeleteStatement(e,
|
||||
[]handler.Condition{
|
||||
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
|
||||
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
|
||||
},
|
||||
), nil
|
||||
}
|
154
internal/query/web_key.go
Normal file
154
internal/query/web_key.go
Normal file
@ -0,0 +1,154 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed web_key_by_state.sql
|
||||
webKeyByStateQuery string
|
||||
//go:embed web_key_list.sql
|
||||
webKeyListQuery string
|
||||
//go:embed web_key_public_keys.sql
|
||||
webKeyPublicKeysQuery string
|
||||
)
|
||||
|
||||
// GetPublicWebKeyByID gets a public key by it's keyID directly from the eventstore.
|
||||
func (q *Queries) GetPublicWebKeyByID(ctx context.Context, keyID string) (webKey *jose.JSONWebKey, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
model := NewWebKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model.State == domain.WebKeyStateUnspecified || model.State == domain.WebKeyStateRemoved {
|
||||
return nil, zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound")
|
||||
}
|
||||
return model.PublicKey, nil
|
||||
}
|
||||
|
||||
// GetActiveSigningWebKey gets the current active signing key from the web_keys projection.
|
||||
// The active signing key is eventual consistent.
|
||||
func (q *Queries) GetActiveSigningWebKey(ctx context.Context) (webKey *jose.JSONWebKey, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var keyValue *crypto.CryptoValue
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
return row.Scan(&keyValue)
|
||||
},
|
||||
webKeyByStateQuery,
|
||||
authz.GetInstance(ctx).InstanceID(),
|
||||
domain.WebKeyStateActive,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Opoh7", "Errors.WebKey.NoActive")
|
||||
}
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Shoo0", "Errors.Internal")
|
||||
}
|
||||
if err = crypto.DecryptJSON(keyValue, &webKey, q.keyEncryptionAlgorithm); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Iuk0s", "Errors.Internal")
|
||||
}
|
||||
return webKey, nil
|
||||
}
|
||||
|
||||
type WebKeyDetails struct {
|
||||
KeyID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence int64
|
||||
State domain.WebKeyState
|
||||
Config crypto.WebKeyConfig
|
||||
}
|
||||
|
||||
type WebKeyList struct {
|
||||
Keys []WebKeyDetails
|
||||
}
|
||||
|
||||
// ListWebKeys gets a list of [WebKeyDetails] for the complete instance from the web_keys projection.
|
||||
// The list is eventual consistent.
|
||||
func (q *Queries) ListWebKeys(ctx context.Context) (list []WebKeyDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
||||
for rows.Next() {
|
||||
var (
|
||||
configData []byte
|
||||
configType crypto.WebKeyConfigType
|
||||
)
|
||||
var details WebKeyDetails
|
||||
if err = rows.Scan(
|
||||
&details.KeyID,
|
||||
&details.CreationDate,
|
||||
&details.ChangeDate,
|
||||
&details.Sequence,
|
||||
&details.State,
|
||||
&configData,
|
||||
&configType,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
details.Config, err = crypto.UnmarshalWebKeyConfig(configData, configType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list = append(list, details)
|
||||
}
|
||||
return rows.Err()
|
||||
},
|
||||
webKeyListQuery,
|
||||
authz.GetInstance(ctx).InstanceID(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetWebKeySet gets a JSON Web Key set from the web_keys projection.
|
||||
// The set contains all existing public keys for the instance.
|
||||
// The set is eventual consistent.
|
||||
func (q *Queries) GetWebKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var keys []jose.JSONWebKey
|
||||
|
||||
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
||||
for rows.Next() {
|
||||
var webKeyData []byte
|
||||
if err = rows.Scan(&webKeyData); err != nil {
|
||||
return err
|
||||
}
|
||||
var webKey jose.JSONWebKey
|
||||
if err = json.Unmarshal(webKeyData, &webKey); err != nil {
|
||||
return err
|
||||
}
|
||||
keys = append(keys, webKey)
|
||||
}
|
||||
return rows.Err()
|
||||
},
|
||||
webKeyPublicKeysQuery,
|
||||
authz.GetInstance(ctx).InstanceID(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Eeng7", "Errors.Internal")
|
||||
}
|
||||
return &jose.JSONWebKeySet{Keys: keys}, nil
|
||||
}
|
5
internal/query/web_key_by_state.sql
Normal file
5
internal/query/web_key_by_state.sql
Normal file
@ -0,0 +1,5 @@
|
||||
select private_key
|
||||
from projections.web_keys
|
||||
where instance_id = $1
|
||||
and state = $2
|
||||
limit 1;
|
4
internal/query/web_key_list.sql
Normal file
4
internal/query/web_key_list.sql
Normal file
@ -0,0 +1,4 @@
|
||||
select key_id, creation_date, change_date, sequence, state, config, config_type
|
||||
from projections.web_keys
|
||||
where instance_id = $1
|
||||
order by creation_date asc;
|
74
internal/query/web_key_model.go
Normal file
74
internal/query/web_key_model.go
Normal file
@ -0,0 +1,74 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
)
|
||||
|
||||
type WebKeyReadModel struct {
|
||||
eventstore.ReadModel
|
||||
State domain.WebKeyState
|
||||
PrivateKey *crypto.CryptoValue
|
||||
PublicKey *jose.JSONWebKey
|
||||
Config crypto.WebKeyConfig
|
||||
}
|
||||
|
||||
func NewWebKeyReadModel(keyID, resourceOwner string) *WebKeyReadModel {
|
||||
return &WebKeyReadModel{
|
||||
ReadModel: eventstore.ReadModel{
|
||||
AggregateID: keyID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *WebKeyReadModel) AppendEvents(events ...eventstore.Event) {
|
||||
wm.ReadModel.AppendEvents(events...)
|
||||
}
|
||||
|
||||
func (wm *WebKeyReadModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *webkey.AddedEvent:
|
||||
if err := wm.reduceAdded(e); err != nil {
|
||||
return err
|
||||
}
|
||||
case *webkey.ActivatedEvent:
|
||||
wm.State = domain.WebKeyStateActive
|
||||
case *webkey.DeactivatedEvent:
|
||||
wm.State = domain.WebKeyStateInactive
|
||||
case *webkey.RemovedEvent:
|
||||
wm.State = domain.WebKeyStateRemoved
|
||||
wm.PrivateKey = nil
|
||||
wm.PublicKey = nil
|
||||
}
|
||||
}
|
||||
return wm.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *WebKeyReadModel) reduceAdded(e *webkey.AddedEvent) (err error) {
|
||||
wm.State = domain.WebKeyStateInitial
|
||||
wm.PrivateKey = e.PrivateKey
|
||||
wm.PublicKey = e.PublicKey
|
||||
wm.Config, err = crypto.UnmarshalWebKeyConfig(e.Config, e.ConfigType)
|
||||
return err
|
||||
}
|
||||
|
||||
func (wm *WebKeyReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(webkey.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
webkey.AddedEventType,
|
||||
webkey.ActivatedEventType,
|
||||
webkey.DeactivatedEventType,
|
||||
webkey.RemovedEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
3
internal/query/web_key_public_keys.sql
Normal file
3
internal/query/web_key_public_keys.sql
Normal file
@ -0,0 +1,3 @@
|
||||
select public_key
|
||||
from projections.web_keys
|
||||
where instance_id = $1;
|
382
internal/query/web_key_test.go
Normal file
382
internal/query/web_key_test.go
Normal file
@ -0,0 +1,382 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/webkey"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestQueries_GetPublicWebKeyByID(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
keyID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *jose.JSONWebKey
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "removed, not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(webkey.NewRemovedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
|
||||
webkey.NewAggregate("key1", "instance1"),
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
&jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
},
|
||||
&crypto.WebKeyECDSAConfig{
|
||||
Curve: crypto.EllipticCurveP384,
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{"key1"},
|
||||
want: &jose.JSONWebKey{
|
||||
Key: &key.PublicKey,
|
||||
KeyID: "key1",
|
||||
Algorithm: string(jose.ES384),
|
||||
Use: crypto.KeyUsageSigning.String(),
|
||||
Certificates: []*x509.Certificate{},
|
||||
CertificateThumbprintSHA1: []byte{},
|
||||
CertificateThumbprintSHA256: []byte{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := q.GetPublicWebKeyByID(ctx, tt.args.keyID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewWebkeyAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
privateKey *crypto.CryptoValue,
|
||||
publicKey *jose.JSONWebKey,
|
||||
config crypto.WebKeyConfig) *webkey.AddedEvent {
|
||||
event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func TestQueries_GetActiveSigningWebKey(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
expQuery := regexp.QuoteMeta(webKeyByStateQuery)
|
||||
queryArgs := []driver.Value{"instance1", domain.WebKeyStateActive}
|
||||
cols := []string{"private_key"}
|
||||
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
encryptedPrivate, _, err := crypto.GenerateEncryptedWebKey("key1", alg, &crypto.WebKeyED25519Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var expectedWebKey *jose.JSONWebKey
|
||||
err = crypto.DecryptJSON(encryptedPrivate, &expectedWebKey, alg)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mock sqlExpectation
|
||||
want *jose.JSONWebKey
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "no active error",
|
||||
mock: mockQueryErr(expQuery, sql.ErrNoRows, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(sql.ErrNoRows, "QUERY-Opoh7", "Errors.WebKey.NoActive"),
|
||||
},
|
||||
{
|
||||
name: "internal error",
|
||||
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Shoo0", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "invalid crypto value error",
|
||||
mock: mockQuery(expQuery, cols, []driver.Value{&crypto.CryptoValue{}}, queryArgs...),
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPT-Nx7XlT", "value was encrypted with a different key"),
|
||||
},
|
||||
{
|
||||
name: "found, ok",
|
||||
mock: mockQuery(expQuery, cols, []driver.Value{encryptedPrivate}, queryArgs...),
|
||||
want: expectedWebKey,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
execMock(t, tt.mock, func(db *sql.DB) {
|
||||
q := &Queries{
|
||||
client: &database.DB{
|
||||
DB: db,
|
||||
Database: &prepareDB{},
|
||||
},
|
||||
keyEncryptionAlgorithm: alg,
|
||||
}
|
||||
got, err := q.GetActiveSigningWebKey(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueries_ListWebKeys(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
expQuery := regexp.QuoteMeta(webKeyListQuery)
|
||||
queryArgs := []driver.Value{"instance1"}
|
||||
cols := []string{"key_id", "creation_date", "change_date", "sequence", "state", "config", "config_type"}
|
||||
|
||||
webKeyConfig := &crypto.WebKeyRSAConfig{
|
||||
Bits: crypto.RSABits4096,
|
||||
Hasher: crypto.RSAHasherSHA512,
|
||||
}
|
||||
webKeyConfigJSON, err := json.Marshal(webKeyConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mock sqlExpectation
|
||||
want []WebKeyDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "internal error",
|
||||
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ohl3A", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "invalid json error",
|
||||
mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{
|
||||
{
|
||||
"key1",
|
||||
time.Unix(1, 2),
|
||||
time.Unix(3, 4),
|
||||
1,
|
||||
domain.WebKeyStateActive,
|
||||
"~~~~~",
|
||||
crypto.WebKeyConfigTypeRSA,
|
||||
},
|
||||
}, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
mock: mockQueries(expQuery, cols, [][]driver.Value{
|
||||
{
|
||||
"key1",
|
||||
time.Unix(1, 2),
|
||||
time.Unix(3, 4),
|
||||
1,
|
||||
domain.WebKeyStateActive,
|
||||
webKeyConfigJSON,
|
||||
crypto.WebKeyConfigTypeRSA,
|
||||
},
|
||||
{
|
||||
"key2",
|
||||
time.Unix(5, 6),
|
||||
time.Unix(7, 8),
|
||||
2,
|
||||
domain.WebKeyStateInitial,
|
||||
webKeyConfigJSON,
|
||||
crypto.WebKeyConfigTypeRSA,
|
||||
},
|
||||
}, queryArgs...),
|
||||
want: []WebKeyDetails{
|
||||
{
|
||||
KeyID: "key1",
|
||||
CreationDate: time.Unix(1, 2),
|
||||
ChangeDate: time.Unix(3, 4),
|
||||
Sequence: 1,
|
||||
State: domain.WebKeyStateActive,
|
||||
Config: webKeyConfig,
|
||||
},
|
||||
{
|
||||
KeyID: "key2",
|
||||
CreationDate: time.Unix(5, 6),
|
||||
ChangeDate: time.Unix(7, 8),
|
||||
Sequence: 2,
|
||||
State: domain.WebKeyStateInitial,
|
||||
Config: webKeyConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
execMock(t, tt.mock, func(db *sql.DB) {
|
||||
q := &Queries{
|
||||
client: &database.DB{
|
||||
DB: db,
|
||||
Database: &prepareDB{},
|
||||
},
|
||||
}
|
||||
got, err := q.ListWebKeys(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueries_GetWebKeySet(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
expQuery := regexp.QuoteMeta(webKeyPublicKeysQuery)
|
||||
queryArgs := []driver.Value{"instance1"}
|
||||
cols := []string{"public_key"}
|
||||
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
conf := &crypto.WebKeyED25519Config{}
|
||||
expectedKeySet := &jose.JSONWebKeySet{
|
||||
Keys: make([]jose.JSONWebKey, 3),
|
||||
}
|
||||
expectedRows := make([][]driver.Value, 3)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
_, pubKey, err := crypto.GenerateEncryptedWebKey(strconv.Itoa(i), alg, conf)
|
||||
require.NoError(t, err)
|
||||
pubKeyJSON, err := json.Marshal(pubKey)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(pubKeyJSON, &expectedKeySet.Keys[i])
|
||||
require.NoError(t, err)
|
||||
expectedRows[i] = []driver.Value{pubKeyJSON}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mock sqlExpectation
|
||||
want *jose.JSONWebKeySet
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "internal error",
|
||||
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Eeng7", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "invalid json error",
|
||||
mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{{"~~~"}}, queryArgs...),
|
||||
wantErr: zerrors.ThrowInternal(nil, "QUERY-Eeng7", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
mock: mockQueries(expQuery, cols, expectedRows, queryArgs...),
|
||||
want: expectedKeySet,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
execMock(t, tt.mock, func(db *sql.DB) {
|
||||
q := &Queries{
|
||||
client: &database.DB{
|
||||
DB: db,
|
||||
Database: &prepareDB{},
|
||||
},
|
||||
}
|
||||
got, err := q.GetWebKeySet(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -23,4 +23,5 @@ func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ var (
|
||||
InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange)
|
||||
InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions)
|
||||
InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance)
|
||||
InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey)
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
@ -18,7 +17,7 @@ const (
|
||||
type AddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Usage domain.KeyUsage `json:"usage"`
|
||||
Usage crypto.KeyUsage `json:"usage"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
PrivateKey *Key `json:"privateKey"`
|
||||
PublicKey *Key `json:"publicKey"`
|
||||
@ -40,7 +39,7 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
func NewAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
usage domain.KeyUsage,
|
||||
usage crypto.KeyUsage,
|
||||
algorithm string,
|
||||
privateCrypto,
|
||||
publicCrypto *crypto.CryptoValue,
|
||||
|
25
internal/repository/webkey/aggregate.go
Normal file
25
internal/repository/webkey/aggregate.go
Normal file
@ -0,0 +1,25 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "web_key"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *eventstore.Aggregate {
|
||||
return &eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
|
||||
return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion)
|
||||
}
|
12
internal/repository/webkey/eventstore.go
Normal file
12
internal/repository/webkey/eventstore.go
Normal file
@ -0,0 +1,12 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, ActivatedEventType, eventstore.GenericEventMapper[ActivatedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, DeactivatedEventType, eventstore.GenericEventMapper[DeactivatedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
|
||||
}
|
160
internal/repository/webkey/webkey.go
Normal file
160
internal/repository/webkey/webkey.go
Normal file
@ -0,0 +1,160 @@
|
||||
package webkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
UniqueWebKeyType = "web_key"
|
||||
)
|
||||
|
||||
const (
|
||||
eventTypePrefix = eventstore.EventType("web_key.")
|
||||
AddedEventType = eventTypePrefix + "added"
|
||||
ActivatedEventType = eventTypePrefix + "activated"
|
||||
DeactivatedEventType = eventTypePrefix + "deactivated"
|
||||
RemovedEventType = eventTypePrefix + "removed"
|
||||
)
|
||||
|
||||
type AddedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
PrivateKey *crypto.CryptoValue `json:"privateKey"`
|
||||
PublicKey *jose.JSONWebKey `json:"publicKey"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
ConfigType crypto.WebKeyConfigType `json:"configType"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return []*eventstore.UniqueConstraint{
|
||||
eventstore.NewAddEventUniqueConstraint(UniqueWebKeyType, e.Agg.ID, "Errors.WebKey.Duplicate"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AddedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||
e.BaseEvent = base
|
||||
}
|
||||
|
||||
func NewAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
privateKey *crypto.CryptoValue,
|
||||
publicKey *jose.JSONWebKey,
|
||||
config crypto.WebKeyConfig,
|
||||
) (*AddedEvent, error) {
|
||||
configJson, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "WEBKEY-IY9fa", "Errors.Internal")
|
||||
}
|
||||
return &AddedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
AddedEventType,
|
||||
),
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
Config: configJson,
|
||||
ConfigType: config.Type(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ActivatedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *ActivatedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ActivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||
e.BaseEvent = base
|
||||
}
|
||||
|
||||
func NewActivatedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *ActivatedEvent {
|
||||
return &ActivatedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
ActivatedEventType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type DeactivatedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *DeactivatedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *DeactivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||
e.BaseEvent = base
|
||||
}
|
||||
|
||||
func NewDeactivatedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *DeactivatedEvent {
|
||||
return &DeactivatedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
DeactivatedEventType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type RemovedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *RemovedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return []*eventstore.UniqueConstraint{
|
||||
eventstore.NewRemoveUniqueConstraint(UniqueWebKeyType, e.Agg.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *RemovedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||
e.BaseEvent = base
|
||||
}
|
||||
|
||||
func NewRemovedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *RemovedEvent {
|
||||
return &RemovedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
RemovedEventType,
|
||||
),
|
||||
}
|
||||
}
|
@ -603,6 +603,13 @@ Errors:
|
||||
NotForAPI: Имитирани токени не са разрешени за API
|
||||
Impersonation:
|
||||
PolicyDisabled: Имитирането е деактивирано в политиката за сигурност на екземпляра
|
||||
WebKey:
|
||||
ActiveDelete: Не може да се изтрие активен уеб ключ
|
||||
Config: Невалидна конфигурация на уеб ключ
|
||||
Duplicate: ID на уеб ключ не е уникален
|
||||
FeatureDisabled: Ключовата уеб функция е деактивирана
|
||||
NoActive: Не е намерен активен уеб ключ
|
||||
NotFound: Уеб ключът не е намерен
|
||||
|
||||
AggregateTypes:
|
||||
action: Действие
|
||||
@ -626,6 +633,7 @@ AggregateTypes:
|
||||
restrictions: Ограничения
|
||||
system: Система
|
||||
session: Сесия
|
||||
web_key: Уеб ключ
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1342,6 +1350,12 @@ EventTypes:
|
||||
deactivated: Потребителската схема е деактивирана
|
||||
reactivated: Потребителската схема е активирана отново
|
||||
deleted: Потребителската схема е изтрита
|
||||
web_key:
|
||||
added: Добавен уеб ключ
|
||||
activated: Уеб ключът е активиран
|
||||
deactivated: Уеб ключът е деактивиран
|
||||
removed: Уеб ключът е премахнат
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
UnsupportedVersion: Вашата OIDC версия не се поддържа
|
||||
|
@ -584,6 +584,13 @@ Errors:
|
||||
NotForAPI: Zosobněné tokeny nejsou pro API povoleny
|
||||
Impersonation:
|
||||
PolicyDisabled: Zosobnění je zakázáno v zásadách zabezpečení instance
|
||||
WebKey:
|
||||
ActiveDelete: Aktivní webový klíč nelze smazat
|
||||
Config: Neplatná konfigurace webového klíče
|
||||
Duplicate: ID webového klíče není jedinečné
|
||||
FeatureDisabled: Funkce webového klíče je zakázána
|
||||
NoActive: Nebyl nalezen žádný aktivní webový klíč
|
||||
NotFound: Webový klíč nebyl nalezen
|
||||
|
||||
AggregateTypes:
|
||||
action: Akce
|
||||
@ -607,6 +614,7 @@ AggregateTypes:
|
||||
restrictions: Omezení
|
||||
system: Systém
|
||||
session: Sezení
|
||||
web_key: Webový klíč
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1308,6 +1316,11 @@ EventTypes:
|
||||
deactivated: Uživatelské schéma deaktivováno
|
||||
reactivated: Uživatelské schéma bylo znovu aktivováno
|
||||
deleted: Uživatelské schéma bylo smazáno
|
||||
web_key:
|
||||
added: Přidán webový klíč
|
||||
activated: Web Key aktivován
|
||||
deactivated: Web Key deaktivován
|
||||
removed: Odstraňte webový klíč
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Imitierte Token sind für die API nicht zulässig
|
||||
Impersonation:
|
||||
PolicyDisabled: Der Identitätswechsel ist in der Sicherheitsrichtlinie der Instanz deaktiviert
|
||||
WebKey:
|
||||
ActiveDelete: Aktiver Webschlüssel kann nicht gelöscht werden
|
||||
Config: Ungültige Webschlüsselkonfiguration
|
||||
Duplicate: Webschlüssel-ID nicht eindeutig
|
||||
FeatureDisabled: Webschlüsselfunktion deaktiviert
|
||||
NoActive: Kein aktiver Webschlüssel gefunden
|
||||
NotFound: Webschlüssel nicht gefunden
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Restriktionen
|
||||
system: System
|
||||
session: Session
|
||||
web_key: Webschlüssel
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1310,6 +1318,11 @@ EventTypes:
|
||||
deactivated: Benutzerschema deaktiviert
|
||||
reactivated: Benutzerschema reaktiviert
|
||||
deleted: Benutzerschema gelöscht
|
||||
web_key:
|
||||
added: Web Key hinzugefügt
|
||||
activated: Web Key aktiviert
|
||||
deactivated: Web Key deaktiviert
|
||||
removed: Web Key entfernen
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Impersonated tokens not allowed for API
|
||||
Impersonation:
|
||||
PolicyDisabled: Impersonation is disabled in the instance security policy
|
||||
WebKey:
|
||||
ActiveDelete: Cannot delete active web key
|
||||
Config: Invalid web key config
|
||||
Duplicate: Web key ID not unique
|
||||
FeatureDisabled: Web key feature disabled
|
||||
NoActive: No active web key found
|
||||
NotFound: Web key not found
|
||||
|
||||
|
||||
AggregateTypes:
|
||||
@ -610,6 +617,7 @@ AggregateTypes:
|
||||
restrictions: Restrictions
|
||||
system: System
|
||||
session: Session
|
||||
web_key: Web Key
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1311,6 +1319,11 @@ EventTypes:
|
||||
deactivated: User Schema deactivated
|
||||
reactivated: User Schema reactivated
|
||||
deleted: User Schema deleted
|
||||
web_key:
|
||||
added: Web Key added
|
||||
activated: Web Key activated
|
||||
deactivated: Web Key deactivated
|
||||
removed: Web Key removed
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Tokens suplantados no permitidos para API
|
||||
Impersonation:
|
||||
PolicyDisabled: La suplantación está deshabilitada en la política de seguridad de la instancia.
|
||||
WebKey:
|
||||
ActiveDelete: No se puede eliminar la clave web activa
|
||||
Config: Configuración de clave web no válida
|
||||
Duplicate: ID de clave web no único
|
||||
FeatureDisabled: Función de clave web deshabilitada
|
||||
NoActive: No se encontró ninguna clave web activa
|
||||
NotFound: Clave web no encontrada
|
||||
|
||||
AggregateTypes:
|
||||
action: Acción
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Restricciones
|
||||
system: Sistema
|
||||
session: Sesión
|
||||
web_key: Clave web
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1310,6 +1318,11 @@ EventTypes:
|
||||
deactivated: Esquema de usuario desactivado
|
||||
reactivated: Esquema de usuario reactivado
|
||||
deleted: Esquema de usuario eliminado
|
||||
web_key:
|
||||
added: Clave web añadida
|
||||
activated: Clave web activada
|
||||
deactivated: Clave web desactivada
|
||||
removed: Clave web eliminada
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Les jetons usurpés d'identité ne sont pas autorisés pour l'API
|
||||
Impersonation:
|
||||
PolicyDisabled: L'usurpation d'identité est désactivée dans la politique de sécurité de l'instance
|
||||
WebKey:
|
||||
ActiveDelete: Impossible de supprimer la clé Web active
|
||||
Config: Configuration de clé Web non valide
|
||||
Duplicate: L'ID de clé Web n'est pas unique
|
||||
FeatureDisabled: Fonctionnalité de clé Web désactivée
|
||||
NoActive: Aucune clé Web active trouvée
|
||||
NotFound: Clé Web introuvable
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Restrictions
|
||||
system: Système
|
||||
session: Session
|
||||
web_key: Clé Web
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1171,140 +1179,146 @@ EventTypes:
|
||||
deactivated: Action désactivée
|
||||
reactivated: Action réactivée
|
||||
removed: Action supprimée
|
||||
instance:
|
||||
added: Instance ajoutée
|
||||
changed: Instance modifiée
|
||||
customtext:
|
||||
removed: Texte personnalisé supprimé
|
||||
set: Ensemble de texte personnalisé
|
||||
template:
|
||||
removed: Modèle de texte personnalisé supprimé
|
||||
default:
|
||||
language:
|
||||
set: Langue par défaut définie
|
||||
org:
|
||||
set: Ensemble d'organisation par défaut
|
||||
domain:
|
||||
added: Domaine ajouté
|
||||
primary:
|
||||
set: Ensemble de domaines principal
|
||||
removed: Domaine supprimé
|
||||
iam:
|
||||
console:
|
||||
set: Ensemble d'applications Console ZITADEL
|
||||
project:
|
||||
set: ZITADEL project set
|
||||
mail:
|
||||
template:
|
||||
added: Modèle de courrier électronique ajouté
|
||||
changed: Modèle d'e-mail modifié
|
||||
text:
|
||||
added: Texte de l'e-mail ajouté
|
||||
changed: Le texte de l'e-mail a été modifié
|
||||
member:
|
||||
added: Membre de l'instance ajouté
|
||||
changed: Membre de l'instance modifié
|
||||
removed: Membre de l'instance supprimé
|
||||
cascade:
|
||||
removed: Cascade de membres de l'instance supprimée
|
||||
notification:
|
||||
provider:
|
||||
debug:
|
||||
fileadded: Fournisseur de notification de débogage de fichiers ajouté
|
||||
filechanged: Le fournisseur de notification de débogage de fichier a été modifié
|
||||
fileremoved: Fournisseur de notification de débogage de fichier supprimé
|
||||
logadded: Fournisseur de notification de débogage de journal ajouté
|
||||
logchanged: Le fournisseur de notification de débogage du journal a été modifié
|
||||
logremoved: Fournisseur de notification de débogage du journal supprimé
|
||||
oidc:
|
||||
settings:
|
||||
added: Paramètres OIDC ajoutés
|
||||
changed: Paramètres OIDC modifiés
|
||||
policy:
|
||||
domain:
|
||||
added: Politique de domaine ajoutée
|
||||
changed: Politique de domaine modifiée
|
||||
label:
|
||||
activated: Politique d'étiquetage activée
|
||||
added: Politique d'étiquetage ajoutée
|
||||
assets:
|
||||
removed: L'élément de la stratégie d'étiquette a été supprimé
|
||||
changed: Politique d'étiquetage modifiée
|
||||
font:
|
||||
added: Police ajoutée à la stratégie d'étiquette
|
||||
removed: Police supprimée de la stratégie relative aux étiquettes
|
||||
icon:
|
||||
added: Icône ajoutée à la politique d'étiquetage
|
||||
removed: Icône supprimée des règles relatives aux étiquettes
|
||||
dark:
|
||||
added: Icône ajoutée à la politique d'étiquette sombre
|
||||
removed: Icône supprimée de la politique relative aux étiquettes sombres
|
||||
logo:
|
||||
added: Logo ajouté à la politique d'étiquetage
|
||||
removed: Logo supprimé de la politique relative aux étiquettes
|
||||
dark:
|
||||
added: Logo ajouté à la politique relative aux étiquettes sombres
|
||||
removed: Logo supprimé de la politique relative aux étiquettes sombres
|
||||
lockout:
|
||||
added: Politique de verrouillage ajoutée
|
||||
changed: La politique de verrouillage a été modifiée
|
||||
login:
|
||||
added: Politique de connexion ajoutée
|
||||
changed: Politique de connexion modifiée
|
||||
idpprovider:
|
||||
added: Fournisseur d'identité ajouté à la politique de connexion
|
||||
cascade:
|
||||
removed: Cascade de fournisseurs d'identité supprimée de la stratégie de connexion
|
||||
removed: Fournisseur d'identité supprimé de la stratégie de connexion
|
||||
multifactor:
|
||||
added: Multifactor ajouté à la politique de connexion
|
||||
removed: Multifactor supprimé de la politique de connexion
|
||||
secondfactor:
|
||||
added: Deuxième facteur ajouté à la politique de connexion
|
||||
removed: Deuxième facteur supprimé de la politique de connexion
|
||||
password:
|
||||
age:
|
||||
added: Politique d'âge du mot de passe ajoutée
|
||||
changed: La politique relative à l'âge du mot de passe a été modifiée
|
||||
complexity:
|
||||
added: Politique de complexité des mots de passe ajoutée
|
||||
changed: Politique de complexité des mots de passe supprimée
|
||||
privacy:
|
||||
added: Politique de confidentialité ajoutée
|
||||
changed: Politique de confidentialité modifiée
|
||||
security:
|
||||
set: Ensemble de règles de sécurité
|
||||
|
||||
removed: Instance removed
|
||||
secret:
|
||||
generator:
|
||||
added: Générateur de secrets ajouté
|
||||
changed: Le générateur de secrets a changé
|
||||
removed: Générateur de secrets supprimé
|
||||
sms:
|
||||
configtwilio:
|
||||
activated: Configuration SMS Twilio activée
|
||||
added: Configuration SMS Twilio ajoutée
|
||||
changed: La configuration des SMS Twilio a été modifiée
|
||||
deactivated: Configuration SMS Twilio désactivée
|
||||
removed: Configuration SMS Twilio supprimée
|
||||
token:
|
||||
changed: Jeton de configuration SMS Twilio modifié
|
||||
smtp:
|
||||
config:
|
||||
added: Configuration SMTP ajoutée
|
||||
changed: Configuration SMTP modifiée
|
||||
activated: Configuration SMTP activée
|
||||
deactivated: Configuration SMTP désactivée
|
||||
password:
|
||||
changed: Mot de passe de configuration SMTP modifié
|
||||
removed: Configuration SMTP supprimée
|
||||
user_schema:
|
||||
created: Schéma utilisateur créé
|
||||
updated: Schéma utilisateur mis à jour
|
||||
deactivated: Schéma utilisateur désactivé
|
||||
reactivated: Schéma utilisateur réactivé
|
||||
deleted: Schéma utilisateur supprimé
|
||||
instance:
|
||||
added: Instance ajoutée
|
||||
changed: Instance modifiée
|
||||
customtext:
|
||||
removed: Texte personnalisé supprimé
|
||||
set: Ensemble de texte personnalisé
|
||||
template:
|
||||
removed: Modèle de texte personnalisé supprimé
|
||||
default:
|
||||
language:
|
||||
set: Langue par défaut définie
|
||||
org:
|
||||
set: Ensemble d'organisation par défaut
|
||||
domain:
|
||||
added: Domaine ajouté
|
||||
primary:
|
||||
set: Ensemble de domaines principal
|
||||
removed: Domaine supprimé
|
||||
iam:
|
||||
console:
|
||||
set: Ensemble d'applications Console ZITADEL
|
||||
project:
|
||||
set: ZITADEL project set
|
||||
mail:
|
||||
template:
|
||||
added: Modèle de courrier électronique ajouté
|
||||
changed: Modèle d'e-mail modifié
|
||||
text:
|
||||
added: Texte de l'e-mail ajouté
|
||||
changed: Le texte de l'e-mail a été modifié
|
||||
member:
|
||||
added: Membre de l'instance ajouté
|
||||
changed: Membre de l'instance modifié
|
||||
removed: Membre de l'instance supprimé
|
||||
cascade:
|
||||
removed: Cascade de membres de l'instance supprimée
|
||||
notification:
|
||||
provider:
|
||||
debug:
|
||||
fileadded: Fournisseur de notification de débogage de fichiers ajouté
|
||||
filechanged: Le fournisseur de notification de débogage de fichier a été modifié
|
||||
fileremoved: Fournisseur de notification de débogage de fichier supprimé
|
||||
logadded: Fournisseur de notification de débogage de journal ajouté
|
||||
logchanged: Le fournisseur de notification de débogage du journal a été modifié
|
||||
logremoved: Fournisseur de notification de débogage du journal supprimé
|
||||
oidc:
|
||||
settings:
|
||||
added: Paramètres OIDC ajoutés
|
||||
changed: Paramètres OIDC modifiés
|
||||
policy:
|
||||
domain:
|
||||
added: Politique de domaine ajoutée
|
||||
changed: Politique de domaine modifiée
|
||||
label:
|
||||
activated: Politique d'étiquetage activée
|
||||
added: Politique d'étiquetage ajoutée
|
||||
assets:
|
||||
removed: L'élément de la stratégie d'étiquette a été supprimé
|
||||
changed: Politique d'étiquetage modifiée
|
||||
font:
|
||||
added: Police ajoutée à la stratégie d'étiquette
|
||||
removed: Police supprimée de la stratégie relative aux étiquettes
|
||||
icon:
|
||||
added: Icône ajoutée à la politique d'étiquetage
|
||||
removed: Icône supprimée des règles relatives aux étiquettes
|
||||
dark:
|
||||
added: Icône ajoutée à la politique d'étiquette sombre
|
||||
removed: Icône supprimée de la politique relative aux étiquettes sombres
|
||||
logo:
|
||||
added: Logo ajouté à la politique d'étiquetage
|
||||
removed: Logo supprimé de la politique relative aux étiquettes
|
||||
dark:
|
||||
added: Logo ajouté à la politique relative aux étiquettes sombres
|
||||
removed: Logo supprimé de la politique relative aux étiquettes sombres
|
||||
lockout:
|
||||
added: Politique de verrouillage ajoutée
|
||||
changed: La politique de verrouillage a été modifiée
|
||||
login:
|
||||
added: Politique de connexion ajoutée
|
||||
changed: Politique de connexion modifiée
|
||||
idpprovider:
|
||||
added: Fournisseur d'identité ajouté à la politique de connexion
|
||||
cascade:
|
||||
removed: Cascade de fournisseurs d'identité supprimée de la stratégie de connexion
|
||||
removed: Fournisseur d'identité supprimé de la stratégie de connexion
|
||||
multifactor:
|
||||
added: Multifactor ajouté à la politique de connexion
|
||||
removed: Multifactor supprimé de la politique de connexion
|
||||
secondfactor:
|
||||
added: Deuxième facteur ajouté à la politique de connexion
|
||||
removed: Deuxième facteur supprimé de la politique de connexion
|
||||
password:
|
||||
age:
|
||||
added: Politique d'âge du mot de passe ajoutée
|
||||
changed: La politique relative à l'âge du mot de passe a été modifiée
|
||||
complexity:
|
||||
added: Politique de complexité des mots de passe ajoutée
|
||||
changed: Politique de complexité des mots de passe supprimée
|
||||
privacy:
|
||||
added: Politique de confidentialité ajoutée
|
||||
changed: Politique de confidentialité modifiée
|
||||
security:
|
||||
set: Ensemble de règles de sécurité
|
||||
web_key:
|
||||
added: Clé Web ajoutée
|
||||
activated: Clé Web activée
|
||||
deactivated: Clé Web désactivée
|
||||
removed: Clé Web supprimée
|
||||
|
||||
removed: Instance removed
|
||||
secret:
|
||||
generator:
|
||||
added: Générateur de secrets ajouté
|
||||
changed: Le générateur de secrets a changé
|
||||
removed: Générateur de secrets supprimé
|
||||
sms:
|
||||
configtwilio:
|
||||
activated: Configuration SMS Twilio activée
|
||||
added: Configuration SMS Twilio ajoutée
|
||||
changed: La configuration des SMS Twilio a été modifiée
|
||||
deactivated: Configuration SMS Twilio désactivée
|
||||
removed: Configuration SMS Twilio supprimée
|
||||
token:
|
||||
changed: Jeton de configuration SMS Twilio modifié
|
||||
smtp:
|
||||
config:
|
||||
added: Configuration SMTP ajoutée
|
||||
changed: Configuration SMTP modifiée
|
||||
activated: Configuration SMTP activée
|
||||
deactivated: Configuration SMTP désactivée
|
||||
password:
|
||||
changed: Mot de passe de configuration SMTP modifié
|
||||
removed: Configuration SMTP supprimée
|
||||
Application:
|
||||
OIDC:
|
||||
UnsupportedVersion: Votre version de l'OIDC n'est pas prise en charge
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Token rappresentati non consentiti per l'API
|
||||
Impersonation:
|
||||
PolicyDisabled: La rappresentazione è disabilitata nella policy di sicurezza dell'istanza
|
||||
WebKey:
|
||||
ActiveDelete: Impossibile eliminare la chiave Web attiva
|
||||
Config: Configurazione chiave Web non valida
|
||||
Duplicate: ID chiave Web non univoco
|
||||
FeatureDisabled: Funzione chiave Web disabilitata
|
||||
NoActive: Nessuna chiave Web attiva trovata
|
||||
NotFound: Chiave Web non trovata
|
||||
|
||||
AggregateTypes:
|
||||
action: Azione
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Restrizioni
|
||||
system: Sistema
|
||||
session: Sessione
|
||||
web_key: Chiave Web
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1172,12 +1180,6 @@ EventTypes:
|
||||
deactivated: Azione disattivata
|
||||
reactivated: Azione riattivata
|
||||
removed: Azione rimossa
|
||||
user_schema:
|
||||
created: Schema utente creato
|
||||
updated: Schema utente aggiornato
|
||||
deactivated: Schema utente disattivato
|
||||
reactivated: Schema utente riattivato
|
||||
deleted: Schema utente eliminato
|
||||
instance:
|
||||
added: Istanza aggiunta
|
||||
changed: L'istanza è cambiata
|
||||
@ -1306,6 +1308,17 @@ EventTypes:
|
||||
password:
|
||||
changed: La password della configurazione SMTP è cambiata
|
||||
removed: Configurazione SMTP rimossa
|
||||
user_schema:
|
||||
created: Schema utente creato
|
||||
updated: Schema utente aggiornato
|
||||
deactivated: Schema utente disattivato
|
||||
reactivated: Schema utente riattivato
|
||||
deleted: Schema utente eliminato
|
||||
web_key:
|
||||
added: Web Key aggiunto
|
||||
activated: Web Key attivato
|
||||
deactivated: Web Key disattivato
|
||||
removed: Web Key rimosso
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -575,6 +575,13 @@ Errors:
|
||||
NotForAPI: 偽装されたトークンは API では許可されません
|
||||
Impersonation:
|
||||
PolicyDisabled: インスタンスのセキュリティ ポリシーで偽装が無効になっています
|
||||
WebKey:
|
||||
ActiveDelete: アクティブな Web キーを削除できません
|
||||
Config: 無効な Web キー設定
|
||||
Duplicate: Web キー ID が一意ではありません
|
||||
FeatureDisabled: Web キー機能が無効です
|
||||
NoActive: アクティブな Web キーが見つかりません
|
||||
NotFound: Web キーが見つかりません
|
||||
|
||||
AggregateTypes:
|
||||
action: アクション
|
||||
@ -598,6 +605,7 @@ AggregateTypes:
|
||||
restrictions: 制限
|
||||
system: システム
|
||||
session: セッション
|
||||
web_key: Web キー
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1296,6 +1304,11 @@ EventTypes:
|
||||
deactivated: ユーザースキーマが非アクティブ化されました
|
||||
reactivated: ユーザースキーマが再アクティブ化されました
|
||||
deleted: ユーザースキーマが削除されました
|
||||
web_key:
|
||||
added: Web キーが追加されました
|
||||
activated: Web キーが有効化されました
|
||||
deactivated: Web キーが無効化されました
|
||||
removed: Web キーが削除されました
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -585,6 +585,13 @@ Errors:
|
||||
NotForAPI: Имитирани токени не се дозволени за API
|
||||
Impersonation:
|
||||
PolicyDisabled: Имитирањето е оневозможено во политиката за безбедност на примерот
|
||||
WebKey:
|
||||
ActiveDelete: Не може да се избрише активниот веб-клуч
|
||||
Config: Неважечка конфигурација на веб-клуч
|
||||
Duplicate: ID на веб-клучот не е единствен
|
||||
FeatureDisabled: Функцијата за веб-клуч е оневозможена
|
||||
NoActive: Не е пронајден активен веб-клуч
|
||||
NotFound: Веб-клучот не е пронајден
|
||||
|
||||
AggregateTypes:
|
||||
action: Акција
|
||||
@ -608,6 +615,7 @@ AggregateTypes:
|
||||
restrictions: Ограничувања
|
||||
system: Систем
|
||||
session: Сесија
|
||||
web_key: Веб клуч
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1308,6 +1316,11 @@ EventTypes:
|
||||
deactivated: Корисничката шема е деактивирана
|
||||
reactivated: Корисничката шема е реактивирана
|
||||
deleted: Корисничката шема е избришана
|
||||
web_key:
|
||||
added: Додаден е веб-клуч
|
||||
activated: Веб-клучот е активиран
|
||||
deactivated: Веб-клучот е деактивиран
|
||||
removed: Веб-клучот е отстранет
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Nagebootste tokens zijn niet toegestaan voor API
|
||||
Impersonation:
|
||||
PolicyDisabled: Nabootsing van identiteit is uitgeschakeld in het beveiligingsbeleid van de instantie.
|
||||
WebKey:
|
||||
ActiveDelete: Kan actieve websleutel niet verwijderen
|
||||
Config: Ongeldige websleutelconfiguratie
|
||||
Duplicate: Websleutel-ID niet uniek
|
||||
FeatureDisabled: Websleutelfunctie uitgeschakeld
|
||||
NoActive: Geen actieve websleutel gevonden
|
||||
NotFound: Websleutel niet gevonden
|
||||
|
||||
AggregateTypes:
|
||||
action: Actie
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Beperkingen
|
||||
system: Systeem
|
||||
session: Sessie
|
||||
web_key: Websleutel
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1305,6 +1313,11 @@ EventTypes:
|
||||
deactivated: Gebruikersschema gedeactiveerd
|
||||
reactivated: Gebruikersschema opnieuw geactiveerd
|
||||
deleted: Gebruikersschema verwijderd
|
||||
web_key:
|
||||
added: Web Key toegevoegd
|
||||
activated: Web Key geactiveerd
|
||||
deactivated: Web Key gedeactiveerd
|
||||
removed: Web Key verwijderd
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Podrabiane tokeny nie są dozwolone w interfejsie API
|
||||
Impersonation:
|
||||
PolicyDisabled: Podszywanie się jest wyłączone w polityce bezpieczeństwa instancji
|
||||
WebKey:
|
||||
ActiveDelete: Nie można usunąć aktywnego klucza internetowego
|
||||
Config: Nieprawidłowa konfiguracja klucza internetowego
|
||||
Duplicate: Identyfikator klucza internetowego nie jest unikalny
|
||||
FeatureDisabled: Funkcja klucza internetowego jest wyłączona
|
||||
NoActive: Nie znaleziono aktywnego klucza internetowego
|
||||
NotFound: Nie znaleziono klucza internetowego
|
||||
|
||||
AggregateTypes:
|
||||
action: Działanie
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Ograniczenia
|
||||
system: System
|
||||
session: Sesja
|
||||
web_key: Klucz internetowy
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1310,6 +1318,11 @@ EventTypes:
|
||||
deactivated: Schemat użytkownika dezaktywowany
|
||||
reactivated: Schemat użytkownika został ponownie aktywowany
|
||||
deleted: Schemat użytkownika został usunięty
|
||||
web_key:
|
||||
added: Dodano klucz internetowy
|
||||
activated: Klucz internetowy aktywowano
|
||||
deactivated: Klucz internetowy dezaktywowano
|
||||
removed: Klucz internetowy usunięto
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -581,6 +581,13 @@ Errors:
|
||||
NotForAPI: Tokens personificados não permitidos para API
|
||||
Impersonation:
|
||||
PolicyDisabled: A representação está desativada na política de segurança da instância
|
||||
WebKey:
|
||||
ActiveDelete: Não é possível eliminar a chave web ativa
|
||||
Config: Configuração de chave web inválida
|
||||
Duplicate: ID da chave Web não exclusivo
|
||||
FeatureDisabled: Recurso chave da Web desativado
|
||||
NoActive: Nenhuma chave web ativa encontrada
|
||||
NotFound: Chave Web não encontrada
|
||||
|
||||
AggregateTypes:
|
||||
action: Ação
|
||||
@ -604,6 +611,7 @@ AggregateTypes:
|
||||
restrictions: Restrições
|
||||
system: Sistema
|
||||
session: Sessão
|
||||
web_key: Chave da Web
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1302,6 +1310,11 @@ EventTypes:
|
||||
deactivated: Esquema de usuário desativado
|
||||
reactivated: Esquema do usuário reativado
|
||||
deleted: Esquema do usuário excluído
|
||||
web_key:
|
||||
added: Chave Web adicionada
|
||||
activated: Chave Web ativada
|
||||
deactivated: Chave Web desativada
|
||||
removed: Chave Web removida
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -575,6 +575,13 @@ Errors:
|
||||
NotForAPI: Олицетворенные токены не разрешены для API.
|
||||
Impersonation:
|
||||
PolicyDisabled: Олицетворение отключено в политике безопасности экземпляра.
|
||||
WebKey:
|
||||
ActiveDelete: Невозможно удалить активный веб-ключ
|
||||
Config: Неверная конфигурация веб-ключа
|
||||
Duplicate: Идентификатор веб-ключа не уникален
|
||||
FeatureDisabled: Функция веб-ключа отключена
|
||||
NoActive: Активный веб-ключ не найден
|
||||
NotFound: Веб-ключ не найден
|
||||
|
||||
AggregateTypes:
|
||||
action: Действие
|
||||
@ -598,6 +605,7 @@ AggregateTypes:
|
||||
restrictions: Ограничения
|
||||
system: Система
|
||||
session: Сеанс
|
||||
web_key: Веб-ключ
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1296,6 +1304,12 @@ EventTypes:
|
||||
deactivated: Пользовательская схема деактивирована
|
||||
reactivated: Пользовательская схема повторно активирована
|
||||
deleted: Пользовательская схема удалена
|
||||
web_key:
|
||||
added: Добавлен веб-ключ
|
||||
activated: Веб-ключ активирован
|
||||
deactivated: Веб-ключ деактивирован
|
||||
removed: Веб-ключ удален
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
UnsupportedVersion: Ваша версия OIDC не поддерживается
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: Imitationstoken tillåts inte för API
|
||||
Impersonation:
|
||||
PolicyDisabled: Imitation är inaktiverad i instansens säkerhetspolicy
|
||||
WebKey:
|
||||
ActiveDelete: Det går inte att ta bort aktiv webbnyckel
|
||||
Config: Ogiltig webbnyckelkonfiguration
|
||||
Duplicate: Webnyckel-ID är inte unikt
|
||||
FeatureDisabled: Webnyckelfunktion inaktiverad
|
||||
NoActive: Ingen aktiv webbnyckel hittades
|
||||
NotFound: Webnyckel hittades inte
|
||||
|
||||
AggregateTypes:
|
||||
action: Åtgärd
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: Restriktioner
|
||||
system: System
|
||||
session: Session
|
||||
web_key: Webbnyckel
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1310,6 +1318,11 @@ EventTypes:
|
||||
deactivated: Användarschema avaktiverat
|
||||
reactivated: Användarschema återaktiverat
|
||||
deleted: Användarschema borttaget
|
||||
web_key:
|
||||
added: Webbnyckel har lagts till
|
||||
activated: Webbnyckel aktiverad
|
||||
deactivated: Webnyckel avaktiverad
|
||||
removed: Webbnyckeln har tagits bort
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -586,6 +586,13 @@ Errors:
|
||||
NotForAPI: API 不允许使用模拟令牌
|
||||
Impersonation:
|
||||
PolicyDisabled: 实例安全策略中禁用模拟
|
||||
WebKey:
|
||||
ActiveDelete: 无法删除活动 Web 密钥
|
||||
Config: 无效的 Web 密钥配置
|
||||
Duplicate: Web 密钥 ID 不唯一
|
||||
FeatureDisabled: Web 密钥功能已禁用
|
||||
NoActive: 未找到活动 Web 密钥
|
||||
NotFound: 未找到 Web 密钥
|
||||
|
||||
AggregateTypes:
|
||||
action: 动作
|
||||
@ -609,6 +616,7 @@ AggregateTypes:
|
||||
restrictions: 限制
|
||||
system: 系统
|
||||
session: 会话
|
||||
web_key: Web 密钥
|
||||
|
||||
EventTypes:
|
||||
execution:
|
||||
@ -1175,12 +1183,6 @@ EventTypes:
|
||||
deactivated: 停用动作
|
||||
reactivated: 启用动作
|
||||
removed: 删除动作
|
||||
user_schema:
|
||||
created: 已创建用户架构
|
||||
updated: 用户架构已更新
|
||||
deactivated: 用户架构已停用
|
||||
reactivated: 用户架构已重新激活
|
||||
deleted: 用户架构已删除
|
||||
instance:
|
||||
added: 实例已添加
|
||||
changed: 实例已更改
|
||||
@ -1309,6 +1311,17 @@ EventTypes:
|
||||
password:
|
||||
changed: SMTP 配置密码已更改
|
||||
removed: SMTP 配置已删除
|
||||
user_schema:
|
||||
created: 已创建用户架构
|
||||
updated: 用户架构已更新
|
||||
deactivated: 用户架构已停用
|
||||
reactivated: 用户架构已重新激活
|
||||
deleted: 用户架构已删除
|
||||
web_key:
|
||||
added: 已添加 Web Key
|
||||
activated: 已激活 Web Key
|
||||
deactivated: 已停用 Web Key
|
||||
removed: 已删除 Web Key
|
||||
|
||||
Application:
|
||||
OIDC:
|
||||
|
@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{
|
||||
description: "Improves performance of specified execution paths.";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool web_key = 8 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetInstanceFeaturesResponse {
|
||||
@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse {
|
||||
description: "Improves performance of specified execution paths.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag web_key = 9 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{
|
||||
description: "Improves performance of specified execution paths.";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool web_key = 8 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetInstanceFeaturesResponse {
|
||||
@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse {
|
||||
description: "Improves performance of specified execution paths.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag web_key = 9 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
41
proto/zitadel/resources/webkey/v3alpha/config.proto
Normal file
41
proto/zitadel/resources/webkey/v3alpha/config.proto
Normal file
@ -0,0 +1,41 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.resources.webkey.v3alpha;
|
||||
|
||||
import "validate/validate.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
|
||||
|
||||
message WebKeyRSAConfig {
|
||||
enum RSABits {
|
||||
RSA_BITS_UNSPECIFIED = 0;
|
||||
RSA_BITS_2048 = 1;
|
||||
RSA_BITS_3072 = 2;
|
||||
RSA_BITS_4096 = 3;
|
||||
}
|
||||
|
||||
enum RSAHasher {
|
||||
RSA_HASHER_UNSPECIFIED = 0;
|
||||
RSA_HASHER_SHA256 = 1;
|
||||
RSA_HASHER_SHA384 = 2;
|
||||
RSA_HASHER_SHA512 = 3;
|
||||
}
|
||||
|
||||
// bit size of the RSA key
|
||||
RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
|
||||
// signing algrithm used
|
||||
RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
|
||||
}
|
||||
|
||||
message WebKeyECDSAConfig {
|
||||
enum ECDSACurve {
|
||||
ECDSA_CURVE_UNSPECIFIED = 0;
|
||||
ECDSA_CURVE_P256 = 1;
|
||||
ECDSA_CURVE_P384 = 2;
|
||||
ECDSA_CURVE_P512 = 3;
|
||||
}
|
||||
|
||||
ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
|
||||
}
|
||||
|
||||
message WebKeyED25519Config {}
|
31
proto/zitadel/resources/webkey/v3alpha/key.proto
Normal file
31
proto/zitadel/resources/webkey/v3alpha/key.proto
Normal file
@ -0,0 +1,31 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.resources.webkey.v3alpha;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "zitadel/resources/webkey/v3alpha/config.proto";
|
||||
import "zitadel/resources/object/v3alpha/object.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
|
||||
|
||||
enum WebKeyState {
|
||||
STATE_UNSPECIFIED = 0;
|
||||
STATE_INITIAL = 1;
|
||||
STATE_ACTIVE = 2;
|
||||
STATE_INACTIVE = 3;
|
||||
STATE_REMOVED = 4;
|
||||
}
|
||||
|
||||
message GetWebKey {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
WebKey config = 2;
|
||||
WebKeyState state = 3;
|
||||
}
|
||||
|
||||
message WebKey {
|
||||
oneof config {
|
||||
WebKeyRSAConfig rsa = 6;
|
||||
WebKeyECDSAConfig ecdsa = 7;
|
||||
WebKeyED25519Config ed25519 = 8;
|
||||
}
|
||||
}
|
278
proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
Normal file
278
proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
Normal file
@ -0,0 +1,278 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.resources.webkey.v3alpha;
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||
|
||||
import "zitadel/resources/webkey/v3alpha/key.proto";
|
||||
import "zitadel/resources/object/v3alpha/object.proto";
|
||||
import "zitadel/object/v3alpha/object.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||
info: {
|
||||
title: "Web key Service";
|
||||
version: "3.0-preview";
|
||||
description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released.";
|
||||
contact:{
|
||||
name: "ZITADEL"
|
||||
url: "https://zitadel.com"
|
||||
email: "hi@zitadel.com"
|
||||
}
|
||||
license: {
|
||||
name: "Apache 2.0",
|
||||
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||
};
|
||||
};
|
||||
schemes: HTTPS;
|
||||
schemes: HTTP;
|
||||
|
||||
consumes: "application/json";
|
||||
produces: "application/json";
|
||||
|
||||
consumes: "application/grpc";
|
||||
produces: "application/grpc";
|
||||
|
||||
consumes: "application/grpc-web+proto";
|
||||
produces: "application/grpc-web+proto";
|
||||
|
||||
host: "${ZITADEL_DOMAIN}";
|
||||
base_path: "/resources/v3alpha/web_keys";
|
||||
|
||||
external_docs: {
|
||||
description: "Detailed information about ZITADEL",
|
||||
url: "https://zitadel.com/docs"
|
||||
}
|
||||
security_definitions: {
|
||||
security: {
|
||||
key: "OAuth2";
|
||||
value: {
|
||||
type: TYPE_OAUTH2;
|
||||
flow: FLOW_ACCESS_CODE;
|
||||
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
|
||||
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
|
||||
scopes: {
|
||||
scope: {
|
||||
key: "openid";
|
||||
value: "openid";
|
||||
}
|
||||
scope: {
|
||||
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
security: {
|
||||
security_requirement: {
|
||||
key: "OAuth2";
|
||||
value: {
|
||||
scope: "openid";
|
||||
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
key: "403";
|
||||
value: {
|
||||
description: "Returned when the user does not have permission to access the resource.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
key: "404";
|
||||
value: {
|
||||
description: "Returned when the resource does not exist.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service ZITADELWebKeys {
|
||||
rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/"
|
||||
body: "key"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.web_key.write"
|
||||
}
|
||||
http_response: {
|
||||
success_code: 201
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Generate a web key pair for the instance";
|
||||
description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/{id}/_activate"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.web_key.write"
|
||||
}
|
||||
http_response: {
|
||||
success_code: 200
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Activate a signing key for the instance";
|
||||
description: "Switch the active signing web key. The previously active key will be deactivated."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/{id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.web_key.delete"
|
||||
}
|
||||
http_response: {
|
||||
success_code: 200
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Generate a web key pair for the instance";
|
||||
description: "Delete a web key. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.web_key.read"
|
||||
}
|
||||
http_response: {
|
||||
success_code: 200
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Generate a web key pair for the instance";
|
||||
description: "List web key details for the instance"
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message CreateWebKeyRequest {
|
||||
optional zitadel.object.v3alpha.Instance instance = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
default: "\"domain from HOST or :authority header\""
|
||||
}
|
||||
];
|
||||
WebKey key = 2;
|
||||
}
|
||||
|
||||
message CreateWebKeyResponse {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message ActivateWebKeyRequest {
|
||||
optional zitadel.object.v3alpha.Instance instance = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
default: "\"domain from HOST or :authority header\""
|
||||
}
|
||||
];
|
||||
string id = 2 [
|
||||
(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\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ActivateWebKeyResponse {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message DeleteWebKeyRequest {
|
||||
optional zitadel.object.v3alpha.Instance instance = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
default: "\"domain from HOST or :authority header\""
|
||||
}
|
||||
];
|
||||
string id = 2 [
|
||||
(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\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message DeleteWebKeyResponse {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message ListWebKeysRequest {
|
||||
optional zitadel.object.v3alpha.Instance instance = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
default: "\"domain from HOST or :authority header\""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ListWebKeysResponse {
|
||||
repeated GetWebKey web_keys = 1;
|
||||
}
|
Loading…
Reference in New Issue
Block a user