diff --git a/service/integration/kas_registry_key_test.go b/service/integration/kas_registry_key_test.go index c5bc0a620d..a8435ce3b3 100644 --- a/service/integration/kas_registry_key_test.go +++ b/service/integration/kas_registry_key_test.go @@ -22,6 +22,7 @@ import ( "github.com/opentdf/platform/service/pkg/db" "github.com/stretchr/testify/suite" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) const ( @@ -458,6 +459,147 @@ func (s *KasRegistryKeySuite) Test_UpdateKeyMetadata_Success() { s.Equal(s.kasKeys[1].ID, resp.GetKey().GetId()) } +func (s *KasRegistryKeySuite) Test_UnsafeUpdateKey_PublicKeyOnlyToRemote_Success() { + providerConfig := s.createUnsafeUpdateKeyProviderConfig("unsafe-update-key-remote") + createdKey := s.createUnsafeUpdateKeyTestKey(policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, "") + s.T().Cleanup(func() { + s.cleanupUnsafeUpdateKeyTestResources([]string{createdKey.GetKey().GetId()}, []string{providerConfig.GetId()}) + }) + expectedKey := proto.CloneOf(createdKey) + expectedKey.GetKey().KeyMode = policy.KeyMode_KEY_MODE_REMOTE + expectedKey.GetKey().ProviderConfig = providerConfig + + updatedKey, err := s.db.PolicyClient.UnsafeUpdateKey(s.ctx, createdKey, &unsafe.UnsafeUpdateKeyRequest{ + Id: createdKey.GetKey().GetId(), + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: providerConfig.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(updatedKey) + expectedKey.GetKey().GetMetadata().UpdatedAt = updatedKey.GetKey().GetMetadata().GetUpdatedAt() + s.True(proto.Equal(expectedKey, updatedKey)) + + gotKey, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: createdKey.GetKey().GetId(), + }) + s.Require().NoError(err) + s.True(proto.Equal(expectedKey, gotKey)) +} + +func (s *KasRegistryKeySuite) Test_UnsafeUpdateKey_RemoteToPublicKeyOnly_Success() { + providerConfig := s.createUnsafeUpdateKeyProviderConfig("unsafe-update-key-public") + createdKey := s.createUnsafeUpdateKeyTestKey(policy.KeyMode_KEY_MODE_REMOTE, providerConfig.GetId()) + s.T().Cleanup(func() { + s.cleanupUnsafeUpdateKeyTestResources([]string{createdKey.GetKey().GetId()}, []string{providerConfig.GetId()}) + }) + expectedKey := proto.CloneOf(createdKey) + expectedKey.GetKey().KeyMode = policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY + expectedKey.GetKey().ProviderConfig = nil + + updatedKey, err := s.db.PolicyClient.UnsafeUpdateKey(s.ctx, createdKey, &unsafe.UnsafeUpdateKeyRequest{ + Id: createdKey.GetKey().GetId(), + TargetKeyMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + }) + s.Require().NoError(err) + s.Require().NotNil(updatedKey) + expectedKey.GetKey().GetMetadata().UpdatedAt = updatedKey.GetKey().GetMetadata().GetUpdatedAt() + s.True(proto.Equal(expectedKey, updatedKey)) + + gotKey, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: createdKey.GetKey().GetId(), + }) + s.Require().NoError(err) + s.True(proto.Equal(expectedKey, gotKey)) +} + +func (s *KasRegistryKeySuite) Test_UnsafeUpdateKey_ChangeProviderConfig_Success() { + originalProviderConfig := s.createUnsafeUpdateKeyProviderConfig("unsafe-update-key-original-provider") + updatedProviderConfig := s.createUnsafeUpdateKeyProviderConfig("unsafe-update-key-updated-provider") + createdKey := s.createUnsafeUpdateKeyTestKey(policy.KeyMode_KEY_MODE_REMOTE, originalProviderConfig.GetId()) + s.T().Cleanup(func() { + s.cleanupUnsafeUpdateKeyTestResources( + []string{createdKey.GetKey().GetId()}, + []string{originalProviderConfig.GetId(), updatedProviderConfig.GetId()}, + ) + }) + expectedKey := proto.CloneOf(createdKey) + expectedKey.GetKey().ProviderConfig = updatedProviderConfig + + updatedKey, err := s.db.PolicyClient.UnsafeUpdateKey(s.ctx, createdKey, &unsafe.UnsafeUpdateKeyRequest{ + Id: createdKey.GetKey().GetId(), + ProviderConfigId: updatedProviderConfig.GetId(), + }) + s.Require().NoError(err) + s.Require().NotNil(updatedKey) + expectedKey.GetKey().GetMetadata().UpdatedAt = updatedKey.GetKey().GetMetadata().GetUpdatedAt() + s.True(proto.Equal(expectedKey, updatedKey)) + + gotKey, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: createdKey.GetKey().GetId(), + }) + s.Require().NoError(err) + s.True(proto.Equal(expectedKey, gotKey)) +} + +func (s *KasRegistryKeySuite) Test_UnsafeUpdateKey_NonexistentProviderConfig_Fails() { + createdKey := s.createUnsafeUpdateKeyTestKey(policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, "") + s.T().Cleanup(func() { + s.cleanupUnsafeUpdateKeyTestResources([]string{createdKey.GetKey().GetId()}, nil) + }) + + updatedKey, err := s.db.PolicyClient.UnsafeUpdateKey(s.ctx, createdKey, &unsafe.UnsafeUpdateKeyRequest{ + Id: createdKey.GetKey().GetId(), + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: uuid.NewString(), + }) + s.Require().Error(err) + s.Nil(updatedKey) + s.Require().ErrorIs(err, db.ErrUnsafeUpdateKeyProviderConfigNotFound) + + gotKey, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: createdKey.GetKey().GetId(), + }) + s.Require().NoError(err) + s.True(proto.Equal(createdKey, gotKey)) +} + +func (s *KasRegistryKeySuite) Test_UnsafeUpdateKey_UnsupportedExistingKeyMode_Fails() { + providerConfig := s.createUnsafeUpdateKeyProviderConfig("unsafe-update-key-unsupported-mode") + keyResp, err := s.db.PolicyClient.CreateKey(s.ctx, &kasregistry.CreateKeyRequest{ + KasId: s.kasKeys[0].KeyAccessServerID, + KeyId: uuid.NewString(), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + PrivateKeyCtx: &policy.PrivateKeyCtx{ + KeyId: validKeyID1, + WrappedKey: keyCtx, + }, + }) + s.Require().NoError(err) + s.Require().NotNil(keyResp) + createdKey := keyResp.GetKasKey() + s.Require().NotNil(createdKey) + s.T().Cleanup(func() { + s.cleanupUnsafeUpdateKeyTestResources([]string{createdKey.GetKey().GetId()}, []string{providerConfig.GetId()}) + }) + + updatedKey, err := s.db.PolicyClient.UnsafeUpdateKey(s.ctx, createdKey, &unsafe.UnsafeUpdateKeyRequest{ + Id: createdKey.GetKey().GetId(), + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: providerConfig.GetId(), + }) + s.Require().Error(err) + s.Nil(updatedKey) + s.Require().ErrorIs(err, db.ErrUnsafeUpdateKeyExistingModeUnsupported) + + gotKey, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: createdKey.GetKey().GetId(), + }) + s.Require().NoError(err) + s.True(proto.Equal(createdKey, gotKey)) +} + func (s *KasRegistryKeySuite) Test_ListKeys_InvalidLimit_Fail() { req := kasregistry.ListKeysRequest{ Pagination: &policy.PageRequest{ @@ -2739,6 +2881,56 @@ func (s *KasRegistryKeySuite) cleanupKeys(keyIDs []string, keyAccessServerIDs [] } } +func (s *KasRegistryKeySuite) createUnsafeUpdateKeyProviderConfig(namePrefix string) *policy.KeyProviderConfig { + providerConfig, err := s.db.PolicyClient.CreateProviderConfig(s.ctx, &keymanagement.CreateProviderConfigRequest{ + Name: namePrefix + "-" + uuid.NewString(), + Manager: "opentdf.io/basic", + ConfigJson: validProviderConfig, + }) + s.Require().NoError(err) + s.Require().NotNil(providerConfig) + return providerConfig +} + +func (s *KasRegistryKeySuite) createUnsafeUpdateKeyTestKey(keyMode policy.KeyMode, providerConfigID string) *policy.KasKey { + keyResp, err := s.db.PolicyClient.CreateKey(s.ctx, &kasregistry.CreateKeyRequest{ + KasId: s.kasKeys[0].KeyAccessServerID, + KeyId: uuid.NewString(), + KeyAlgorithm: policy.Algorithm_ALGORITHM_RSA_2048, + KeyMode: keyMode, + PublicKeyCtx: &policy.PublicKeyCtx{Pem: keyCtx}, + ProviderConfigId: providerConfigID, + }) + s.Require().NoError(err) + s.Require().NotNil(keyResp) + s.Require().NotNil(keyResp.GetKasKey()) + return keyResp.GetKasKey() +} + +func (s *KasRegistryKeySuite) cleanupUnsafeUpdateKeyTestResources(keyIDs []string, providerConfigIDs []string) { + for _, id := range keyIDs { + key, err := s.db.PolicyClient.GetKey(s.ctx, &kasregistry.GetKeyRequest_Id{ + Id: id, + }) + if err != nil { + s.Require().ErrorContains(err, db.ErrNotFound.Error()) + continue + } + + _, err = s.db.PolicyClient.UnsafeDeleteKey(s.ctx, key, &unsafe.UnsafeDeleteKasKeyRequest{ + Id: key.GetKey().GetId(), + KasUri: key.GetKasUri(), + Kid: key.GetKey().GetKeyId(), + }) + s.Require().NoError(err) + } + + for _, id := range providerConfigIDs { + _, err := s.db.PolicyClient.DeleteProviderConfig(s.ctx, id) + s.Require().NoError(err) + } +} + func listKeysSearchIDs(keyIDsByKID map[string]string) []string { keyIDs := make([]string, 0, len(keyIDsByKID)) for _, id := range keyIDsByKID { diff --git a/service/pkg/db/errors.go b/service/pkg/db/errors.go index 74b77f7fb0..9c0bbe79cf 100644 --- a/service/pkg/db/errors.go +++ b/service/pkg/db/errors.go @@ -15,34 +15,40 @@ import ( ) var ( - ErrUniqueConstraintViolation = errors.New("ErrUniqueConstraintViolation: value must be unique") - ErrNotNullViolation = errors.New("ErrNotNullViolation: value cannot be null") - ErrForeignKeyViolation = errors.New("ErrForeignKeyViolation: value is referenced by another table") - ErrRestrictViolation = errors.New("ErrRestrictViolation: value cannot be deleted due to restriction") - ErrNotFound = errors.New("ErrNotFound: value not found") - ErrAttributeValueInactive = errors.New("ErrAttributeValueInactive: attribute value inactive") - ErrEnumValueInvalid = errors.New("ErrEnumValueInvalid: not a valid enum value") - ErrUUIDInvalid = errors.New("ErrUUIDInvalid: value not a valid UUID") - ErrMissingValue = errors.New("ErrMissingValue: value must be included") - ErrListLimitTooLarge = errors.New("ErrListLimitTooLarge: requested limit greater than configured maximum") - ErrTxBeginFailed = errors.New("ErrTxBeginFailed: failed to begin DB transaction") - ErrTxRollbackFailed = errors.New("ErrTxRollbackFailed: failed to rollback DB transaction") - ErrTxCommitFailed = errors.New("ErrTxCommitFailed: failed to commit DB transaction") - ErrSelectIdentifierInvalid = errors.New("ErrSelectIdentifierInvalid: invalid identifier value for select query") - ErrIdentifierDeprecated = errors.New("ErrIdentifierDeprecated: identifier is deprecated") - ErrUnknownSelectIdentifier = errors.New("ErrUnknownSelectIdentifier: unknown identifier type for select query") - ErrCannotUpdateToUnspecified = errors.New("ErrCannotUpdateToUnspecified: cannot update to unspecified value") - ErrKeyRotationFailed = errors.New("ErrTextKeyRotationFailed: key rotation failed") - ErrExpectedBase64EncodedValue = errors.New("ErrExpectedBase64EncodedValue: expected base64 encoded value") - ErrUnencryptedPrivateKey = errors.New("ErrUnencryptedPrivateKey: unencrypted private key not allowed") - ErrMarshalValueFailed = errors.New("ErrMashalValueFailed: failed to marshal value") - ErrUnmarshalValueFailed = errors.New("ErrUnmarshalValueFailed: failed to unmarshal value") - ErrNamespaceMismatch = errors.New("ErrNamespaceMismatch: namespace mismatch") - ErrKIDMismatch = errors.New("ErrKIDMismatch: Key ID mismatch") - ErrKasURIMismatch = errors.New("ErrKasURIMismatch: KAS URI mismatch") - ErrInvalidOblTriParam = errors.New("ErrInvalidOblTriParam: either the obligation value, attribute value, or action provided was not found") - ErrCheckViolation = errors.New("ErrCheckViolation: check constraint violation") - ErrFqnMismatch = errors.New("ErrFqnMismatch: FQN mismatch") + ErrUniqueConstraintViolation = errors.New("ErrUniqueConstraintViolation: value must be unique") + ErrNotNullViolation = errors.New("ErrNotNullViolation: value cannot be null") + ErrForeignKeyViolation = errors.New("ErrForeignKeyViolation: value is referenced by another table") + ErrRestrictViolation = errors.New("ErrRestrictViolation: value cannot be deleted due to restriction") + ErrNotFound = errors.New("ErrNotFound: value not found") + ErrAttributeValueInactive = errors.New("ErrAttributeValueInactive: attribute value inactive") + ErrEnumValueInvalid = errors.New("ErrEnumValueInvalid: not a valid enum value") + ErrUUIDInvalid = errors.New("ErrUUIDInvalid: value not a valid UUID") + ErrMissingValue = errors.New("ErrMissingValue: value must be included") + ErrListLimitTooLarge = errors.New("ErrListLimitTooLarge: requested limit greater than configured maximum") + ErrTxBeginFailed = errors.New("ErrTxBeginFailed: failed to begin DB transaction") + ErrTxRollbackFailed = errors.New("ErrTxRollbackFailed: failed to rollback DB transaction") + ErrTxCommitFailed = errors.New("ErrTxCommitFailed: failed to commit DB transaction") + ErrSelectIdentifierInvalid = errors.New("ErrSelectIdentifierInvalid: invalid identifier value for select query") + ErrUnknownSelectIdentifier = errors.New("ErrUnknownSelectIdentifier: unknown identifier type for select query") + ErrIdentifierDeprecated = errors.New("ErrIdentifierDeprecated: identifier is deprecated") + ErrCannotUpdateToUnspecified = errors.New("ErrCannotUpdateToUnspecified: cannot update to unspecified value") + ErrKeyRotationFailed = errors.New("ErrTextKeyRotationFailed: key rotation failed") + ErrExpectedBase64EncodedValue = errors.New("ErrExpectedBase64EncodedValue: expected base64 encoded value") + ErrUnencryptedPrivateKey = errors.New("ErrUnencryptedPrivateKey: unencrypted private key not allowed") + ErrMarshalValueFailed = errors.New("ErrMashalValueFailed: failed to marshal value") + ErrUnmarshalValueFailed = errors.New("ErrUnmarshalValueFailed: failed to unmarshal value") + ErrNamespaceMismatch = errors.New("ErrNamespaceMismatch: namespace mismatch") + ErrKIDMismatch = errors.New("ErrKIDMismatch: Key ID mismatch") + ErrKasURIMismatch = errors.New("ErrKasURIMismatch: KAS URI mismatch") + ErrInvalidOblTriParam = errors.New("ErrInvalidOblTriParam: either the obligation value, attribute value, or action provided was not found") + ErrCheckViolation = errors.New("ErrCheckViolation: check constraint violation") + ErrFqnMismatch = errors.New("ErrFqnMismatch: FQN mismatch") + ErrUnsafeUpdateKeyExistingModeUnsupported = errors.New("ErrUnsafeUpdateKeyExistingModeUnsupported: existing key mode cannot be unsafely updated") + ErrUnsafeUpdateKeyTargetModeUnsupported = errors.New("ErrUnsafeUpdateKeyTargetModeUnsupported: target key mode cannot be used for an unsafe update") + ErrUnsafeUpdateKeyProviderConfigExistingMode = errors.New("ErrUnsafeUpdateKeyProviderConfigExistingMode: existing key mode cannot receive provider_config_id update with unspecified key mode") + ErrUnsafeUpdateKeyProviderConfigRequired = errors.New("ErrUnsafeUpdateKeyProviderConfigRequired: provider_config_id is required for requested key mode") + ErrUnsafeUpdateKeyProviderConfigNotAllowed = errors.New("ErrUnsafeUpdateKeyProviderConfigNotAllowed: provider_config_id must be empty for requested key mode") + ErrUnsafeUpdateKeyProviderConfigNotFound = errors.New("ErrUnsafeUpdateKeyProviderConfigNotFound: provider_config_id does not reference an existing provider config") ) // Get helpful error message for PostgreSQL violation @@ -111,35 +117,41 @@ func NewUniqueAlreadyExistsError(value string) error { } const ( - ErrTextCreationFailed = "resource creation failed" - ErrTextDeletionFailed = "resource deletion failed" - ErrTextDeactivationFailed = "resource deactivation failed" - ErrTextGetRetrievalFailed = "resource retrieval failed" - ErrTextListRetrievalFailed = "resource list retrieval failed" - ErrTextUpdateFailed = "resource update failed" - ErrTextNotFound = "resource not found" - ErrTextConflict = "resource unique field violation" - ErrTextRelationInvalid = "resource relation invalid" - ErrTextEnumValueInvalid = "enum value invalid" - ErrTextUUIDInvalid = "invalid input syntax for type uuid" - ErrTextRestrictViolation = "intended action would violate a restriction" - ErrTextFqnMissingValue = "FQN must specify a valid value and be of format 'https:///attr//value/'" - ErrTextListLimitTooLarge = "requested pagination limit must be less than or equal to configured limit" - ErrTextInvalidIdentifier = "value sepcified as the identifier is invalid" - ErrorIdentifierDeprecated = "identifier is deprecated" - ErrorTextUnknownIdentifier = "could not match identifier to known type" - ErrorTextUpdateToUnspecified = "cannot update to unspecified value" - ErrTextKeyRotationFailed = "key rotation failed" - ErrorTextExpectedBase64EncodedValue = "expected base64 encoded value" - ErrorTextUnencryptedPrivateKey = "unencrypted private key not allowed" - ErrorTextMarshalFailed = "failed to marshal value" - ErrorTextUnmarsalFailed = "failed to unmarshal value" - ErrorTextNamespaceMismatch = "namespace mismatch" - ErrorTextKasURIMismatch = "kas uri mismatch" - ErrorTextKIDMismatch = "key id mismatch" - ErrorTextInvalidOblTrigParam = "either the obligation value, attribute value, or action provided is invalid" - ErrorTextFqnMismatch = "fqn mismatch" - ErrorTextInactiveAttributeValue = "inactive attribute value" + ErrTextCreationFailed = "resource creation failed" + ErrTextDeletionFailed = "resource deletion failed" + ErrTextDeactivationFailed = "resource deactivation failed" + ErrTextGetRetrievalFailed = "resource retrieval failed" + ErrTextListRetrievalFailed = "resource list retrieval failed" + ErrTextUpdateFailed = "resource update failed" + ErrTextNotFound = "resource not found" + ErrTextConflict = "resource unique field violation" + ErrTextRelationInvalid = "resource relation invalid" + ErrTextEnumValueInvalid = "enum value invalid" + ErrTextUUIDInvalid = "invalid input syntax for type uuid" + ErrTextRestrictViolation = "intended action would violate a restriction" + ErrTextFqnMissingValue = "FQN must specify a valid value and be of format 'https:///attr//value/'" + ErrTextListLimitTooLarge = "requested pagination limit must be less than or equal to configured limit" + ErrTextInvalidIdentifier = "value sepcified as the identifier is invalid" + ErrorTextUnknownIdentifier = "could not match identifier to known type" + ErrorTextIdentifierDeprecated = "identifier is deprecated" + ErrorTextUpdateToUnspecified = "cannot update to unspecified value" + ErrTextKeyRotationFailed = "key rotation failed" + ErrorTextExpectedBase64EncodedValue = "expected base64 encoded value" + ErrorTextUnencryptedPrivateKey = "unencrypted private key not allowed" + ErrorTextMarshalFailed = "failed to marshal value" + ErrorTextUnmarsalFailed = "failed to unmarshal value" + ErrorTextNamespaceMismatch = "namespace mismatch" + ErrorTextKasURIMismatch = "kas uri mismatch" + ErrorTextKIDMismatch = "key id mismatch" + ErrorTextInvalidOblTrigParam = "either the obligation value, attribute value, or action provided is invalid" + ErrorTextFqnMismatch = "fqn mismatch" + ErrorTextInactiveAttributeValue = "inactive attribute value" + ErrorTextUnsafeUpdateKeyExistingModeUnsupported = "existing key mode cannot be updated" + ErrorTextUnsafeUpdateKeyTargetModeUnsupported = "target key mode cannot be used for an unsafe update" + ErrorTextUnsafeUpdateKeyProviderConfigExistingMode = "existing key mode cannot receive provider_config_id update with unspecified key mode" + ErrorTextUnsafeUpdateKeyProviderConfigRequired = "provider_config_id is required for requested key mode" + ErrorTextUnsafeUpdateKeyProviderConfigNotAllowed = "provider_config_id must be empty for requested key mode" + ErrorTextUnsafeUpdateKeyProviderConfigNotFound = "provider_config_id does not reference an existing provider config" ) func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackErr string, logs ...any) error { @@ -152,6 +164,10 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr l.ErrorContext(ctx, ErrTextNotFound, logs...) return connect.NewError(connect.CodeNotFound, errors.New(ErrTextNotFound)) } + if errors.Is(err, ErrUnsafeUpdateKeyProviderConfigNotFound) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyProviderConfigNotFound, logs...) + return connect.NewError(connect.CodeNotFound, errors.New(ErrorTextUnsafeUpdateKeyProviderConfigNotFound)) + } if errors.Is(err, ErrForeignKeyViolation) { l.ErrorContext(ctx, ErrTextRelationInvalid, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrTextRelationInvalid)) @@ -177,8 +193,8 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrTextInvalidIdentifier)) } if errors.Is(err, ErrIdentifierDeprecated) { - l.ErrorContext(ctx, ErrorIdentifierDeprecated, logs...) - return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorIdentifierDeprecated)) + l.ErrorContext(ctx, ErrorTextIdentifierDeprecated, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextIdentifierDeprecated)) } if errors.Is(err, ErrUnknownSelectIdentifier) { l.ErrorContext(ctx, ErrorTextUnknownIdentifier, logs...) @@ -224,6 +240,26 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr l.ErrorContext(ctx, ErrorTextInactiveAttributeValue, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextInactiveAttributeValue)) } + if errors.Is(err, ErrUnsafeUpdateKeyExistingModeUnsupported) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyExistingModeUnsupported, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnsafeUpdateKeyExistingModeUnsupported)) + } + if errors.Is(err, ErrUnsafeUpdateKeyTargetModeUnsupported) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyTargetModeUnsupported, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnsafeUpdateKeyTargetModeUnsupported)) + } + if errors.Is(err, ErrUnsafeUpdateKeyProviderConfigExistingMode) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyProviderConfigExistingMode, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnsafeUpdateKeyProviderConfigExistingMode)) + } + if errors.Is(err, ErrUnsafeUpdateKeyProviderConfigRequired) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyProviderConfigRequired, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnsafeUpdateKeyProviderConfigRequired)) + } + if errors.Is(err, ErrUnsafeUpdateKeyProviderConfigNotAllowed) { + l.ErrorContext(ctx, ErrorTextUnsafeUpdateKeyProviderConfigNotAllowed, logs...) + return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextUnsafeUpdateKeyProviderConfigNotAllowed)) + } if errors.Is(err, ErrNamespaceMismatch) { l.ErrorContext(ctx, ErrorTextNamespaceMismatch, logs...) return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextNamespaceMismatch)) diff --git a/service/pkg/db/errors_test.go b/service/pkg/db/errors_test.go index 30be9c9f5b..b23083b801 100644 --- a/service/pkg/db/errors_test.go +++ b/service/pkg/db/errors_test.go @@ -4,8 +4,10 @@ import ( "errors" "testing" + "connectrpc.com/connect" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5/pgconn" + "github.com/opentdf/platform/service/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -32,3 +34,65 @@ func Test_Is_Pg_Error(t *testing.T) { require.ErrorIs(t, isPgError(pgError), pgError) assert.Nil(t, isPgError(errors.New("test error"))) } + +func TestStatusifyErrorUnsafeUpdateKeyErrors(t *testing.T) { + testLogger, err := logger.NewLogger(logger.Config{ + Level: "error", + Output: "stdout", + Type: "json", + }) + require.NoError(t, err) + + testCases := []struct { + name string + err error + code connect.Code + text string + }{ + { + name: "provider config not found", + err: ErrUnsafeUpdateKeyProviderConfigNotFound, + code: connect.CodeNotFound, + text: ErrorTextUnsafeUpdateKeyProviderConfigNotFound, + }, + { + name: "existing mode unsupported", + err: ErrUnsafeUpdateKeyExistingModeUnsupported, + code: connect.CodeInvalidArgument, + text: ErrorTextUnsafeUpdateKeyExistingModeUnsupported, + }, + { + name: "target mode unsupported", + err: ErrUnsafeUpdateKeyTargetModeUnsupported, + code: connect.CodeInvalidArgument, + text: ErrorTextUnsafeUpdateKeyTargetModeUnsupported, + }, + { + name: "provider config existing mode", + err: ErrUnsafeUpdateKeyProviderConfigExistingMode, + code: connect.CodeInvalidArgument, + text: ErrorTextUnsafeUpdateKeyProviderConfigExistingMode, + }, + { + name: "provider config required", + err: ErrUnsafeUpdateKeyProviderConfigRequired, + code: connect.CodeInvalidArgument, + text: ErrorTextUnsafeUpdateKeyProviderConfigRequired, + }, + { + name: "provider config not allowed", + err: ErrUnsafeUpdateKeyProviderConfigNotAllowed, + code: connect.CodeInvalidArgument, + text: ErrorTextUnsafeUpdateKeyProviderConfigNotAllowed, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := StatusifyError(t.Context(), testLogger, tc.err, ErrTextUpdateFailed) + + require.Equal(t, tc.code, connect.CodeOf(got)) + require.Contains(t, got.Error(), tc.text) + }) + } +} diff --git a/service/policy/db/key_access_server_registry.go b/service/policy/db/key_access_server_registry.go index 82ae8e3cf7..f8632da0d4 100644 --- a/service/policy/db/key_access_server_registry.go +++ b/service/policy/db/key_access_server_registry.go @@ -15,6 +15,7 @@ import ( "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/keymanagement" "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/protocol/go/policy/unsafe" "github.com/opentdf/platform/service/pkg/db" @@ -552,6 +553,95 @@ func (c PolicyDBClient) UpdateKey(ctx context.Context, r *kasregistry.UpdateKeyR }) } +func (c PolicyDBClient) UnsafeUpdateKey(ctx context.Context, existing *policy.KasKey, r *unsafe.UnsafeUpdateKeyRequest) (*policy.KasKey, error) { + if existing.GetKey() == nil { + return nil, errors.New("existing key is nil") + } + + id := r.GetId() + if !pgtypeUUID(id).Valid { + return nil, db.ErrUUIDInvalid + } + if existing.GetKey().GetId() != id { + return nil, fmt.Errorf("key ID mismatch: expected %s, got %s", existing.GetKey().GetId(), id) + } + + params, err := validateUnsafeUpdateKey(existing, r) + if err != nil { + return nil, err + } + if params.ProviderConfigID.Valid { + _, err := c.GetProviderConfig(ctx, &keymanagement.GetProviderConfigRequest{ + Identifier: &keymanagement.GetProviderConfigRequest_Id{ + Id: r.GetProviderConfigId(), + }, + }) + if err != nil { + if errors.Is(err, db.ErrNotFound) { + err = db.ErrUnsafeUpdateKeyProviderConfigNotFound + } + return nil, err + } + } + + count, err := c.queries.unsafeUpdateKey(ctx, params) + if err != nil { + return nil, db.WrapIfKnownInvalidQueryErr(err) + } + if count == 0 { + return nil, db.ErrNotFound + } else if count > 1 { + c.logger.Warn("unsafeUpdateKey updated more than one row", slog.Int64("count", count)) + } + + return c.GetKey(ctx, &kasregistry.GetKeyRequest_Id{ + Id: id, + }) +} + +// validateUnsafeUpdateKey allows only remote and public-key-only keys to be +// updated, requiring a provider config when the resulting mode uses one. +func validateUnsafeUpdateKey(existing *policy.KasKey, r *unsafe.UnsafeUpdateKeyRequest) (unsafeUpdateKeyParams, error) { + existingMode := existing.GetKey().GetKeyMode() + params := unsafeUpdateKeyParams{ + ID: r.GetId(), + } + newKeyMode := pgtypeInt4(int32(r.GetTargetKeyMode()), true) + newProviderConfiguration := pgtypeUUID(r.GetProviderConfigId()) + + if existingMode != policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY && existingMode != policy.KeyMode_KEY_MODE_REMOTE { + return params, db.ErrUnsafeUpdateKeyExistingModeUnsupported + } + + switch r.GetTargetKeyMode() { + case policy.KeyMode_KEY_MODE_REMOTE: + if !newProviderConfiguration.Valid { + return params, db.ErrUnsafeUpdateKeyProviderConfigRequired + } + params.KeyMode = newKeyMode + params.ProviderConfigID = newProviderConfiguration + case policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY: + if r.GetProviderConfigId() != "" { + return params, db.ErrUnsafeUpdateKeyProviderConfigNotAllowed + } + params.KeyMode = newKeyMode + case policy.KeyMode_KEY_MODE_UNSPECIFIED: + if !newProviderConfiguration.Valid { + return params, db.ErrUnsafeUpdateKeyProviderConfigRequired + } + if existingMode != policy.KeyMode_KEY_MODE_REMOTE { + return params, db.ErrUnsafeUpdateKeyProviderConfigExistingMode + } + params.ProviderConfigID = newProviderConfiguration + case policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, policy.KeyMode_KEY_MODE_PROVIDER_ROOT_KEY: + fallthrough + default: + return params, fmt.Errorf("%w: %s", db.ErrUnsafeUpdateKeyTargetModeUnsupported, r.GetTargetKeyMode()) + } + + return params, nil +} + func (c PolicyDBClient) ListKeys(ctx context.Context, r *kasregistry.ListKeysRequest) (*kasregistry.ListKeysResponse, error) { limit, offset := c.getRequestedLimitOffset(r.GetPagination()) maxLimit := c.listCfg.limitMax diff --git a/service/policy/db/key_access_server_registry.sql.go b/service/policy/db/key_access_server_registry.sql.go index aff7345304..90d53a88b4 100644 --- a/service/policy/db/key_access_server_registry.sql.go +++ b/service/policy/db/key_access_server_registry.sql.go @@ -1303,6 +1303,35 @@ func (q *Queries) setBaseKey(ctx context.Context, keyAccessServerKeyID pgtype.UU return result.RowsAffected(), nil } +const unsafeUpdateKey = `-- name: unsafeUpdateKey :execrows +UPDATE key_access_server_keys +SET + key_mode = COALESCE($1, key_mode), + provider_config_id = $2 +WHERE id = $3 +` + +type unsafeUpdateKeyParams struct { + KeyMode pgtype.Int4 `json:"key_mode"` + ProviderConfigID pgtype.UUID `json:"provider_config_id"` + ID string `json:"id"` +} + +// unsafeUpdateKey +// +// UPDATE key_access_server_keys +// SET +// key_mode = COALESCE($1, key_mode), +// provider_config_id = $2 +// WHERE id = $3 +func (q *Queries) unsafeUpdateKey(ctx context.Context, arg unsafeUpdateKeyParams) (int64, error) { + result, err := q.db.Exec(ctx, unsafeUpdateKey, arg.KeyMode, arg.ProviderConfigID, arg.ID) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const updateKey = `-- name: updateKey :execrows UPDATE key_access_server_keys SET diff --git a/service/policy/db/key_access_server_registry_test.go b/service/policy/db/key_access_server_registry_test.go new file mode 100644 index 0000000000..e7decfd329 --- /dev/null +++ b/service/policy/db/key_access_server_registry_test.go @@ -0,0 +1,132 @@ +package db + +import ( + "testing" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/unsafe" + servicedb "github.com/opentdf/platform/service/pkg/db" + "github.com/stretchr/testify/require" +) + +const unsafeUpdateKeyTestUUID = "00000000-0000-0000-0000-000000000000" + +func TestValidateUnsafeUpdateKey(t *testing.T) { + tests := []struct { + name string + existingMode policy.KeyMode + requestMode policy.KeyMode + providerConfig string + wantMode pgtype.Int4 + wantProvider pgtype.UUID + wantErr error + }{ + { + name: "remote from public key only", + existingMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + requestMode: policy.KeyMode_KEY_MODE_REMOTE, + providerConfig: unsafeUpdateKeyTestUUID, + wantMode: pgtypeInt4(int32(policy.KeyMode_KEY_MODE_REMOTE), true), + wantProvider: pgtypeUUID(unsafeUpdateKeyTestUUID), + }, + { + name: "remote from remote", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_REMOTE, + providerConfig: unsafeUpdateKeyTestUUID, + wantMode: pgtypeInt4(int32(policy.KeyMode_KEY_MODE_REMOTE), true), + wantProvider: pgtypeUUID(unsafeUpdateKeyTestUUID), + }, + { + name: "public key only from remote", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + wantMode: pgtypeInt4(int32(policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY), true), + }, + { + name: "provider config only from remote", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_UNSPECIFIED, + providerConfig: unsafeUpdateKeyTestUUID, + wantProvider: pgtypeUUID(unsafeUpdateKeyTestUUID), + }, + { + name: "public key only from public key only", + existingMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + requestMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + wantMode: pgtypeInt4(int32(policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY), true), + }, + { + name: "provider config only from public key only rejected", + existingMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + requestMode: policy.KeyMode_KEY_MODE_UNSPECIFIED, + providerConfig: unsafeUpdateKeyTestUUID, + wantErr: servicedb.ErrUnsafeUpdateKeyProviderConfigExistingMode, + }, + { + name: "provider config update - invalid provider config", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_UNSPECIFIED, + providerConfig: "", + wantErr: servicedb.ErrUnsafeUpdateKeyProviderConfigRequired, + }, + { + name: "remote requires provider config", + existingMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + requestMode: policy.KeyMode_KEY_MODE_REMOTE, + wantErr: servicedb.ErrUnsafeUpdateKeyProviderConfigRequired, + }, + { + name: "provider config only update requires provider config", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_UNSPECIFIED, + wantErr: servicedb.ErrUnsafeUpdateKeyProviderConfigRequired, + }, + { + name: "public key only rejects provider config", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + providerConfig: unsafeUpdateKeyTestUUID, + wantErr: servicedb.ErrUnsafeUpdateKeyProviderConfigNotAllowed, + }, + { + name: "existing config root key rejected before request validation", + existingMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + requestMode: policy.KeyMode_KEY_MODE_REMOTE, + providerConfig: unsafeUpdateKeyTestUUID, + wantErr: servicedb.ErrUnsafeUpdateKeyExistingModeUnsupported, + }, + { + name: "unsupported target mode rejected", + existingMode: policy.KeyMode_KEY_MODE_REMOTE, + requestMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + wantErr: servicedb.ErrUnsafeUpdateKeyTargetModeUnsupported, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotParams, err := validateUnsafeUpdateKey(&policy.KasKey{ + Key: &policy.AsymmetricKey{ + KeyMode: tt.existingMode, + }, + }, &unsafe.UnsafeUpdateKeyRequest{ + Id: unsafeUpdateKeyTestUUID, + TargetKeyMode: tt.requestMode, + ProviderConfigId: tt.providerConfig, + }) + + if tt.wantErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, unsafeUpdateKeyTestUUID, gotParams.ID) + require.Equal(t, tt.wantMode, gotParams.KeyMode) + require.Equal(t, tt.wantProvider, gotParams.ProviderConfigID) + }) + } +} diff --git a/service/policy/db/queries/key_access_server_registry.sql b/service/policy/db/queries/key_access_server_registry.sql index d06bd5cb73..160e877c27 100644 --- a/service/policy/db/queries/key_access_server_registry.sql +++ b/service/policy/db/queries/key_access_server_registry.sql @@ -351,6 +351,13 @@ SET metadata = COALESCE(sqlc.narg('metadata'), metadata) WHERE id = $1; +-- name: unsafeUpdateKey :execrows +UPDATE key_access_server_keys +SET + key_mode = COALESCE(sqlc.narg('key_mode'), key_mode), + provider_config_id = sqlc.arg('provider_config_id') +WHERE id = @id; + -- name: keyAccessServerExists :one SELECT EXISTS ( SELECT 1 diff --git a/service/policy/unsafe/unsafe.go b/service/policy/unsafe/unsafe.go index e250854050..f52d9842be 100644 --- a/service/policy/unsafe/unsafe.go +++ b/service/policy/unsafe/unsafe.go @@ -2,7 +2,6 @@ package unsafe import ( "context" - "errors" "fmt" "log/slog" @@ -412,8 +411,70 @@ func (s *UnsafeService) UnsafeDeleteAttributeValue(ctx context.Context, req *con return connect.NewResponse(rsp), nil } -func (s *UnsafeService) UnsafeUpdateKey(_ context.Context, _ *connect.Request[unsafe.UnsafeUpdateKeyRequest]) (*connect.Response[unsafe.UnsafeUpdateKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("policy.unsafe.UnsafeService.UnsafeUpdateKey is not implemented")) +func (s *UnsafeService) UnsafeUpdateKey(ctx context.Context, req *connect.Request[unsafe.UnsafeUpdateKeyRequest]) (*connect.Response[unsafe.UnsafeUpdateKeyResponse], error) { + id := req.Msg.GetId() + + rsp := &unsafe.UnsafeUpdateKeyResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeUpdate, + ObjectType: audit.ObjectTypeKasRegistryKeys, + ObjectID: id, + } + + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + existing, err := txClient.GetKey(ctx, &kasregistry.GetKeyRequest_Id{Id: id}) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return db.StatusifyError(ctx, s.logger, err, db.ErrTextGetRetrievalFailed, slog.String("id", id)) + } + + auditParams.Original = unsafeUpdateKeyAuditValue(existing) + + updated, err := txClient.UnsafeUpdateKey(ctx, existing, req.Msg) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return db.StatusifyError(ctx, s.logger, err, db.ErrTextUpdateFailed, slog.String("id", id)) + } + + auditParams.Updated = unsafeUpdateKeyAuditValue(updated) + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Key = updated + return nil + }) + if err != nil { + return nil, err + } + + return connect.NewResponse(rsp), nil +} + +func unsafeUpdateKeyAuditValue(kasKey *policy.KasKey) *policy.KasKey { + if kasKey.GetKey() == nil { + return nil + } + + return &policy.KasKey{ + KasUri: kasKey.GetKasUri(), + Key: &policy.AsymmetricKey{ + KeyId: kasKey.GetKey().GetKeyId(), + KeyMode: kasKey.GetKey().GetKeyMode(), + ProviderConfig: unsafeUpdateKeyAuditProviderConfig(kasKey.GetKey().GetProviderConfig()), + }, + } +} + +func unsafeUpdateKeyAuditProviderConfig(providerConfig *policy.KeyProviderConfig) *policy.KeyProviderConfig { + if providerConfig == nil { + return nil + } + + return &policy.KeyProviderConfig{ + Id: providerConfig.GetId(), + Name: providerConfig.GetName(), + Manager: providerConfig.GetManager(), + } } func (s *UnsafeService) UnsafeDeleteKasKey(ctx context.Context, req *connect.Request[unsafe.UnsafeDeleteKasKeyRequest]) (*connect.Response[unsafe.UnsafeDeleteKasKeyResponse], error) { diff --git a/service/policy/unsafe/unsafe_test.go b/service/policy/unsafe/unsafe_test.go index 2d72785632..297409d220 100644 --- a/service/policy/unsafe/unsafe_test.go +++ b/service/policy/unsafe/unsafe_test.go @@ -1 +1,129 @@ package unsafe + +import ( + "testing" + + "buf.build/go/protovalidate" + "github.com/opentdf/platform/protocol/go/policy" + unsafepb "github.com/opentdf/platform/protocol/go/policy/unsafe" + "github.com/stretchr/testify/require" +) + +const ( + validUUID = "00000000-0000-0000-0000-000000000000" + ruleIDStringUUID = "string.uuid" + ruleIDKeyModeSupported = "enum.in" +) + +func getValidator() protovalidate.Validator { + v, err := protovalidate.New() + if err != nil { + panic(err) + } + return v +} + +func TestUnsafeUpdateKeyRequest(t *testing.T) { + testCases := []struct { + name string + req *unsafepb.UnsafeUpdateKeyRequest + expectError bool + errorMessage string + }{ + { + name: "valid update to remote mode key", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: validUUID, + }, + expectError: false, + }, + { + name: "valid update to public key only mode", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + }, + expectError: false, + }, + { + name: "valid provider config update with unspecified mode", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_UNSPECIFIED, + ProviderConfigId: validUUID, + }, + expectError: false, + }, + { + name: "invalid id", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: "not-a-uuid", + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: validUUID, + }, + expectError: true, + errorMessage: ruleIDStringUUID, + }, + { + name: "invalid provider config id", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: "not-a-uuid", + }, + expectError: true, + errorMessage: ruleIDStringUUID, + }, + { + name: "invalid provider config id with spaces only", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + ProviderConfigId: " ", + }, + expectError: true, + errorMessage: ruleIDStringUUID, + }, + { + name: "remote mode provider config requirement is service-owned", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_REMOTE, + }, + expectError: false, + }, + { + name: "public key only provider config prohibition is service-owned", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY, + ProviderConfigId: validUUID, + }, + expectError: false, + }, + { + name: "unsupported key mode", + req: &unsafepb.UnsafeUpdateKeyRequest{ + Id: validUUID, + TargetKeyMode: policy.KeyMode_KEY_MODE_CONFIG_ROOT_KEY, + }, + expectError: true, + errorMessage: ruleIDKeyModeSupported, + }, + } + + v := getValidator() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := v.Validate(tc.req) + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errorMessage) + return + } + require.NoError(t, err) + }) + } +}