From 5cf33fa8b34f8d2537bd3175d540b6dcd5cf05b2 Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:28:37 -0500 Subject: [PATCH 01/11] feat(credentials): Add PasswordAttributes credential type (#6110) --- globals/prefixes.go | 6 + internal/credential/public_ids.go | 12 ++ .../handlers/targets/target_service.go | 6 + .../resources/credentials/v1/credential.proto | 22 +++ .../resources/credentials/credential.pb.go | 137 +++++++++++++----- 5 files changed, 146 insertions(+), 37 deletions(-) diff --git a/globals/prefixes.go b/globals/prefixes.go index 801b75f5a2..facafb323b 100644 --- a/globals/prefixes.go +++ b/globals/prefixes.go @@ -83,6 +83,8 @@ const ( // UsernamePasswordCredentialPreviousPrefix is the previous prefix for // username/password creds UsernamePasswordCredentialPreviousPrefix = "cred" + // PasswordCredentialPrefix is the prefix for password creds + PasswordCredentialPrefix = "credp" // SshPrivateKeyCredentialPrefix is the prefix for SSH private key creds SshPrivateKeyCredentialPrefix = "credspk" // JsonCredentialPrefix is the prefix for generic JSON creds @@ -266,6 +268,10 @@ var prefixToResourceType = map[string]ResourceInfo{ Type: resource.Credential, Subtype: UnknownSubtype, }, + PasswordCredentialPrefix: { + Type: resource.Credential, + Subtype: UnknownSubtype, + }, SshPrivateKeyCredentialPrefix: { Type: resource.Credential, Subtype: UnknownSubtype, diff --git a/internal/credential/public_ids.go b/internal/credential/public_ids.go index 0a9dcc9289..9cee87659e 100644 --- a/internal/credential/public_ids.go +++ b/internal/credential/public_ids.go @@ -16,6 +16,7 @@ func init() { globals.RegisterPrefixToResourceInfo(globals.UsernamePasswordCredentialPrefix, resource.Credential, Domain, UsernamePasswordSubtype) globals.RegisterPrefixToResourceInfo(globals.UsernamePasswordDomainCredentialPrefix, resource.Credential, Domain, UsernamePasswordDomainSubtype) globals.RegisterPrefixToResourceInfo(globals.UsernamePasswordCredentialPreviousPrefix, resource.Credential, Domain, UsernamePasswordSubtype) + globals.RegisterPrefixToResourceInfo(globals.PasswordCredentialPrefix, resource.Credential, Domain, PasswordSubtype) globals.RegisterPrefixToResourceInfo(globals.SshPrivateKeyCredentialPrefix, resource.Credential, Domain, SshPrivateKeySubtype) globals.RegisterPrefixToResourceInfo(globals.JsonCredentialPrefix, resource.Credential, Domain, JsonSubtype) } @@ -25,6 +26,8 @@ const ( UsernamePasswordDomainSubtype = globals.Subtype("username_password_domain") + PasswordSubtype = globals.Subtype("password") + SshPrivateKeySubtype = globals.Subtype("ssh_private_key") JsonSubtype = globals.Subtype("json") @@ -48,6 +51,15 @@ func NewUsernamePasswordDomainCredentialId(ctx context.Context) (string, error) return id, nil } +// PasswordCredentialId generates a new public ID for a password credential. +func PasswordCredentialId(ctx context.Context) (string, error) { + id, err := db.NewPublicId(ctx, globals.PasswordCredentialPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, "credential.PasswordCredentialId") + } + return id, nil +} + // New SshPrivateKeyCredentialId generates a new public ID for an SSH private key credential. func NewSshPrivateKeyCredentialId(ctx context.Context) (string, error) { id, err := db.NewPublicId(ctx, globals.SshPrivateKeyCredentialPrefix) diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 8554c494c7..22d619c0a9 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -2060,6 +2060,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) @@ -2074,6 +2075,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break @@ -2100,6 +2102,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) @@ -2114,6 +2117,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break @@ -2144,6 +2148,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) @@ -2158,6 +2163,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) diff --git a/internal/proto/controller/api/resources/credentials/v1/credential.proto b/internal/proto/controller/api/resources/credentials/v1/credential.proto index c3bca93290..0243c7e28a 100644 --- a/internal/proto/controller/api/resources/credentials/v1/credential.proto +++ b/internal/proto/controller/api/resources/credentials/v1/credential.proto @@ -157,6 +157,28 @@ message UsernamePasswordDomainAttributes { ]; // @gotags: `class:"public"` } +// The attributes of a Password Credential. +message PasswordAttributes { + // Input only. The password associated with the credential. + google.protobuf.StringValue password = 10 [ + json_name = "password", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.password" + that: "Password" + } + ]; // @gotags: `class:"secret"` + + // Output only. The hmac value of the password. + string password_hmac = 20 [ + json_name = "password_hmac", + (custom_options.v1.mask_mapping) = { + this: "attributes.password_hmac" + that: "PasswordHmac" + } + ]; // @gotags: `class:"public"` +} + // The attributes of a SshPrivateKey Credential. message SshPrivateKeyAttributes { // The username associated with the credential. diff --git a/sdk/pbs/controller/api/resources/credentials/credential.pb.go b/sdk/pbs/controller/api/resources/credentials/credential.pb.go index ca908ac033..aa382ab11e 100644 --- a/sdk/pbs/controller/api/resources/credentials/credential.pb.go +++ b/sdk/pbs/controller/api/resources/credentials/credential.pb.go @@ -388,6 +388,61 @@ func (x *UsernamePasswordDomainAttributes) GetDomain() *wrapperspb.StringValue { return nil } +// The attributes of a Password Credential. +type PasswordAttributes struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Input only. The password associated with the credential. + Password *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=password,proto3" json:"password,omitempty" class:"secret"` // @gotags: `class:"secret"` + // Output only. The hmac value of the password. + PasswordHmac string `protobuf:"bytes,20,opt,name=password_hmac,proto3" json:"password_hmac,omitempty" class:"public"` // @gotags: `class:"public"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordAttributes) Reset() { + *x = PasswordAttributes{} + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordAttributes) ProtoMessage() {} + +func (x *PasswordAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordAttributes.ProtoReflect.Descriptor instead. +func (*PasswordAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{3} +} + +func (x *PasswordAttributes) GetPassword() *wrapperspb.StringValue { + if x != nil { + return x.Password + } + return nil +} + +func (x *PasswordAttributes) GetPasswordHmac() string { + if x != nil { + return x.PasswordHmac + } + return "" +} + // The attributes of a SshPrivateKey Credential. type SshPrivateKeyAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -407,7 +462,7 @@ type SshPrivateKeyAttributes struct { func (x *SshPrivateKeyAttributes) Reset() { *x = SshPrivateKeyAttributes{} - mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3] + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -419,7 +474,7 @@ func (x *SshPrivateKeyAttributes) String() string { func (*SshPrivateKeyAttributes) ProtoMessage() {} func (x *SshPrivateKeyAttributes) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3] + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -432,7 +487,7 @@ func (x *SshPrivateKeyAttributes) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyAttributes.ProtoReflect.Descriptor instead. func (*SshPrivateKeyAttributes) Descriptor() ([]byte, []int) { - return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{3} + return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{4} } func (x *SshPrivateKeyAttributes) GetUsername() *wrapperspb.StringValue { @@ -483,7 +538,7 @@ type JsonAttributes struct { func (x *JsonAttributes) Reset() { *x = JsonAttributes{} - mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[4] + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -495,7 +550,7 @@ func (x *JsonAttributes) String() string { func (*JsonAttributes) ProtoMessage() {} func (x *JsonAttributes) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[4] + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -508,7 +563,7 @@ func (x *JsonAttributes) ProtoReflect() protoreflect.Message { // Deprecated: Use JsonAttributes.ProtoReflect.Descriptor instead. func (*JsonAttributes) Descriptor() ([]byte, []int) { - return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{4} + return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{5} } func (x *JsonAttributes) GetObject() *structpb.Struct { @@ -575,7 +630,13 @@ const file_controller_api_resources_credentials_v1_credential_proto_rawDesc = "" "\rpassword_hmac\x18\x1e \x01(\tB,\xc2\xdd)(\n" + "\x18attributes.password_hmac\x12\fPasswordHmacR\rpassword_hmac\x12Y\n" + "\x06domain\x18( \x01(\v2\x1c.google.protobuf.StringValueB#\xa0\xda)\x01\xc2\xdd)\x1b\n" + - "\x11attributes.domain\x12\x06DomainR\x06domain\"\xee\x04\n" + + "\x11attributes.domain\x12\x06DomainR\x06domain\"\xcb\x01\n" + + "\x12PasswordAttributes\x12a\n" + + "\bpassword\x18\n" + + " \x01(\v2\x1c.google.protobuf.StringValueB'\xa0\xda)\x01\xc2\xdd)\x1f\n" + + "\x13attributes.password\x12\bPasswordR\bpassword\x12R\n" + + "\rpassword_hmac\x18\x14 \x01(\tB,\xc2\xdd)(\n" + + "\x18attributes.password_hmac\x12\fPasswordHmacR\rpassword_hmac\"\xee\x04\n" + "\x17SshPrivateKeyAttributes\x12a\n" + "\busername\x18\n" + " \x01(\v2\x1c.google.protobuf.StringValueB'\xa0\xda)\x01\xc2\xdd)\x1f\n" + @@ -609,43 +670,45 @@ func file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP() return file_controller_api_resources_credentials_v1_credential_proto_rawDescData } -var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_api_resources_credentials_v1_credential_proto_goTypes = []any{ (*Credential)(nil), // 0: controller.api.resources.credentials.v1.Credential (*UsernamePasswordAttributes)(nil), // 1: controller.api.resources.credentials.v1.UsernamePasswordAttributes (*UsernamePasswordDomainAttributes)(nil), // 2: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes - (*SshPrivateKeyAttributes)(nil), // 3: controller.api.resources.credentials.v1.SshPrivateKeyAttributes - (*JsonAttributes)(nil), // 4: controller.api.resources.credentials.v1.JsonAttributes - (*scopes.ScopeInfo)(nil), // 5: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 6: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 8: google.protobuf.Struct + (*PasswordAttributes)(nil), // 3: controller.api.resources.credentials.v1.PasswordAttributes + (*SshPrivateKeyAttributes)(nil), // 4: controller.api.resources.credentials.v1.SshPrivateKeyAttributes + (*JsonAttributes)(nil), // 5: controller.api.resources.credentials.v1.JsonAttributes + (*scopes.ScopeInfo)(nil), // 6: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 7: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 9: google.protobuf.Struct } var file_controller_api_resources_credentials_v1_credential_proto_depIdxs = []int32{ - 5, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 6, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue - 6, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue - 7, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp - 7, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp - 8, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct + 6, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 7, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue + 7, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue + 8, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp + 8, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp + 9, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.credentials.v1.Credential.username_password_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordAttributes - 3, // 7: controller.api.resources.credentials.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.credentials.v1.SshPrivateKeyAttributes - 4, // 8: controller.api.resources.credentials.v1.Credential.json_attributes:type_name -> controller.api.resources.credentials.v1.JsonAttributes + 4, // 7: controller.api.resources.credentials.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.credentials.v1.SshPrivateKeyAttributes + 5, // 8: controller.api.resources.credentials.v1.Credential.json_attributes:type_name -> controller.api.resources.credentials.v1.JsonAttributes 2, // 9: controller.api.resources.credentials.v1.Credential.username_password_domain_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes - 6, // 10: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue - 6, // 11: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue - 6, // 12: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.username:type_name -> google.protobuf.StringValue - 6, // 13: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.password:type_name -> google.protobuf.StringValue - 6, // 14: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.domain:type_name -> google.protobuf.StringValue - 6, // 15: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue - 6, // 16: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue - 6, // 17: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue - 8, // 18: controller.api.resources.credentials.v1.JsonAttributes.object:type_name -> google.protobuf.Struct - 19, // [19:19] is the sub-list for method output_type - 19, // [19:19] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 7, // 10: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue + 7, // 11: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue + 7, // 12: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.username:type_name -> google.protobuf.StringValue + 7, // 13: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.password:type_name -> google.protobuf.StringValue + 7, // 14: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.domain:type_name -> google.protobuf.StringValue + 7, // 15: controller.api.resources.credentials.v1.PasswordAttributes.password:type_name -> google.protobuf.StringValue + 7, // 16: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue + 7, // 17: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue + 7, // 18: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue + 9, // 19: controller.api.resources.credentials.v1.JsonAttributes.object:type_name -> google.protobuf.Struct + 20, // [20:20] is the sub-list for method output_type + 20, // [20:20] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_controller_api_resources_credentials_v1_credential_proto_init() } @@ -666,7 +729,7 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_api_resources_credentials_v1_credential_proto_rawDesc), len(file_controller_api_resources_credentials_v1_credential_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, From 8bce75093352b51f8eeba206c136967ff1ee9938 Mon Sep 17 00:00:00 2001 From: Justin Nguyen Date: Fri, 3 Oct 2025 17:13:41 -0500 Subject: [PATCH 02/11] feat(db): Add new tables for password credential type (#6101) --- ...edential_static_password_credential.up.sql | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql new file mode 100644 index 0000000000..a9ab5401bf --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql @@ -0,0 +1,118 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + create table credential_static_password_credential ( + public_id wt_public_id primary key, + store_id wt_public_id not null + constraint credential_static_store_fkey + references credential_static_store(public_id) + on delete cascade + on update cascade, + name wt_name null, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + password_encrypted bytea not null, + constraint password_encrypted_must_not_be_empty + check( + length(password_encrypted) > 0 + ), + password_hmac bytea not null, + constraint password_hmac_must_not_be_empty + check( + length(password_hmac) > 0 + ), + key_id kms_private_id not null + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade, + project_id wt_public_id not null, + constraint credential_static_fkey + foreign key (project_id, store_id, public_id) + references credential_static (project_id, store_id, public_id) + on delete cascade + on update cascade, + constraint credential_static_password_credential_store_id_name_uq + unique (store_id, name), + constraint credential_static_password_store_id_public_id_uq + unique (store_id, public_id) + ); + comment on table credential_static_password_credential is + 'credential_static_password_credential is a table where each row is a resource that represents a static password credential. ' + 'It is a credential_static subtype and an aggregate root.'; + + create trigger default_create_time_column before insert on credential_static_password_credential + for each row execute procedure default_create_time(); + + create trigger insert_credential_static_subtype before insert on credential_static_password_credential + for each row execute procedure insert_credential_static_subtype(); + + create trigger immutable_columns before update on credential_static_password_credential + for each row execute procedure immutable_columns('public_id', 'store_id', 'project_id', 'create_time'); + + create trigger update_credential_static_table_update_time before update on credential_static_password_credential + for each row execute procedure update_credential_static_table_update_time(); + + create trigger update_time_column before update on credential_static_password_credential + for each row execute procedure update_time_column(); + + create trigger update_version_column after update on credential_static_password_credential + for each row execute procedure update_version_column(); + + create trigger delete_credential_static_subtype after delete on credential_static_password_credential + for each row execute procedure delete_credential_static_subtype(); + + create table credential_static_password_credential_hst ( + public_id wt_public_id not null, + name wt_name, + description wt_description, + project_id wt_public_id not null, + store_id wt_public_id not null, + password_hmac bytea not null, + history_id wt_url_safe_id default wt_url_safe_id() primary key + constraint credential_static_history_base_fkey + references credential_static_history_base (history_id) + on delete cascade + on update cascade, + valid_range tstzrange not null default tstzrange (current_timestamp, null), + constraint credential_static_password_credential_hst_valid_range_excl + exclude using gist (public_id with =, valid_range with &&) + ); + comment on table credential_static_password_credential_hst is + 'credential_static_password_credential_hst is a history table where each row contains the values from a row ' + 'in the credential_static_password_credential table during the time range in the valid_range column.'; + + create trigger insert_credential_static_history_subtype before insert on credential_static_password_credential_hst + for each row execute function insert_credential_static_history_subtype(); + + create trigger hst_on_insert after insert on credential_static_password_credential + for each row execute function hst_on_insert(); + + create trigger hst_on_update after update on credential_static_password_credential + for each row execute function hst_on_update(); + + create trigger hst_on_delete after delete on credential_static_password_credential + for each row execute function hst_on_delete(); + + create trigger delete_credential_static_history_subtype after delete on credential_static_password_credential_hst + for each row execute function delete_credential_static_history_subtype(); + + create table credential_static_password_credential_deleted ( + public_id wt_public_id primary key, + delete_time wt_timestamp not null + ); + comment on table credential_static_password_credential_deleted is + 'credential_static_password_credential_deleted holds the ID and delete_time ' + 'of every deleted static password credential. ' + 'It is automatically trimmed of records older than 30 days by a job.'; + + create trigger insert_deleted_id after delete on credential_static_password_credential + for each row execute procedure insert_deleted_id('credential_static_password_credential_deleted'); + + create index credential_static_password_credential_deleted_delete_time_idx on credential_static_password_credential_deleted (delete_time); + +commit; From 1c2bc2cc74e12f9c7d7a700eb8145a41bcbb7ece Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:06:21 -0500 Subject: [PATCH 03/11] feat(db): Password credential migration (#6125) --- ...edential_static_password_credential.up.sql | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql index a9ab5401bf..3ce6a865c4 100644 --- a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql @@ -115,4 +115,98 @@ begin; create index credential_static_password_credential_deleted_delete_time_idx on credential_static_password_credential_deleted (delete_time); + create view credential_static_password_credential_hst_aggregate as + select + rsc.recording_id, + spc.public_id, + spc.name, + spc.description, + spc.password_hmac, + css.public_id as store_public_id, + css.project_id as store_project_id, + css.name as store_name, + css.description as store_description, + string_agg(distinct rsc.credential_purpose, '|') as purposes + from + credential_static_password_credential_hst as spc + left join recording_static_credential as rsc on spc.history_id = rsc.credential_static_hst_id + join credential_static_store_hst as css on rsc.credential_static_store_hst_id = css.history_id + group by spc.history_id, rsc.recording_id, css.history_id; + comment on view credential_static_password_credential_hst_aggregate is + 'credential_static_password_credential_hst_aggregate contains the password credential history data along with its store and purpose data.'; + + -- This constraint replaces the previous constraint created in 98/01_credential_static_username_password_domain_credential.up.sql + alter table credential_type_enm + drop constraint only_predefined_credential_types_allowed; + + alter table credential_type_enm + add constraint only_predefined_credential_types_allowed + check ( + name in ( + 'unspecified', + 'username_password', + 'ssh_private_key', + 'ssh_certificate', + 'username_password_domain', + 'password' + ) + ); + + insert into credential_type_enm (name) + values ('password'); + + -- This function replaces the previous function created in 98/01_credential_static_username_password_domain_credential.up.sql + create or replace function insert_recording_static_credentials() returns trigger + as $$ + begin + with + session_recording(session_id, recording_id) as ( + select session_id, public_id + from recording_session + where session_id = new.session_id + ), + session_static_creds(cred_id, purpose, recording_id) as ( + select credential_static_id, credential_purpose, recording_id + from session_credential_static + join session_recording using (session_id) + ), + static_cred_history(public_id, store_id, cred_hst_id, valid_range) as ( + select public_id, store_id, history_id, valid_range + from credential_static_json_credential_hst + union + select public_id, store_id, history_id, valid_range + from credential_static_ssh_private_key_credential_hst + union + select public_id, store_id, history_id, valid_range + from credential_static_username_password_credential_hst + union + select public_id, store_id, history_id, valid_range + from credential_static_username_password_domain_credential_hst + union + select public_id, store_id, history_id, valid_range + from credential_static_password_credential_hst + ), + final(recording_id, cred_id, store_id, cred_hst_id, store_hst_id, cred_purpose) as ( + select ssc.recording_id, sc.public_id, sc.store_id, sc.cred_hst_id, store_hst.history_id, ssc.purpose + from static_cred_history as sc + join credential_static_store_hst as store_hst on sc.store_id = store_hst.public_id + and store_hst.valid_range @> current_timestamp + join session_static_creds as ssc on sc.public_id = ssc.cred_id + where sc.public_id in (select cred_id from session_static_creds) + and sc.valid_range @> current_timestamp + ) + insert into recording_static_credential + (recording_id, credential_static_store_hst_id, credential_static_hst_id, credential_purpose) + select recording_id, store_hst_id, cred_hst_id, cred_purpose + from final; + + return new; + end; + $$ language plpgsql; + + comment on function insert_recording_static_credentials is + 'insert_recording_static_credentials is an after insert trigger for the recording_session table.'; + + insert into oplog_ticket (name, version) + values ('credential_static_password_credential', 1); commit; From 3ab1019bc2c24d680a5d019e074e012320e76283 Mon Sep 17 00:00:00 2001 From: Justin Nguyen Date: Fri, 10 Oct 2025 10:34:10 -0500 Subject: [PATCH 04/11] feat(db): Add db migration to support vault password credential (#6129) --- ...username_password_domain_credential.up.sql | 4 +- ...edential_static_password_credential.up.sql | 2 +- ...2_credential_vault_password_library.up.sql | 223 ++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql diff --git a/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql b/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql index d22c1f8b7b..2054192b02 100644 --- a/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql @@ -154,7 +154,7 @@ begin; 'credential_static_username_password_domain_credential_hst_aggregate contains the username password credential history data along with its store and purpose data.'; - -- This constraint replaces the previous constraint created in 63/01_credential_vault_ssh_cert_library.up.sql + -- This constraint is replaced in 99/01_credential_static_password_credential.up.sql alter table credential_type_enm drop constraint only_predefined_credential_types_allowed; @@ -174,7 +174,7 @@ begin; values ('username_password_domain'); --- This function replaces the previous function created in 71/14_recording_static_credential.up.sql +-- This function is updated in 99/01_credential_static_password_credential.up.sql. create or replace function insert_recording_static_credentials() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql index 3ce6a865c4..d227d6a10e 100644 --- a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql @@ -135,7 +135,7 @@ begin; comment on view credential_static_password_credential_hst_aggregate is 'credential_static_password_credential_hst_aggregate contains the password credential history data along with its store and purpose data.'; - -- This constraint replaces the previous constraint created in 98/01_credential_static_username_password_domain_credential.up.sql + -- This constraint replaces the previous constraint created in 98/01_credential_static_username_password_domain_credential.up.sql alter table credential_type_enm drop constraint only_predefined_credential_types_allowed; diff --git a/internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql b/internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql new file mode 100644 index 0000000000..80dd3a5f5d --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql @@ -0,0 +1,223 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + create table credential_vault_library_password_mapping_override ( + library_id wt_public_id primary key + constraint credential_vault_library_fkey + references credential_vault_library (public_id) + on delete cascade + on update cascade + constraint credential_vault_library_mapping_override_fkey + references credential_vault_library_mapping_override (library_id) + on delete cascade + on update cascade, + password_attribute wt_sentinel + default wt_to_sentinel('no override') + not null + ); + comment on table credential_vault_library_password_mapping_override is + 'credential_vault_library_password_mapping_override is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a password credential type ' + 'for a vault credential library.'; + + create trigger insert_credential_vault_library_mapping_override_subtype before insert on credential_vault_library_password_mapping_override + for each row execute procedure insert_credential_vault_library_mapping_override_subtype(); + + create trigger delete_credential_vault_library_mapping_override_subtype after delete on credential_vault_library_password_mapping_override + for each row execute procedure delete_credential_vault_library_mapping_override_subtype(); + + + -- Replaces view from 98/02_username_password_domain_vault.up.sql + drop view credential_vault_library_issue_credentials; + create view credential_vault_library_issue_credentials as + with + username_password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_mapping_override + ), + username_password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_domain_mapping_ovrd + ), + password_override (library_id, password_attribute) as ( + select library_id, + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_password_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + null as key_type, + null as key_bits, + null as username, + null as ttl, + null as key_id, + null as critical_options, + null as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + coalesce(upasso.username_attribute, sshpk.username_attribute, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute, po.password_attribute) + as password_attribute, + pd.domain_attribute as domain_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, + 'generic' as cred_lib_type, -- used to switch on + null as additional_valid_principals + from credential_vault_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join username_password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id + left join username_password_domain_override pd + on library.public_id = pd.library_id + left join password_override po + on library.public_id = po.library_id + union + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + null as http_method, + null as http_request_body, + library.credential_type as credential_type, + library.key_type as key_type, + library.key_bits as key_bits, + library.username as username, + library.ttl as ttl, + library.key_id as key_id, + library.critical_options as critical_options, + library.extensions as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + null as username_attribute, + null as password_attribute, + null as domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type, -- used to switch on + additional_valid_principals as additional_valid_principals + from credential_vault_ssh_cert_library library + join credential_vault_store_client store + on library.store_id = store.public_id; + comment on view credential_vault_library_issue_credentials is + 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' + 'This view should not be used to retrieve data which will be returned external to boundary.'; + + + -- Replaces view created in 98/02_username_password_domain_vault.up.sql + drop view credential_vault_library_list_lookup; + create view credential_vault_library_list_lookup as + with + username_password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_mapping_override + ), + username_password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_domain_mapping_ovrd + ), + password_override (library_id, password_attribute) as ( + select library_id, + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_password_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + coalesce(upasso.username_attribute, sshpk.username_attribute, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute, po.password_attribute) + as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, + pd.domain_attribute as domain_attribute + from credential_vault_library library + left join username_password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id + left join username_password_domain_override pd + on library.public_id = pd.library_id + left join password_override po + on library.public_id = po.library_id; + comment on view credential_vault_library_list_lookup is + 'credential_vault_library_list_lookup is a view where each row contains a credential library and any of library''s credential mapping overrides. ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + +commit; From 4a8009fec4ba357ea7630e070526e8c64e17a99e Mon Sep 17 00:00:00 2001 From: Justin Nguyen Date: Wed, 15 Oct 2025 14:31:25 -0500 Subject: [PATCH 05/11] feat(credential/vault): Implement repository for vault password credential (#6140) --- globals/credentials.go | 1 + internal/credential/credential.go | 7 + .../credential/vault/internal/password/doc.go | 6 + .../vault/internal/password/password.go | 118 +++++++++ .../vault/internal/password/password_test.go | 176 +++++++++++++ .../credential/vault/mapping_overriders.go | 61 +++++ .../vault/mapping_overriders_test.go | 20 ++ internal/credential/vault/private_library.go | 42 ++++ .../credential/vault/private_library_test.go | 238 ++++++++++++++++++ .../vault/repository_credential_library.go | 8 + .../repository_credential_library_test.go | 202 ++++++++++++++- internal/credential/vault/store/vault.pb.go | 125 ++++++--- .../credential/vault/store/v1/vault.proto | 16 ++ 13 files changed, 990 insertions(+), 30 deletions(-) create mode 100644 internal/credential/vault/internal/password/doc.go create mode 100644 internal/credential/vault/internal/password/password.go create mode 100644 internal/credential/vault/internal/password/password_test.go diff --git a/globals/credentials.go b/globals/credentials.go index ee1f2bbd47..988af79ae5 100644 --- a/globals/credentials.go +++ b/globals/credentials.go @@ -11,6 +11,7 @@ const ( UnspecifiedCredentialType CredentialType = "unspecified" UsernamePasswordCredentialType CredentialType = "username_password" UsernamePasswordDomainCredentialType CredentialType = "username_password_domain" + PasswordCredentialType CredentialType = "password" SshPrivateKeyCredentialType CredentialType = "ssh_private_key" SshCertificateCredentialType CredentialType = "ssh_certificate" JsonCredentialType CredentialType = "json" diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 03bf3d5c5a..f28ee9b925 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -143,6 +143,13 @@ type UsernamePasswordDomain interface { Domain() string } +// PasswordCredential is a credential containing a username and a password. +// Does not follow naming convention to avoid conflict with existing Password type. +type PasswordCredential interface { + Credential + Password() Password +} + // SshPrivateKey is a credential containing a username an SSH private key and // an optional private key passphrase. type SshPrivateKey interface { diff --git a/internal/credential/vault/internal/password/doc.go b/internal/credential/vault/internal/password/doc.go new file mode 100644 index 0000000000..78e6efc184 --- /dev/null +++ b/internal/credential/vault/internal/password/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package password provides access to the password +// stored in a Vault secret. +package password diff --git a/internal/credential/vault/internal/password/password.go b/internal/credential/vault/internal/password/password.go new file mode 100644 index 0000000000..f0f13bca85 --- /dev/null +++ b/internal/credential/vault/internal/password/password.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package password + +import ( + "strings" + + "github.com/mitchellh/pointerstructure" +) + +type ( + data map[string]any + + // extractFunc attempts to extract the password + // from sd using the provided attribute names, using a known + // Vault data response format. + extractFunc func(sd data, passwordAttr string) string +) + +// Extract attempts to extract the values of the password +// stored within the provided data using the given attribute names. +// +// Extract does not return partial results, i.e. if one of the attributes +// were extracted but not the other ("") will be returned. +func Extract(d data, passwordAttr string) string { + for _, f := range []extractFunc{ + defaultExtract, + kv2Extract, + } { + password := f(d, passwordAttr) + if password != "" { + // got valid password from secret + return password + } + } + + return "" +} + +// defaultExtract looks for the passwordAttr in the data map +func defaultExtract(sd data, passwordAttr string) (password string) { + if sd == nil { + // nothing to do return early + return "" + } + + var p any + switch { + case strings.HasPrefix(passwordAttr, "/"): + var err error + p, err = pointerstructure.Get(sd, passwordAttr) + if err != nil { + return "" + } + + default: + p = sd[passwordAttr] + } + + if p, ok := p.(string); ok { + password = p + } + + return password +} + +// kv2Extract looks for the the passwordAttr in the embedded +// 'data' field within the data map. +// +// Additionally it validates the data is in the expected KV-v2 format: +// +// { +// "data": {}, +// "metadata: {} +// } +// +// If the format does not match, it returns (""). See: +// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1 +func kv2Extract(sd data, passwordAttr string) (password string) { + if sd == nil { + // nothing to do return early + return "" + } + + var data, metadata map[string]any + for k, v := range sd { + switch k { + case "data": + var ok bool + if data, ok = v.(map[string]any); !ok { + // data field should be of type map[string]interface{} in KV-v2 + return "" + } + case "metadata": + var ok bool + if metadata, ok = v.(map[string]any); !ok { + // metadata field should be of type map[string]interface{} in KV-v2 + return "" + } + default: + // secretData contains a non valid KV-v2 top level field + return "" + } + } + if data == nil || metadata == nil { + // missing required KV-v2 field + return "" + } + + if p, ok := data[passwordAttr]; ok { + if p, ok := p.(string); ok { + password = p + } + } + + return password +} diff --git a/internal/credential/vault/internal/password/password_test.go b/internal/credential/vault/internal/password/password_test.go new file mode 100644 index 0000000000..6f05cc6538 --- /dev/null +++ b/internal/credential/vault/internal/password/password_test.go @@ -0,0 +1,176 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package password + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBaseToPass(t *testing.T) { + t.Parallel() + + type args struct { + s data + pAttr string + } + type pass struct { + pass string + } + tests := []struct { + name string + given args + want pass + }{ + { + name: "nil-input", + want: pass{pass: ""}, + }, + { + name: "no-input", + given: args{}, + want: pass{pass: ""}, + }, + { + name: "no-secret", + given: args{ + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "no-match-password-secret", + given: args{ + s: data{ + "password-wrong": "pass", + }, + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "valid-default", + given: args{ + s: data{ + "password": "pass", + }, + pAttr: "password", + }, + want: pass{pass: "pass"}, + }, + { + name: "no-match-password-secret-kv2", + given: args{ + s: data{ + "metadata": map[string]any{}, + "data": map[string]any{ + "password-wrong": "pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "valid-kv2", + given: args{ + s: data{ + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: "pass"}, + }, + { + name: "no-metadata-kv2", + given: args{ + s: data{ + "data": map[string]any{ + "password": "pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "invalid-metadata-kv2", + given: args{ + s: data{ + "metadata": "string", + "data": map[string]any{ + "password": "pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "invalid-field-kv2", + given: args{ + s: data{ + "invalid": map[string]any{}, + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: ""}, + }, + { + name: "valid-order-default-first", + given: args{ + s: data{ + "password": "default-pass", + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "kv2-pass", + }, + }, + pAttr: "password", + }, + want: pass{pass: "default-pass"}, + }, + { + name: "json-pointer-password", + given: args{ + s: data{ + "testing": map[string]any{ + "my-password": "secret", + }, + }, + pAttr: "/testing/my-password", + }, + want: pass{pass: "secret"}, + }, + { + name: "deep-json-pointer", + given: args{ + s: data{ + "first-path": map[string]any{ + "deeper-path": map[string]any{ + "my-password": "deeper-secret", + }, + }, + }, + pAttr: "/first-path/deeper-path/my-password", + }, + want: pass{pass: "deeper-secret"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + pass := Extract(tt.given.s, tt.given.pAttr) + assert.Equal(tt.want.pass, pass) + }) + } +} diff --git a/internal/credential/vault/mapping_overriders.go b/internal/credential/vault/mapping_overriders.go index eef7559e97..e7f5e9416f 100644 --- a/internal/credential/vault/mapping_overriders.go +++ b/internal/credential/vault/mapping_overriders.go @@ -21,6 +21,8 @@ func validMappingOverride(m MappingOverride, ct globals.CredentialType) bool { return ct == globals.UsernamePasswordCredentialType case *UsernamePasswordDomainOverride: return ct == globals.UsernamePasswordDomainCredentialType + case *PasswordOverride: + return ct == globals.PasswordCredentialType case *SshPrivateKeyOverride: return ct == globals.SshPrivateKeyCredentialType default: @@ -171,6 +173,65 @@ func (o *UsernamePasswordDomainOverride) SetTableName(n string) { o.tableName = n } +// A PasswordOverride contains optional values for overriding the +// default mappings used to map a Vault secret to a Password credential +// type for the credential library that owns it. +type PasswordOverride struct { + *store.PasswordOverride + tableName string `gorm:"-"` +} + +var _ MappingOverride = (*PasswordOverride)(nil) + +// NewPasswordOverride creates a new in memory PasswordOverride. +// WithOverrideAttribute and WithOverridePasswordAttribute are the +// only valid options. All other options are ignored. +func NewPasswordOverride(opt ...Option) *PasswordOverride { + opts := getOpts(opt...) + o := &PasswordOverride{ + PasswordOverride: &store.PasswordOverride{ + PasswordAttribute: sanitize.String(opts.withOverridePasswordAttribute), + }, + } + return o +} + +func allocPasswordOverride() *PasswordOverride { + return &PasswordOverride{ + PasswordOverride: &store.PasswordOverride{}, + } +} + +func (o *PasswordOverride) clone() MappingOverride { + cp := proto.Clone(o.PasswordOverride) + return &PasswordOverride{ + PasswordOverride: cp.(*store.PasswordOverride), + } +} + +func (o *PasswordOverride) setLibraryId(i string) { + o.LibraryId = i +} + +func (o *PasswordOverride) sanitize() { + if sentinel.Is(o.PasswordAttribute) { + o.PasswordAttribute = "" + } +} + +// TableName returns the table name. +func (o *PasswordOverride) TableName() string { + if o.tableName != "" { + return o.tableName + } + return "credential_vault_library_password_mapping_override" +} + +// SetTableName sets the table name. +func (o *PasswordOverride) SetTableName(n string) { + o.tableName = n +} + // A SshPrivateKeyOverride contains optional values for overriding the // default mappings used to map a Vault secret to a SshPrivateKey credential // type for the credential library that owns it. diff --git a/internal/credential/vault/mapping_overriders_test.go b/internal/credential/vault/mapping_overriders_test.go index 4199602fa7..94668d1498 100644 --- a/internal/credential/vault/mapping_overriders_test.go +++ b/internal/credential/vault/mapping_overriders_test.go @@ -45,6 +45,11 @@ func TestValidMappingOverrides(t *testing.T) { ct: globals.UsernamePasswordDomainCredentialType, want: true, }, + { + m: nil, + ct: globals.PasswordCredentialType, + want: true, + }, { m: unknownMapper(1), ct: globals.UnspecifiedCredentialType, @@ -60,6 +65,11 @@ func TestValidMappingOverrides(t *testing.T) { ct: globals.UsernamePasswordDomainCredentialType, want: false, }, + { + m: unknownMapper(1), + ct: globals.PasswordCredentialType, + want: false, + }, { m: allocUsernamePasswordOverride(), ct: globals.UnspecifiedCredentialType, @@ -80,6 +90,16 @@ func TestValidMappingOverrides(t *testing.T) { ct: globals.UsernamePasswordDomainCredentialType, want: true, }, + { + m: allocPasswordOverride(), + ct: globals.UnspecifiedCredentialType, + want: false, + }, + { + m: allocPasswordOverride(), + ct: globals.PasswordCredentialType, + want: true, + }, { m: allocSshPrivateKeyOverride(), ct: globals.UnspecifiedCredentialType, diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 5a00b1a48f..7843c87b21 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -23,6 +23,7 @@ import ( ldapv3 "github.com/go-ldap/ldap/v3" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/internal/password" "github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey" "github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword" "github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassworddomain" @@ -66,6 +67,8 @@ func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) { return baseToSshPriKey(ctx, bc) case globals.UsernamePasswordDomainCredentialType: return baseToUsrPassDomain(ctx, bc) + case globals.PasswordCredentialType: + return baseToPass(ctx, bc) } return bc, nil } @@ -166,6 +169,45 @@ func baseToUsrPassDomain(ctx context.Context, bc *baseCred) (*usrPassDomainCred, }, nil } +var _ credential.PasswordCredential = (*passCred)(nil) + +type passCred struct { + *baseCred + password credential.Password +} + +func (c *passCred) Password() credential.Password { return c.password } + +func baseToPass(ctx context.Context, bc *baseCred) (*passCred, error) { + switch { + case bc == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred")) + case bc.lib == nil: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib")) + case bc.Library().CredentialType() != globals.PasswordCredentialType: + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) + } + + lib, ok := bc.lib.(*genericIssuingCredentialLibrary) + if !ok { + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary")) + } + + pAttr := lib.PasswordAttribute + if pAttr == "" { + pAttr = "password" + } + password := password.Extract(bc.secretData, pAttr) + if password == "" { + return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping)) + } + + return &passCred{ + baseCred: bc, + password: credential.Password(password), + }, nil +} + var _ credential.SshPrivateKey = (*sshPrivateKeyCred)(nil) type sshPrivateKeyCred struct { diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index a57d4e2ab0..266e472198 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -344,6 +344,37 @@ func TestRepository_getPrivateLibraries(t *testing.T) { req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.BrokeredPurpose} requests = append(requests, req) } + { + opts := []Option{ + WithCredentialType(globals.PasswordCredentialType), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.BrokeredPurpose} + requests = append(requests, req) + } + { + opts := []Option{ + WithCredentialType(globals.PasswordCredentialType), + WithMappingOverride(NewPasswordOverride( + WithOverridePasswordAttribute("test-password"), + )), + } + libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...) + assert.NoError(err) + require.NotNil(libIn) + lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) + assert.NoError(err) + require.NotNil(lib) + libs[lib.GetPublicId()] = lib + req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.BrokeredPurpose} + requests = append(requests, req) + } { opts := []Option{ WithCredentialType(globals.SshPrivateKeyCredentialType), @@ -460,6 +491,8 @@ func TestRepository_getPrivateLibraries(t *testing.T) { assert.Equal(w.UsernameAttribute, got.UsernameAttribute) assert.Equal(w.PasswordAttribute, got.PasswordAttribute) assert.Equal(w.DomainAttribute, got.DomainAttribute) + case *PasswordOverride: + assert.Equal(w.PasswordAttribute, got.PasswordAttribute) case *SshPrivateKeyOverride: assert.Equal(w.UsernameAttribute, got.UsernameAttribute) assert.Equal(w.PrivateKeyAttribute, got.PrivateKeyAttribute) @@ -1424,6 +1457,211 @@ func TestBaseToUsrPassDomain(t *testing.T) { } } +func TestBaseToPass(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + given *baseCred + want *passCred + wantErr errors.Code + }{ + { + name: "nil-input", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-library", + given: &baseCred{}, + wantErr: errors.InvalidParameter, + }, + { + name: "library-not-password-type", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.UnspecifiedCredentialType), + }, + }, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-password-attribute", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{}, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-default-attributes", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "password": "my-password", + }, + }, + want: &passCred{ + password: credential.Password("my-password"), + }, + }, + { + name: "valid-override-attributes", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + PasswordAttribute: "test-password", + }, + secretData: map[string]any{ + "password": "default-password", + "test-password": "override-password", + }, + }, + want: &passCred{ + password: credential.Password("override-password"), + }, + }, + { + name: "invalid-password-override", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + PasswordAttribute: "missing-password", + }, + secretData: map[string]any{ + "password": "default-password", + "test-password": "override-password", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-metadata-field", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "data": map[string]any{ + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-no-data-field", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "metadata": map[string]any{}, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-metadata-type", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "metadata": "hello", + "data": map[string]any{ + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-invalid-metadata-type", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "metadata": map[string]any{}, + "data": "hello", + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "invalid-kv2-additional-field", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "bad-field": "hello", + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "my-password", + }, + }, + }, + wantErr: errors.VaultInvalidCredentialMapping, + }, + { + name: "valid-kv2-default-password-attribute", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + }, + secretData: map[string]any{ + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "my-password", + }, + }, + }, + want: &passCred{ + password: credential.Password("my-password"), + }, + }, + { + name: "valid-kv2-override-password-attribute", + given: &baseCred{ + lib: &genericIssuingCredentialLibrary{ + CredType: string(globals.PasswordCredentialType), + PasswordAttribute: "test-password", + }, + secretData: map[string]any{ + "metadata": map[string]any{}, + "data": map[string]any{ + "password": "default-password", + "test-password": "override-password", + }, + }, + }, + want: &passCred{ + password: credential.Password("override-password"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := baseToPass(context.Background(), tt.given) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + want := tt.want + want.baseCred = tt.given + assert.Equal(want, got) + }) + } +} + func TestBaseToSshPriKey(t *testing.T) { t.Parallel() diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index 707d64ccf0..d3ac193412 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -406,6 +406,14 @@ func (pl *listLookupLibrary) toCredentialLibrary() *CredentialLibrary { upd.sanitize() cl.MappingOverride = upd } + case string(globals.PasswordCredentialType): + if pl.PasswordAttribute != "" { + p := allocPasswordOverride() + p.LibraryId = pl.PublicId + p.PasswordAttribute = pl.PasswordAttribute + p.sanitize() + cl.MappingOverride = p + } case string(globals.SshPrivateKeyCredentialType): if pl.UsernameAttribute != "" || pl.PrivateKeyAttribute != "" || pl.PrivateKeyPassphraseAttribute != "" { pk := allocSshPrivateKeyOverride() diff --git a/internal/credential/vault/repository_credential_library_test.go b/internal/credential/vault/repository_credential_library_test.go index fccb901339..c9b8d60f4f 100644 --- a/internal/credential/vault/repository_credential_library_test.go +++ b/internal/credential/vault/repository_credential_library_test.go @@ -293,6 +293,76 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { }, }, }, + { + name: "valid-password-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + }, + { + name: "unknown-password-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: unknownMapper(1), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "invalid-password-mapping-override-type", + in: &CredentialLibrary{ + MappingOverride: NewUsernamePasswordOverride(WithOverrideUsernameAttribute("test")), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + wantErr: errors.VaultInvalidMappingOverride, + }, + { + name: "valid-password-credential-type-with-password-override", + in: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + want: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + }, { name: "valid-ssh-private-key-credential-type", in: &CredentialLibrary{ @@ -339,7 +409,7 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { wantErr: errors.VaultInvalidMappingOverride, }, { - name: "valid-ssh-prviate-key-credential-type-with-username-override", + name: "valid-ssh-private-key-credential-type-with-username-override", in: &CredentialLibrary{ MappingOverride: NewSshPrivateKeyOverride( WithOverrideUsernameAttribute("utest"), @@ -486,6 +556,15 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) { override := allocUsernamePasswordOverride() assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", []any{got.GetPublicId()})) + case *PasswordOverride: + g, ok := got.MappingOverride.(*PasswordOverride) + require.True(ok) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + + // verify it was persisted in the database + override := allocPasswordOverride() + assert.NoError(rw.LookupWhere(ctx, &override, "library_id = ?", []any{got.GetPublicId()})) + case *SshPrivateKeyOverride: g, ok := got.MappingOverride.(*SshPrivateKeyOverride) require.True(ok) @@ -1273,6 +1352,94 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { masks: []string{"MappingOverride"}, wantErr: errors.VaultInvalidMappingOverride, }, + { + name: "password-attribute-change-password-attribute", + orig: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("orig-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + chgFn: changeMappingOverride( + NewPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + wantCount: 1, + }, + { + name: "no-mapping-override-change-password-attributes", + orig: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + chgFn: changeMappingOverride( + NewPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + ), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("changed-password"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + wantCount: 1, + }, + { + name: "password-attributes-delete-mapping-override", + orig: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("orig-password"), + WithOverridePrivateKeyAttribute("orig-private-key"), + WithOverridePrivateKeyPassphraseAttribute("orig-passphrase"), + ), + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + chgFn: changeMappingOverride(nil), + masks: []string{"MappingOverride"}, + want: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + HttpMethod: "GET", + VaultPath: "/some/path", + Name: "test-password-repo", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + wantCount: 1, + }, { name: "ssh-private-key-attributes-change-username-attribute", orig: &CredentialLibrary{ @@ -1535,6 +1702,10 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) { require.True(ok) assert.Equal(w.UsernameAttribute, g.UsernameAttribute) assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + case *PasswordOverride: + g, ok := got.MappingOverride.(*PasswordOverride) + require.True(ok) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) case *SshPrivateKeyOverride: g, ok := got.MappingOverride.(*SshPrivateKeyOverride) require.True(ok) @@ -1800,6 +1971,31 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { }, }, }, + { + name: "valid-password-credential-type", + in: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + }, + { + name: "valid-password-credential-type-with-password-override", + in: &CredentialLibrary{ + MappingOverride: NewPasswordOverride( + WithOverridePasswordAttribute("ptest"), + ), + CredentialLibrary: &store.CredentialLibrary{ + StoreId: cs.GetPublicId(), + HttpMethod: "GET", + VaultPath: "/some/path", + CredentialType: string(globals.PasswordCredentialType), + }, + }, + }, { name: "valid-ssh-private-key-credential-type", in: &CredentialLibrary{ @@ -1901,6 +2097,10 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) { require.True(ok) assert.Equal(w.UsernameAttribute, g.UsernameAttribute) assert.Equal(w.PasswordAttribute, g.PasswordAttribute) + case *PasswordOverride: + g, ok := got.MappingOverride.(*PasswordOverride) + require.True(ok) + assert.Equal(w.PasswordAttribute, g.PasswordAttribute) case *SshPrivateKeyOverride: g, ok := got.MappingOverride.(*SshPrivateKeyOverride) require.True(ok) diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index 637fa4fa0d..4323f55d0b 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -1174,6 +1174,68 @@ func (x *UsernamePasswordDomainOverride) GetDomainAttribute() string { return "" } +type PasswordOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + LibraryId string `protobuf:"bytes,1,opt,name=library_id,json=libraryId,proto3" json:"library_id,omitempty" gorm:"primary_key"` + // password_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a password. + // If set, it overrides any default attribute names the system uses to + // find a password attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + PasswordAttribute string `protobuf:"bytes,2,opt,name=password_attribute,json=passwordAttribute,proto3" json:"password_attribute,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordOverride) Reset() { + *x = PasswordOverride{} + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordOverride) ProtoMessage() {} + +func (x *PasswordOverride) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordOverride.ProtoReflect.Descriptor instead. +func (*PasswordOverride) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{8} +} + +func (x *PasswordOverride) GetLibraryId() string { + if x != nil { + return x.LibraryId + } + return "" +} + +func (x *PasswordOverride) GetPasswordAttribute() string { + if x != nil { + return x.PasswordAttribute + } + return "" +} + type SshPrivateKeyOverride struct { state protoimpl.MessageState `protogen:"open.v1"` // library_id of the owning vault credential library. @@ -1213,7 +1275,7 @@ type SshPrivateKeyOverride struct { func (x *SshPrivateKeyOverride) Reset() { *x = SshPrivateKeyOverride{} - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[8] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1225,7 +1287,7 @@ func (x *SshPrivateKeyOverride) String() string { func (*SshPrivateKeyOverride) ProtoMessage() {} func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[8] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1238,7 +1300,7 @@ func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyOverride.ProtoReflect.Descriptor instead. func (*SshPrivateKeyOverride) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{8} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{9} } func (x *SshPrivateKeyOverride) GetLibraryId() string { @@ -1306,7 +1368,7 @@ type LdapCredentialLibrary struct { func (x *LdapCredentialLibrary) Reset() { *x = LdapCredentialLibrary{} - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1318,7 +1380,7 @@ func (x *LdapCredentialLibrary) String() string { func (*LdapCredentialLibrary) ProtoMessage() {} func (x *LdapCredentialLibrary) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1331,7 +1393,7 @@ func (x *LdapCredentialLibrary) ProtoReflect() protoreflect.Message { // Deprecated: Use LdapCredentialLibrary.ProtoReflect.Descriptor instead. func (*LdapCredentialLibrary) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{9} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{10} } func (x *LdapCredentialLibrary) GetPublicId() string { @@ -1546,7 +1608,11 @@ const file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = "" "library_id\x18\x01 \x01(\tR\tlibraryId\x12-\n" + "\x12username_attribute\x18\x02 \x01(\tR\x11usernameAttribute\x12-\n" + "\x12password_attribute\x18\x03 \x01(\tR\x11passwordAttribute\x12)\n" + - "\x10domain_attribute\x18\x04 \x01(\tR\x0fdomainAttribute\"\xe2\x01\n" + + "\x10domain_attribute\x18\x04 \x01(\tR\x0fdomainAttribute\"`\n" + + "\x10PasswordOverride\x12\x1d\n" + + "\n" + + "library_id\x18\x01 \x01(\tR\tlibraryId\x12-\n" + + "\x12password_attribute\x18\x02 \x01(\tR\x11passwordAttribute\"\xe2\x01\n" + "\x15SshPrivateKeyOverride\x12\x1d\n" + "\n" + "library_id\x18\x01 \x01(\tR\tlibraryId\x12-\n" + @@ -1582,7 +1648,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP() return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescData } -var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []any{ (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token @@ -1592,28 +1658,29 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []an (*Credential)(nil), // 5: controller.storage.credential.vault.store.v1.Credential (*UsernamePasswordOverride)(nil), // 6: controller.storage.credential.vault.store.v1.UsernamePasswordOverride (*UsernamePasswordDomainOverride)(nil), // 7: controller.storage.credential.vault.store.v1.UsernamePasswordDomainOverride - (*SshPrivateKeyOverride)(nil), // 8: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride - (*LdapCredentialLibrary)(nil), // 9: controller.storage.credential.vault.store.v1.LdapCredentialLibrary - (*timestamp.Timestamp)(nil), // 10: controller.storage.timestamp.v1.Timestamp + (*PasswordOverride)(nil), // 8: controller.storage.credential.vault.store.v1.PasswordOverride + (*SshPrivateKeyOverride)(nil), // 9: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride + (*LdapCredentialLibrary)(nil), // 10: controller.storage.credential.vault.store.v1.LdapCredentialLibrary + (*timestamp.Timestamp)(nil), // 11: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 10, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 14: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 15: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // 16: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 14: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 15: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 16: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp 17, // [17:17] is the sub-list for method output_type 17, // [17:17] is the sub-list for method input_type 17, // [17:17] is the sub-list for extension type_name @@ -1632,7 +1699,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc), len(file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto index 25a3b02d9a..cdd03fb4c6 100644 --- a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto @@ -499,6 +499,22 @@ message UsernamePasswordDomainOverride { string domain_attribute = 4; } +message PasswordOverride { + // library_id of the owning vault credential library. + // @inject_tag: `gorm:"primary_key"` + string library_id = 1; + + // password_attribute is the name of the attribute in the Data field of a + // Vault api.Secret that maps to a password. + // If set, it overrides any default attribute names the system uses to + // find a password attribute. + // + // See https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25 + // + // @inject_tag: `gorm:"default:null"` + string password_attribute = 2; +} + message SshPrivateKeyOverride { // library_id of the owning vault credential library. // @inject_tag: `gorm:"primary_key"` From 82745e16bea09fdb81caef109ea10108998a16a3 Mon Sep 17 00:00:00 2001 From: Justin Nguyen Date: Mon, 20 Oct 2025 17:43:10 -0500 Subject: [PATCH 06/11] feat(handlers): Implement CRUDL for vault generic Password credential (#6161) --- internal/credential/credential.go | 4 +- internal/credential/vault/private_library.go | 2 +- .../credentiallibrary_service.go | 31 + .../credentiallibrary_service_test.go | 749 +++++++----------- .../handlers/targets/credentials.go | 11 + .../targets/tcp/target_service_test.go | 102 ++- .../api/resources/targets/v1/target.proto | 6 + .../api/resources/targets/target.pb.go | 133 +++- 8 files changed, 540 insertions(+), 498 deletions(-) diff --git a/internal/credential/credential.go b/internal/credential/credential.go index f28ee9b925..038a54f728 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -143,9 +143,9 @@ type UsernamePasswordDomain interface { Domain() string } -// PasswordCredential is a credential containing a username and a password. +// PasswordOnly is a credential containing a password. // Does not follow naming convention to avoid conflict with existing Password type. -type PasswordCredential interface { +type PasswordOnly interface { Credential Password() Password } diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 7843c87b21..0011a6df04 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -169,7 +169,7 @@ func baseToUsrPassDomain(ctx context.Context, bc *baseCred) (*usrPassDomainCred, }, nil } -var _ credential.PasswordCredential = (*passCred)(nil) +var _ credential.PasswordOnly = (*passCred)(nil) type passCred struct { *baseCred diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index b6a61c335a..dcfe81c804 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -83,6 +83,7 @@ var ( globals.SshPrivateKeyCredentialType, globals.UnspecifiedCredentialType, globals.UsernamePasswordDomainCredentialType, + globals.PasswordCredentialType, } validKeyTypes = []string{ @@ -851,6 +852,11 @@ func toProto(ctx context.Context, in credential.Library, opt ...handlers.Option) m[domainAttribute] = mapping.DomainAttribute } + case *vault.PasswordOverride: + if mapping.PasswordAttribute != "" { + m[passwordAttribute] = mapping.PasswordAttribute + } + case *vault.SshPrivateKeyOverride: if mapping.UsernameAttribute != "" { m[usernameAttribute] = mapping.UsernameAttribute @@ -1006,6 +1012,17 @@ func toStorageVaultLibrary(ctx context.Context, storeId string, in *pb.Credentia opts = append(opts, vault.WithMappingOverride(vault.NewUsernamePasswordDomainOverride(mapOpts...))) } + case globals.PasswordCredentialType: + opts = append(opts, vault.WithCredentialType(credentialType)) + overrides := in.CredentialMappingOverrides.AsMap() + var mapOpts []vault.Option + if password := overrides[passwordAttribute]; password != nil { + mapOpts = append(mapOpts, vault.WithOverridePasswordAttribute(password.(string))) + } + if len(mapOpts) > 0 { + opts = append(opts, vault.WithMappingOverride(vault.NewPasswordOverride(mapOpts...))) + } + case globals.SshPrivateKeyCredentialType: opts = append(opts, vault.WithCredentialType(credentialType)) overrides := in.CredentialMappingOverrides.AsMap() @@ -1340,6 +1357,8 @@ func validateMapping(badFields map[string]string, credentialType globals.Credent validFields[usernameAttribute] = true validFields[passwordAttribute] = true validFields[domainAttribute] = true + case globals.PasswordCredentialType: + validFields[passwordAttribute] = true default: badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", credentialType) return @@ -1457,6 +1476,18 @@ func getMappingUpdates(credentialType globals.CredentialType, current vault.Mapp default: ret[domainAttribute] = currentDomain } + case globals.PasswordCredentialType: + var currentPass any + if overrides, ok := current.(*vault.PasswordOverride); ok { + currentPass = overrides.PasswordAttribute + } + + switch { + case masks[passwordAttribute]: + ret[passwordAttribute] = new[passwordAttribute] + default: + ret[passwordAttribute] = currentPass + } case globals.SshPrivateKeyCredentialType: var currentUser, currentpPass, currentPk any if overrides, ok := current.(*vault.SshPrivateKeyOverride); ok { diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index e4b59200d3..4552b9a7d8 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -14,7 +14,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -87,25 +86,6 @@ func sshCredentialLibraryToProto(credLib *vault.SSHCertificateCredentialLibrary, } } -func ldapCredentialLibraryToProto(credLib *vault.LdapCredentialLibrary, project *iam.Scope) *pb.CredentialLibrary { - return &pb.CredentialLibrary{ - Id: credLib.GetPublicId(), - CredentialStoreId: credLib.GetStoreId(), - CredentialType: credLib.GetCredentialType(), - Scope: &scopepb.ScopeInfo{Id: project.GetPublicId(), Type: scope.Project.String(), ParentScopeId: project.GetParentId()}, - CreatedTime: credLib.GetCreateTime().GetTimestamp(), - UpdatedTime: credLib.GetUpdateTime().GetTimestamp(), - Version: credLib.GetVersion(), - Type: vault.LdapCredentialLibrarySubtype.String(), - AuthorizedActions: testAuthorizedActions, - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String(credLib.GetVaultPath()), - }, - }, - } -} - func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -134,9 +114,6 @@ func TestList(t *testing.T) { for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { wantLibraries = append(wantLibraries, sshCredentialLibraryToProto(l, prj)) } - for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { - wantLibraries = append(wantLibraries, ldapCredentialLibraryToProto(l, prj)) - } cases := []struct { name string @@ -154,7 +131,7 @@ func TestList(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 30, + EstItemCount: 20, }, anonRes: &pbs.ListCredentialLibrariesResponse{ Items: wantLibraries, @@ -162,7 +139,7 @@ func TestList(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 30, + EstItemCount: 20, }, }, { @@ -287,20 +264,16 @@ func TestList_Attributes(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) - ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 3) - storeGeneric, storeSSHCertificate, storeLdap := ts[0], ts[1], ts[2] + ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + storeGeneric, storeSSHCertificate := ts[0], ts[1] var wantLibrariesGeneric []*pb.CredentialLibrary var wantLibrariesSSHCertificate []*pb.CredentialLibrary - var wantLibrariesLdap []*pb.CredentialLibrary for _, l := range vault.TestCredentialLibraries(t, conn, wrapper, storeGeneric.GetPublicId(), globals.UnspecifiedCredentialType, 5) { wantLibrariesGeneric = append(wantLibrariesGeneric, vaultCredentialLibraryToProto(l, prj)) } for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, storeSSHCertificate.GetPublicId(), 10) { wantLibrariesSSHCertificate = append(wantLibrariesSSHCertificate, sshCredentialLibraryToProto(l, prj)) } - for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, storeLdap.GetPublicId(), 10) { - wantLibrariesLdap = append(wantLibrariesLdap, ldapCredentialLibraryToProto(l, prj)) - } cases := []struct { name string @@ -345,24 +318,6 @@ func TestList_Attributes(t *testing.T) { RemovedIds: nil, }, // anonymous user does not have access to attributes }, - { - name: "Filter on Attribute Ldap Library", - req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeLdap.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesLdap[2].GetVaultLdapCredentialLibraryAttributes().GetPath().Value)}, - res: &pbs.ListCredentialLibrariesResponse{ - Items: wantLibrariesLdap[2:3], - ResponseType: "complete", - SortBy: "created_time", - SortDir: "desc", - RemovedIds: nil, - EstItemCount: 1, - }, - anonRes: &pbs.ListCredentialLibrariesResponse{ - ResponseType: "complete", - SortBy: "created_time", - SortDir: "desc", - RemovedIds: nil, - }, // anonymous user does not have access to attributes - }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -632,47 +587,26 @@ func TestCreate(t *testing.T) { err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { - name: "No Vault LDAP path", - req: &pbs.CreateCredentialLibraryRequest{ - Item: &pb.CredentialLibrary{ - CredentialStoreId: store.GetPublicId(), - Type: vault.LdapCredentialLibrarySubtype.String(), - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String(""), - }, - }, - }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Invalid Vault LDAP credential type", - req: &pbs.CreateCredentialLibraryRequest{ - Item: &pb.CredentialLibrary{ - CredentialStoreId: store.GetPublicId(), - Type: vault.LdapCredentialLibrarySubtype.String(), - CredentialType: string(globals.SshPrivateKeyCredentialType), - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("ldap/invalid/path"), - }, + name: "Invalid password mapping", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), }, }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Invalid credential library type", - req: &pbs.CreateCredentialLibraryRequest{ - Item: &pb.CredentialLibrary{ - CredentialStoreId: store.GetPublicId(), - Type: "bla-invalid", - CredentialType: "bla-invalid", - }, - }, + CredentialType: string(globals.PasswordCredentialType), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: "password-test", + "invalid": "invalid-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, @@ -1074,7 +1008,7 @@ func TestCreate(t *testing.T) { }, }, { - name: "Create a valid vault CredentialLibrary ssh_private_key type", + name: "Create a valid vault CredentialLibrary password type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Type: vault.GenericLibrarySubtype.String(), @@ -1083,7 +1017,7 @@ func TestCreate(t *testing.T) { Path: wrapperspb.String("something"), }, }, - CredentialType: string(globals.SshPrivateKeyCredentialType), + CredentialType: string(globals.PasswordCredentialType), }}, idPrefix: globals.VaultCredentialLibraryPrefix + "_", res: &pbs.CreateCredentialLibraryResponse{ @@ -1102,13 +1036,13 @@ func TestCreate(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: string(globals.SshPrivateKeyCredentialType), + CredentialType: string(globals.PasswordCredentialType), AuthorizedActions: testAuthorizedActions, }, }, }, { - name: "Create a valid vault CredentialLibrary ssh_private_key type with mapping", + name: "Create a valid vault CredentialLibrary password type with password mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Type: vault.GenericLibrarySubtype.String(), @@ -1119,15 +1053,13 @@ func TestCreate(t *testing.T) { }, CredentialMappingOverrides: func() *structpb.Struct { v := map[string]any{ - usernameAttribute: "user-test", - privateKeyAttribute: "pk-test", - pkPassphraseAttribute: "pass-test", + passwordAttribute: "password-test", } ret, err := structpb.NewStruct(v) require.NoError(t, err) return ret }(), - CredentialType: string(globals.SshPrivateKeyCredentialType), + CredentialType: string(globals.PasswordCredentialType), }}, idPrefix: globals.VaultCredentialLibraryPrefix + "_", res: &pbs.CreateCredentialLibraryResponse{ @@ -1146,12 +1078,10 @@ func TestCreate(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: string(globals.SshPrivateKeyCredentialType), + CredentialType: string(globals.PasswordCredentialType), CredentialMappingOverrides: func() *structpb.Struct { v := map[string]any{ - usernameAttribute: "user-test", - privateKeyAttribute: "pk-test", - pkPassphraseAttribute: "pass-test", + passwordAttribute: "password-test", } ret, err := structpb.NewStruct(v) require.NoError(t, err) @@ -1162,15 +1092,16 @@ func TestCreate(t *testing.T) { }, }, { - name: "Create a valid vault CredentialLibrary with the 'vault' subtype", + name: "Create a valid vault CredentialLibrary ssh_private_key type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Type: vault.GenericLibrarySubtype.String(), - Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ - VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), }, }, + CredentialType: string(globals.SshPrivateKeyCredentialType), }}, idPrefix: globals.VaultCredentialLibraryPrefix + "_", res: &pbs.CreateCredentialLibraryResponse{ @@ -1189,26 +1120,79 @@ func TestCreate(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, + CredentialType: string(globals.SshPrivateKeyCredentialType), AuthorizedActions: testAuthorizedActions, }, }, }, { - name: "Create a valid Vault LDAP Credential library", - req: &pbs.CreateCredentialLibraryRequest{ + name: "Create a valid vault CredentialLibrary ssh_private_key type with mapping", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + usernameAttribute: "user-test", + privateKeyAttribute: "pk-test", + pkPassphraseAttribute: "pass-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + CredentialType: string(globals.SshPrivateKeyCredentialType), + }}, + idPrefix: globals.VaultCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", globals.VaultCredentialLibraryPrefix), Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), - Type: vault.LdapCredentialLibrarySubtype.String(), - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("ldap/creds/hi"), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), }, }, + CredentialType: string(globals.SshPrivateKeyCredentialType), + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + usernameAttribute: "user-test", + privateKeyAttribute: "pk-test", + pkPassphraseAttribute: "pass-test", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + AuthorizedActions: testAuthorizedActions, }, }, - idPrefix: globals.VaultLdapCredentialLibraryPrefix + "_", + }, + { + name: "Create a valid vault CredentialLibrary with the 'vault' subtype", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + }}, + idPrefix: globals.VaultCredentialLibraryPrefix + "_", res: &pbs.CreateCredentialLibraryResponse{ - Uri: fmt.Sprintf("credential-libraries/%s_", globals.VaultLdapCredentialLibraryPrefix), + Uri: fmt.Sprintf("credential-libraries/%s_", globals.VaultCredentialLibraryPrefix), Item: &pb.CredentialLibrary{ Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), @@ -1216,11 +1200,11 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.LdapCredentialLibrarySubtype.String(), - CredentialType: string(globals.UsernamePasswordDomainCredentialType), - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("ldap/creds/hi"), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), }, }, AuthorizedActions: testAuthorizedActions, @@ -1324,6 +1308,17 @@ func TestGet(t *testing.T) { userPassDomainLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), libDomain) require.NoError(t, err) + libPassword, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path", + vault.WithCredentialType("password"), + vault.WithMappingOverride( + vault.NewPasswordOverride( + vault.WithOverridePasswordAttribute("pass"), + ))) + + require.NoError(t, err) + passwordLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), libPassword) + require.NoError(t, err) + lib1, err := vault.NewCredentialLibrary(store.GetPublicId(), "vault/path/ssh", vault.WithCredentialType("ssh_private_key"), vault.WithMappingOverride( @@ -1341,13 +1336,6 @@ func TestGet(t *testing.T) { sshCertLib, err := repo.CreateSSHCertificateCredentialLibrary(context.Background(), prj.GetPublicId(), lib2) require.NoError(t, err) - libLdap1, err := vault.NewLdapCredentialLibrary(store.GetPublicId(), "ldap/creds/test") - require.NoError(t, err) - require.NotNil(t, libLdap1) - libLdap1, err = repo.CreateLdapCredentialLibrary(t.Context(), prj.GetPublicId(), libLdap1) - require.NoError(t, err) - require.NotNil(t, libLdap1) - cases := []struct { name string id string @@ -1441,6 +1429,37 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-password", + id: passwordLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: passwordLib.GetPublicId(), + CredentialStoreId: passwordLib.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.GenericLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: passwordLib.CreateTime.GetTimestamp(), + UpdatedTime: passwordLib.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.CredentialLibrary_VaultGenericCredentialLibraryAttributes{ + VaultGenericCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(passwordLib.GetVaultPath()), + HttpMethod: wrapperspb.String(passwordLib.GetHttpMethod()), + }, + }, + CredentialType: "password", + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: "pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + }, { name: "success-ssh-private-key", id: sshPkLib.GetPublicId(), @@ -1498,28 +1517,6 @@ func TestGet(t *testing.T) { }, }, }, - { - name: "success-ldap", - id: libLdap1.GetPublicId(), - res: &pbs.GetCredentialLibraryResponse{ - Item: &pb.CredentialLibrary{ - Id: libLdap1.GetPublicId(), - CredentialStoreId: libLdap1.GetStoreId(), - CredentialType: libLdap1.GetCredentialType(), - Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.LdapCredentialLibrarySubtype.String(), - AuthorizedActions: testAuthorizedActions, - CreatedTime: libLdap1.CreateTime.GetTimestamp(), - UpdatedTime: libLdap1.UpdateTime.GetTimestamp(), - Version: 1, - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String(libLdap1.GetVaultPath()), - }, - }, - }, - }, - }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.VaultCredentialLibraryPrefix), @@ -1582,7 +1579,6 @@ func TestDelete(t *testing.T) { store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), globals.UnspecifiedCredentialType, 1)[0] vl2 := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] - vl3 := vault.TestLdapCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] s, err := NewService(ctx, iamRepoFn, repoFn, 1000) require.NoError(t, err) cases := []struct { @@ -1599,10 +1595,6 @@ func TestDelete(t *testing.T) { name: "success-ssh-cert", id: vl2.GetPublicId(), }, - { - name: "success-ldap", - id: vl3.GetPublicId(), - }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.VaultCredentialLibraryPrefix), @@ -2118,6 +2110,84 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "password-attribute-change-password-attribute", + opts: []vault.Option{ + vault.WithCredentialType("password"), + vault.WithMappingOverride( + vault.NewPasswordOverride( + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: "changed-pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides.Fields[passwordAttribute] = structpb.NewStringValue("changed-pass") + return out + }, + }, + { + name: "password-no-mapping-override-change-password-attribute", + opts: []vault.Option{ + vault.WithCredentialType("password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: "new-pass", + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + v := map[string]any{ + passwordAttribute: "new-pass", + } + var err error + out.CredentialMappingOverrides, err = structpb.NewStruct(v) + require.NoError(t, err) + return out + }, + }, + { + name: "password-attribute-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("password"), + vault.WithMappingOverride( + vault.NewPasswordOverride( + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, { name: "no-mapping-override-delete-mapping-override", opts: []vault.Option{ @@ -2166,7 +2236,7 @@ func TestUpdate(t *testing.T) { }, }, { - name: "username-password-attributes-delete-mapping-override-field-specific", + name: "username-password-domain-attributes-delete-mapping-override-field-specific", opts: []vault.Option{ vault.WithCredentialType("username_password_domain"), vault.WithMappingOverride( @@ -2248,6 +2318,75 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "password-no-mapping-override-delete-mapping-override", + opts: []vault.Option{ + vault.WithCredentialType("password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(credentialMappingPathField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: nil, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "password-no-mapping-override-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("password"), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, + { + name: "password-delete-mapping-override-field-specific", + opts: []vault.Option{ + vault.WithCredentialType("password"), + vault.WithMappingOverride( + vault.NewPasswordOverride( + vault.WithOverridePasswordAttribute("orig-pass"), + )), + }, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(passwordAttrField), + Item: &pb.CredentialLibrary{ + CredentialMappingOverrides: func() *structpb.Struct { + v := map[string]any{ + passwordAttribute: nil, + } + ret, err := structpb.NewStruct(v) + require.NoError(t, err) + return ret + }(), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.CredentialMappingOverrides = nil + return out + }, + }, { name: "ssh-private-key-attributes-change-username-attribute", opts: []vault.Option{ @@ -3402,287 +3541,6 @@ func TestUpdate_SSHCertificateCredentialLibrary(t *testing.T) { } } -func TestUpdate_LdapCredentialLibrary(t *testing.T) { - testCtx := context.Background() - conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - sche := scheduler.TestScheduler(t, conn, wrapper) - rw := db.New(conn) - - iamRepo := iam.TestRepo(t, conn, wrapper) - iamRepoFn := func() (*iam.Repository, error) { - return iamRepo, nil - } - repoFn := func() (*vault.Repository, error) { - return vault.NewRepository(testCtx, rw, rw, kms, sche) - } - - _, prj := iam.TestScopes(t, iamRepo) - ctx := auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()) - - s, err := NewService(testCtx, iamRepoFn, repoFn, 1000) - require.NoError(t, err) - cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1) - store := cs[0] - - freshLibrary := func(opt ...vault.Option) (*vault.LdapCredentialLibrary, func()) { - repo, err := repoFn() - require.NoError(t, err) - lib, err := vault.NewLdapCredentialLibrary(store.GetPublicId(), "ldap/creds/foo", opt...) - require.NoError(t, err) - - vl, err := repo.CreateLdapCredentialLibrary(ctx, prj.GetPublicId(), lib) - require.NoError(t, err) - clean := func() { - _, err := s.DeleteCredentialLibrary(ctx, &pbs.DeleteCredentialLibraryRequest{Id: vl.GetPublicId()}) - require.NoError(t, err) - } - return vl, clean - } - - tests := []struct { - name string - req *pbs.UpdateCredentialLibraryRequest - res func(*pb.CredentialLibrary) *pb.CredentialLibrary - wantErr bool - wantErrStatus int - wantErrStr string - }{ - { - name: "updateNonExistentField", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"doesntExist"}}, - Item: &pb.CredentialLibrary{ - Name: wrapperspb.String("updateNonExistentField_test"), - }, - }, - wantErr: true, - wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), - wantErrStr: "No valid fields included in the update mask.", - }, - { - name: "updateImmutableField", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"storeId"}}, - Item: &pb.CredentialLibrary{ - CredentialStoreId: "immutable", - }, - }, - wantErr: true, - wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), - wantErrStr: "No valid fields included in the update mask.", - }, - { - name: "updateValidFieldWithInvalidData", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, - Item: &pb.CredentialLibrary{ - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("invalid_path"), - }, - }, - }, - }, - wantErr: true, - wantErrStr: "vault_path_must_have_staticcred_or_creds constraint failed", - }, - { - name: "updateImmutableCredentialTypeField", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"credentialType"}}, - Item: &pb.CredentialLibrary{ - CredentialType: string(globals.SshPrivateKeyCredentialType), - }, - }, - wantErr: true, - wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), - wantErrStr: "Cannot modify credential type.", - }, - { - name: "updateImmutableTypeField", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"type"}}, - Item: &pb.CredentialLibrary{ - Type: string(globals.SshCertificateCredentialType), - }, - }, - wantErr: true, - wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), - wantErrStr: "Cannot modify resource type.", - }, - { - name: "unsetVaultPath", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, - Item: &pb.CredentialLibrary{ - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String(""), - }, - }, - }, - }, - wantErr: true, - wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), - wantErrStr: "attributes.path\", desc: \"This is a required field and cannot be set to empty.", - }, - { - name: "updateName", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name"}}, - Item: &pb.CredentialLibrary{ - Name: wrapperspb.String("updated_name"), - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.Name = wrapperspb.String("updated_name") - return cl - }, - }, - { - name: "updateDescription", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"description"}}, - Item: &pb.CredentialLibrary{ - Description: wrapperspb.String("updated_description"), - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.Description = wrapperspb.String("updated_description") - return cl - }, - }, - { - name: "updateValidVaultPath", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, - Item: &pb.CredentialLibrary{ - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("ldap/static-cred/updated/path"), - }, - }, - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.GetVaultLdapCredentialLibraryAttributes().Path = wrapperspb.String("ldap/static-cred/updated/path") - return cl - }, - }, - { - name: "updateMultipleOneNonExistent", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "doesntExist"}}, - Item: &pb.CredentialLibrary{ - Name: wrapperspb.String("updated_name"), - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.Name = wrapperspb.String("updated_name") - return cl - }, - }, - { - name: "updateMultipleOneImmutable", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime. - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "credentialStoreId"}}, - Item: &pb.CredentialLibrary{ - Name: wrapperspb.String("updated_name"), - CredentialStoreId: "immutable", - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.Name = wrapperspb.String("updated_name") - return cl - }, - }, - { - name: "updateMultiple", - req: &pbs.UpdateCredentialLibraryRequest{ - Id: "", // Set at test runtime - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "attributes.path"}}, - Item: &pb.CredentialLibrary{ - Name: wrapperspb.String("updated_name"), - Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ - VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ - Path: wrapperspb.String("ldap/creds/updated/path"), - }, - }, - }, - }, - res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { - cl.Name = wrapperspb.String("updated_name") - cl.GetVaultLdapCredentialLibraryAttributes().Path = wrapperspb.String("ldap/creds/updated/path") - return cl - }, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - st, cleanup := freshLibrary() - defer cleanup() - - if tc.req.Item.GetVersion() == 0 { - tc.req.Item.Version = 1 - } - if tc.req.GetId() == "" { - tc.req.Id = st.GetPublicId() - } - resToChange, err := s.GetCredentialLibrary(ctx, &pbs.GetCredentialLibraryRequest{Id: st.GetPublicId()}) - require.NoError(err) - - got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) - if tc.wantErr { - require.ErrorContains(gErr, tc.wantErrStr) - - gApiErr, ok := gErr.(*handlers.ApiError) - if ok { - // We don't always return status. - require.NotNil(gApiErr) - require.EqualValues(tc.wantErrStatus, gApiErr.Status) - } - require.Nil(got) - return - } - require.NoError(gErr) - require.NotNil(got) - - want := &pbs.UpdateCredentialLibraryResponse{Item: tc.res(resToChange.GetItem())} - gotUpdateTime := got.GetItem().GetUpdatedTime() - created := st.GetCreateTime().GetTimestamp() - assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) - - want.Item.UpdatedTime = got.Item.UpdatedTime - - assert.EqualValues(2, got.Item.Version) - want.Item.Version = 2 - - assert.Empty(cmp.Diff( - got, - want, - protocmp.Transform(), - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - )) - }) - } -} - func TestListPagination(t *testing.T) { // Set database read timeout to avoid duplicates in response oldReadTimeout := globals.RefreshReadLookbackDuration @@ -3731,9 +3589,6 @@ func TestListPagination(t *testing.T) { for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 5) { allCredentialLibraries = append(allCredentialLibraries, sshCredentialLibraryToProto(l, prj)) } - for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 5) { - allCredentialLibraries = append(allCredentialLibraries, ldapCredentialLibraryToProto(l, prj)) - } // Reverse as we return items sorted by create_time desceding (newest first) slices.Reverse(allCredentialLibraries) @@ -3787,7 +3642,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3812,7 +3667,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3824,10 +3679,10 @@ func TestListPagination(t *testing.T) { // Request rest of results req.ListToken = got.ListToken - req.PageSize = 15 + req.PageSize = 10 got, err = s.ListCredentialLibraries(ctx, req) require.NoError(err) - require.Len(got.GetItems(), 11) + require.Len(got.GetItems(), 6) // Compare without comparing the list token assert.Empty( cmp.Diff( @@ -3838,7 +3693,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3863,14 +3718,14 @@ func TestListPagination(t *testing.T) { // Update one of the other stores allCredentialLibraries[1].Name = wrapperspb.String("new-name") allCredentialLibraries[1].Version = 2 - updatedLibrary := &vault.LdapCredentialLibrary{ - LdapCredentialLibrary: &store.LdapCredentialLibrary{ + updatedLibrary := &vault.SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ PublicId: allCredentialLibraries[1].GetId(), StoreId: allCredentialLibraries[1].GetCredentialStoreId(), Name: allCredentialLibraries[1].Name.Value, }, } - cred, _, err := vaultRepo.UpdateLdapCredentialLibrary(ctx, prj.PublicId, updatedLibrary, 1, []string{"name"}) + cred, _, err := vaultRepo.UpdateSSHCertificateCredentialLibrary(ctx, prj.PublicId, updatedLibrary, 1, []string{"name"}) require.NoError(err) allCredentialLibraries[1].UpdatedTime = cred.UpdateTime.GetTimestamp() allCredentialLibraries[1].Version = cred.Version @@ -3904,7 +3759,7 @@ func TestListPagination(t *testing.T) { SortDir: "desc", // Should contain the deleted library RemovedIds: []string{deletedCredLib.Id}, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3929,7 +3784,7 @@ func TestListPagination(t *testing.T) { SortBy: "updated_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3957,7 +3812,7 @@ func TestListPagination(t *testing.T) { SortDir: "desc", // Should be empty again RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3980,7 +3835,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 15, + EstItemCount: 10, }, cmpopts.SortSlices(func(a, b string) bool { return a < b diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index 5d1a5ae73d..ec3a2c2f75 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -137,6 +137,17 @@ func dynamicToSessionCredential(ctx context.Context, cred credential.Dynamic) (* return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for credential")) } + case credential.PasswordOnly: + credData, err = handlers.ProtoToStruct( + ctx, + &pb.PasswordCredential{ + Password: string(c.Password()), + }, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for credential")) + } + case credential.SshPrivateKey: credData, err = handlers.ProtoToStruct( ctx, diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index 8354386710..a372d713fa 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -4170,7 +4170,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "username_password", + CredentialType: string(globals.UsernamePasswordCredentialType), }}) require.NoError(t, err) @@ -4203,7 +4203,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "username_password", + CredentialType: string(globals.UsernamePasswordCredentialType), CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{ "username_attribute": structpb.NewStringValue("non-default-user"), "password_attribute": structpb.NewStringValue("non-default-pass"), @@ -4211,6 +4211,44 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { }}) require.NoError(t, err) + defaultPassword := v.CreateKVSecret(t, "default-password", []byte(`{"data": {"password": "my-default-password"}}`)) + require.NotNil(t, defaultPassword) + nonDefaultPassword := v.CreateKVSecret(t, "non-default-password", []byte(`{"data": {"non-default-password": "my-non-default-password"}}`)) + require.NotNil(t, nonDefaultPassword) + + clsRespPassword, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ + CredentialStoreId: vaultStore.GetPublicId(), + Name: wrapperspb.String("Password Library"), + Description: wrapperspb.String("Password Library Description"), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(path.Join("secret", "data", "default-password")), + HttpMethod: wrapperspb.String("GET"), + }, + }, + CredentialType: string(globals.PasswordCredentialType), + }}) + require.NoError(t, err) + + clsRespPasswordWithMapping, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ + CredentialStoreId: vaultStore.GetPublicId(), + Name: wrapperspb.String("Password Mapping Library"), + Description: wrapperspb.String("Password Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), + Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(path.Join("secret", "data", "non-default-password")), + HttpMethod: wrapperspb.String("GET"), + }, + }, + CredentialType: string(globals.PasswordCredentialType), + CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{ + "password_attribute": structpb.NewStringValue("non-default-password"), + }}, + }}) + require.NoError(t, err) + staticStore := credstatic.TestCredentialStore(t, conn, wrapper, proj.GetPublicId()) credService, err := credentials.NewService(ctx, iamRepoFn, staticCredRepoFn, 1000) require.NoError(t, err) @@ -4291,7 +4329,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "ssh_private_key", + CredentialType: string(globals.SshPrivateKeyCredentialType), }}) require.NoError(t, err) @@ -4310,7 +4348,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "ssh_private_key", + CredentialType: string(globals.SshPrivateKeyCredentialType), CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{ "username_attribute": structpb.NewStringValue("non-default-user"), "private_key_attribute": structpb.NewStringValue("non-default-pk"), @@ -4333,7 +4371,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "ssh_private_key", + CredentialType: string(globals.SshPrivateKeyCredentialType), }}) require.NoError(t, err) require.NotNil(t, clsRespSshPrivateKeyWithPass) @@ -4353,7 +4391,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { HttpMethod: wrapperspb.String("GET"), }, }, - CredentialType: "ssh_private_key", + CredentialType: string(globals.SshPrivateKeyCredentialType), CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{ "username_attribute": structpb.NewStringValue("/data/non-default-user"), "private_key_attribute": structpb.NewStringValue("/data/non-default-pk"), @@ -4443,6 +4481,58 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { }, wantedConnectionLimit: 100, }, + { + name: "vault-password", + hostSourceId: shs.GetPublicId(), + credSourceId: clsRespPassword.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: clsRespPassword.GetItem().GetId(), + Name: clsRespPassword.GetItem().GetName().GetValue(), + Description: clsRespPassword.GetItem().GetDescription().GetValue(), + CredentialStoreId: vaultStore.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), + CredentialType: string(globals.PasswordCredentialType), + }, + Credential: func() *structpb.Struct { + data := map[string]any{ + "password": "my-default-password", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + wantedConnectionLimit: 10, + }, + { + name: "vault-password-with-mapping", + hostSourceId: shs.GetPublicId(), + credSourceId: clsRespPasswordWithMapping.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: clsRespPasswordWithMapping.GetItem().GetId(), + Name: clsRespPasswordWithMapping.GetItem().GetName().GetValue(), + Description: clsRespPasswordWithMapping.GetItem().GetDescription().GetValue(), + CredentialStoreId: vaultStore.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), + CredentialType: string(globals.PasswordCredentialType), + }, + Credential: func() *structpb.Struct { + data := map[string]any{ + "password": "my-non-default-password", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + wantedConnectionLimit: 100, + }, { name: "static-UsernamePassword", hostSourceId: shs.GetPublicId(), diff --git a/internal/proto/controller/api/resources/targets/v1/target.proto b/internal/proto/controller/api/resources/targets/v1/target.proto index 62d111bdc1..975029bbb8 100644 --- a/internal/proto/controller/api/resources/targets/v1/target.proto +++ b/internal/proto/controller/api/resources/targets/v1/target.proto @@ -475,6 +475,12 @@ message UsernamePasswordDomainCredential { string domain = 3; // @gotags: `class:"public"` } +// The layout of the struct for "credential" field in SessionCredential for a password credential type. +message PasswordCredential { + // Password of the credential + string password = 1; // @gotags: `class:"secret"` +} + // The layout of the struct for "credential" field in SessionCredential for a ssh_private_key credential type. message SshPrivateKeyCredential { // Username of the credential diff --git a/sdk/pbs/controller/api/resources/targets/target.pb.go b/sdk/pbs/controller/api/resources/targets/target.pb.go index dbfe433365..b7547a1bef 100644 --- a/sdk/pbs/controller/api/resources/targets/target.pb.go +++ b/sdk/pbs/controller/api/resources/targets/target.pb.go @@ -1487,6 +1487,52 @@ func (x *UsernamePasswordDomainCredential) GetDomain() string { return "" } +// The layout of the struct for "credential" field in SessionCredential for a password credential type. +type PasswordCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Password of the credential + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty" class:"secret"` // @gotags: `class:"secret"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordCredential) Reset() { + *x = PasswordCredential{} + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordCredential) ProtoMessage() {} + +func (x *PasswordCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordCredential.ProtoReflect.Descriptor instead. +func (*PasswordCredential) Descriptor() ([]byte, []int) { + return file_controller_api_resources_targets_v1_target_proto_rawDescGZIP(), []int{16} +} + +func (x *PasswordCredential) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + // The layout of the struct for "credential" field in SessionCredential for a ssh_private_key credential type. type SshPrivateKeyCredential struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1502,7 +1548,7 @@ type SshPrivateKeyCredential struct { func (x *SshPrivateKeyCredential) Reset() { *x = SshPrivateKeyCredential{} - mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[16] + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1514,7 +1560,7 @@ func (x *SshPrivateKeyCredential) String() string { func (*SshPrivateKeyCredential) ProtoMessage() {} func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[16] + mi := &file_controller_api_resources_targets_v1_target_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1527,7 +1573,7 @@ func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyCredential.ProtoReflect.Descriptor instead. func (*SshPrivateKeyCredential) Descriptor() ([]byte, []int) { - return file_controller_api_resources_targets_v1_target_proto_rawDescGZIP(), []int{16} + return file_controller_api_resources_targets_v1_target_proto_rawDescGZIP(), []int{17} } func (x *SshPrivateKeyCredential) GetUsername() string { @@ -1709,7 +1755,9 @@ const file_controller_api_resources_targets_v1_target_proto_rawDesc = "" + " UsernamePasswordDomainCredential\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x16\n" + - "\x06domain\x18\x03 \x01(\tR\x06domain\"\x8c\x01\n" + + "\x06domain\x18\x03 \x01(\tR\x06domain\"0\n" + + "\x12PasswordCredential\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\"\x8c\x01\n" + "\x17SshPrivateKeyCredential\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x1f\n" + "\vprivate_key\x18\x02 \x01(\tR\n" + @@ -1728,7 +1776,7 @@ func file_controller_api_resources_targets_v1_target_proto_rawDescGZIP() []byte return file_controller_api_resources_targets_v1_target_proto_rawDescData } -var file_controller_api_resources_targets_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_controller_api_resources_targets_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_controller_api_resources_targets_v1_target_proto_goTypes = []any{ (*Alias)(nil), // 0: controller.api.resources.targets.v1.Alias (*TargetAliasAttributes)(nil), // 1: controller.api.resources.targets.v1.TargetAliasAttributes @@ -1746,57 +1794,58 @@ var file_controller_api_resources_targets_v1_target_proto_goTypes = []any{ (*SessionAuthorization)(nil), // 13: controller.api.resources.targets.v1.SessionAuthorization (*UsernamePasswordCredential)(nil), // 14: controller.api.resources.targets.v1.UsernamePasswordCredential (*UsernamePasswordDomainCredential)(nil), // 15: controller.api.resources.targets.v1.UsernamePasswordDomainCredential - (*SshPrivateKeyCredential)(nil), // 16: controller.api.resources.targets.v1.SshPrivateKeyCredential - (*structpb.Struct)(nil), // 17: google.protobuf.Struct - (*scopes.ScopeInfo)(nil), // 18: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 19: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp - (*wrapperspb.UInt32Value)(nil), // 21: google.protobuf.UInt32Value - (*wrapperspb.Int32Value)(nil), // 22: google.protobuf.Int32Value - (*wrapperspb.BoolValue)(nil), // 23: google.protobuf.BoolValue + (*PasswordCredential)(nil), // 16: controller.api.resources.targets.v1.PasswordCredential + (*SshPrivateKeyCredential)(nil), // 17: controller.api.resources.targets.v1.SshPrivateKeyCredential + (*structpb.Struct)(nil), // 18: google.protobuf.Struct + (*scopes.ScopeInfo)(nil), // 19: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 20: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp + (*wrapperspb.UInt32Value)(nil), // 22: google.protobuf.UInt32Value + (*wrapperspb.Int32Value)(nil), // 23: google.protobuf.Int32Value + (*wrapperspb.BoolValue)(nil), // 24: google.protobuf.BoolValue } var file_controller_api_resources_targets_v1_target_proto_depIdxs = []int32{ 1, // 0: controller.api.resources.targets.v1.Alias.attributes:type_name -> controller.api.resources.targets.v1.TargetAliasAttributes 2, // 1: controller.api.resources.targets.v1.TargetAliasAttributes.authorize_session_arguments:type_name -> controller.api.resources.targets.v1.AuthorizeSessionArguments - 17, // 2: controller.api.resources.targets.v1.SessionSecret.decoded:type_name -> google.protobuf.Struct + 18, // 2: controller.api.resources.targets.v1.SessionSecret.decoded:type_name -> google.protobuf.Struct 4, // 3: controller.api.resources.targets.v1.SessionCredential.credential_source:type_name -> controller.api.resources.targets.v1.CredentialSource 5, // 4: controller.api.resources.targets.v1.SessionCredential.secret:type_name -> controller.api.resources.targets.v1.SessionSecret - 17, // 5: controller.api.resources.targets.v1.SessionCredential.credential:type_name -> google.protobuf.Struct - 18, // 6: controller.api.resources.targets.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 19, // 7: controller.api.resources.targets.v1.Target.name:type_name -> google.protobuf.StringValue - 19, // 8: controller.api.resources.targets.v1.Target.description:type_name -> google.protobuf.StringValue - 20, // 9: controller.api.resources.targets.v1.Target.created_time:type_name -> google.protobuf.Timestamp - 20, // 10: controller.api.resources.targets.v1.Target.updated_time:type_name -> google.protobuf.Timestamp + 18, // 5: controller.api.resources.targets.v1.SessionCredential.credential:type_name -> google.protobuf.Struct + 19, // 6: controller.api.resources.targets.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 20, // 7: controller.api.resources.targets.v1.Target.name:type_name -> google.protobuf.StringValue + 20, // 8: controller.api.resources.targets.v1.Target.description:type_name -> google.protobuf.StringValue + 21, // 9: controller.api.resources.targets.v1.Target.created_time:type_name -> google.protobuf.Timestamp + 21, // 10: controller.api.resources.targets.v1.Target.updated_time:type_name -> google.protobuf.Timestamp 3, // 11: controller.api.resources.targets.v1.Target.host_sources:type_name -> controller.api.resources.targets.v1.HostSource - 21, // 12: controller.api.resources.targets.v1.Target.session_max_seconds:type_name -> google.protobuf.UInt32Value - 22, // 13: controller.api.resources.targets.v1.Target.session_connection_limit:type_name -> google.protobuf.Int32Value - 19, // 14: controller.api.resources.targets.v1.Target.worker_filter:type_name -> google.protobuf.StringValue - 19, // 15: controller.api.resources.targets.v1.Target.egress_worker_filter:type_name -> google.protobuf.StringValue - 19, // 16: controller.api.resources.targets.v1.Target.ingress_worker_filter:type_name -> google.protobuf.StringValue + 22, // 12: controller.api.resources.targets.v1.Target.session_max_seconds:type_name -> google.protobuf.UInt32Value + 23, // 13: controller.api.resources.targets.v1.Target.session_connection_limit:type_name -> google.protobuf.Int32Value + 20, // 14: controller.api.resources.targets.v1.Target.worker_filter:type_name -> google.protobuf.StringValue + 20, // 15: controller.api.resources.targets.v1.Target.egress_worker_filter:type_name -> google.protobuf.StringValue + 20, // 16: controller.api.resources.targets.v1.Target.ingress_worker_filter:type_name -> google.protobuf.StringValue 4, // 17: controller.api.resources.targets.v1.Target.brokered_credential_sources:type_name -> controller.api.resources.targets.v1.CredentialSource 4, // 18: controller.api.resources.targets.v1.Target.injected_application_credential_sources:type_name -> controller.api.resources.targets.v1.CredentialSource - 17, // 19: controller.api.resources.targets.v1.Target.attributes:type_name -> google.protobuf.Struct + 18, // 19: controller.api.resources.targets.v1.Target.attributes:type_name -> google.protobuf.Struct 8, // 20: controller.api.resources.targets.v1.Target.tcp_target_attributes:type_name -> controller.api.resources.targets.v1.TcpTargetAttributes 9, // 21: controller.api.resources.targets.v1.Target.ssh_target_attributes:type_name -> controller.api.resources.targets.v1.SshTargetAttributes 10, // 22: controller.api.resources.targets.v1.Target.rdp_target_attributes:type_name -> controller.api.resources.targets.v1.RdpTargetAttributes - 19, // 23: controller.api.resources.targets.v1.Target.address:type_name -> google.protobuf.StringValue + 20, // 23: controller.api.resources.targets.v1.Target.address:type_name -> google.protobuf.StringValue 0, // 24: controller.api.resources.targets.v1.Target.aliases:type_name -> controller.api.resources.targets.v1.Alias 0, // 25: controller.api.resources.targets.v1.Target.with_aliases:type_name -> controller.api.resources.targets.v1.Alias - 21, // 26: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 21, // 27: controller.api.resources.targets.v1.TcpTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value - 21, // 28: controller.api.resources.targets.v1.SshTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 21, // 29: controller.api.resources.targets.v1.SshTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value - 19, // 30: controller.api.resources.targets.v1.SshTargetAttributes.storage_bucket_id:type_name -> google.protobuf.StringValue - 23, // 31: controller.api.resources.targets.v1.SshTargetAttributes.enable_session_recording:type_name -> google.protobuf.BoolValue - 21, // 32: controller.api.resources.targets.v1.RdpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value - 21, // 33: controller.api.resources.targets.v1.RdpTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value - 18, // 34: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 20, // 35: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp - 20, // 36: controller.api.resources.targets.v1.SessionAuthorizationData.expiration:type_name -> google.protobuf.Timestamp + 22, // 26: controller.api.resources.targets.v1.TcpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 22, // 27: controller.api.resources.targets.v1.TcpTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value + 22, // 28: controller.api.resources.targets.v1.SshTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 22, // 29: controller.api.resources.targets.v1.SshTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value + 20, // 30: controller.api.resources.targets.v1.SshTargetAttributes.storage_bucket_id:type_name -> google.protobuf.StringValue + 24, // 31: controller.api.resources.targets.v1.SshTargetAttributes.enable_session_recording:type_name -> google.protobuf.BoolValue + 22, // 32: controller.api.resources.targets.v1.RdpTargetAttributes.default_port:type_name -> google.protobuf.UInt32Value + 22, // 33: controller.api.resources.targets.v1.RdpTargetAttributes.default_client_port:type_name -> google.protobuf.UInt32Value + 19, // 34: controller.api.resources.targets.v1.SessionAuthorizationData.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 21, // 35: controller.api.resources.targets.v1.SessionAuthorizationData.created_time:type_name -> google.protobuf.Timestamp + 21, // 36: controller.api.resources.targets.v1.SessionAuthorizationData.expiration:type_name -> google.protobuf.Timestamp 11, // 37: controller.api.resources.targets.v1.SessionAuthorizationData.worker_info:type_name -> controller.api.resources.targets.v1.WorkerInfo - 18, // 38: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 20, // 39: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp - 20, // 40: controller.api.resources.targets.v1.SessionAuthorization.expiration:type_name -> google.protobuf.Timestamp + 19, // 38: controller.api.resources.targets.v1.SessionAuthorization.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 21, // 39: controller.api.resources.targets.v1.SessionAuthorization.created_time:type_name -> google.protobuf.Timestamp + 21, // 40: controller.api.resources.targets.v1.SessionAuthorization.expiration:type_name -> google.protobuf.Timestamp 6, // 41: controller.api.resources.targets.v1.SessionAuthorization.credentials:type_name -> controller.api.resources.targets.v1.SessionCredential 42, // [42:42] is the sub-list for method output_type 42, // [42:42] is the sub-list for method input_type @@ -1822,7 +1871,7 @@ func file_controller_api_resources_targets_v1_target_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_api_resources_targets_v1_target_proto_rawDesc), len(file_controller_api_resources_targets_v1_target_proto_rawDesc)), NumEnums: 0, - NumMessages: 17, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, From ec7450961c578bf88043da74282a0cd98bd47815 Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:21:15 -0500 Subject: [PATCH 07/11] feat(credential/static): Implement CRUDL support for password credentials (#6163) --- internal/credential/public_ids.go | 6 +- internal/credential/static/credential.go | 18 + .../credential/static/password_credential.go | 141 +++ .../static/password_credential_test.go | 164 ++++ internal/credential/static/query.go | 95 ++ .../static/repository_credential.go | 221 +++++ .../static/repository_credential_test.go | 854 +++++++++++++++--- .../static/repository_credentials.go | 22 +- .../static/repository_credentials_test.go | 27 +- internal/credential/static/rewrapping.go | 46 + internal/credential/static/rewrapping_test.go | 64 ++ internal/credential/static/store/static.pb.go | 244 ++++- internal/credential/static/testing.go | 71 ++ .../credential/static/store/v1/static.proto | 63 ++ 14 files changed, 1881 insertions(+), 155 deletions(-) create mode 100644 internal/credential/static/password_credential.go create mode 100644 internal/credential/static/password_credential_test.go diff --git a/internal/credential/public_ids.go b/internal/credential/public_ids.go index 9cee87659e..b5a04e960b 100644 --- a/internal/credential/public_ids.go +++ b/internal/credential/public_ids.go @@ -51,11 +51,11 @@ func NewUsernamePasswordDomainCredentialId(ctx context.Context) (string, error) return id, nil } -// PasswordCredentialId generates a new public ID for a password credential. -func PasswordCredentialId(ctx context.Context) (string, error) { +// NewPasswordCredentialId generates a new public ID for a password credential. +func NewPasswordCredentialId(ctx context.Context) (string, error) { id, err := db.NewPublicId(ctx, globals.PasswordCredentialPrefix) if err != nil { - return "", errors.Wrap(ctx, err, "credential.PasswordCredentialId") + return "", errors.Wrap(ctx, err, "credential.NewPasswordCredentialId") } return id, nil } diff --git a/internal/credential/static/credential.go b/internal/credential/static/credential.go index fd7665239a..2c992bb7d8 100644 --- a/internal/credential/static/credential.go +++ b/internal/credential/static/credential.go @@ -92,6 +92,24 @@ func (c *listCredentialResult) toCredential(ctx context.Context) (credential.Sta cred.PasswordHmac = []byte(c.Hmac1) } return cred, nil + case "p": + cred := &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + PublicId: c.PublicId, + StoreId: c.StoreId, + Name: c.Name, + Description: c.Description, + CreateTime: c.CreateTime, + UpdateTime: c.UpdateTime, + Version: uint32(c.Version), + KeyId: c.KeyId, + }, + } + // Assign byte slices only if the string isn't empty + if c.Hmac1 != "" { + cred.PasswordHmac = []byte(c.Hmac1) + } + return cred, nil case "ssh": cred := &SshPrivateKeyCredential{ SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ diff --git a/internal/credential/static/password_credential.go b/internal/credential/static/password_credential.go new file mode 100644 index 0000000000..e2620befd7 --- /dev/null +++ b/internal/credential/static/password_credential.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package static + +import ( + "context" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/resource" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" + "google.golang.org/protobuf/proto" +) + +var _ credential.Static = (*PasswordCredential)(nil) + +// PasswordCredential contains the credential with a password. +// It is owned by a credential store. +type PasswordCredential struct { + *store.PasswordCredential + tableName string `gorm:"-"` +} + +// NewPasswordCredential creates a new in memory static Credential containing a +// password that is assigned to storeId. Name and description are the only +// valid options. All other options are ignored. +func NewPasswordCredential( + storeId string, + password credential.Password, + opt ...Option, +) (*PasswordCredential, error) { + opts := getOpts(opt...) + l := &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + Password: []byte(password), + }, + } + return l, nil +} + +func allocPasswordCredential() *PasswordCredential { + return &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{}, + } +} + +func (c *PasswordCredential) clone() *PasswordCredential { + cp := proto.Clone(c.PasswordCredential) + return &PasswordCredential{ + PasswordCredential: cp.(*store.PasswordCredential), + } +} + +// TableName returns the table name. +func (c *PasswordCredential) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "credential_static_password_credential" +} + +// SetTableName sets the table name. +func (c *PasswordCredential) SetTableName(n string) { + c.tableName = n +} + +// GetResourceType returns the resource type of the Credential +func (c *PasswordCredential) GetResourceType() resource.Type { + return resource.Credential +} + +func (c *PasswordCredential) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.PublicId}, + "resource-type": []string{"credential-static-password"}, + "op-type": []string{op.String()}, + } + if c.StoreId != "" { + metadata["store-id"] = []string{c.StoreId} + } + return metadata +} + +func (c *PasswordCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).encrypt" + if len(c.Password) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "no password defined") + } + if err := structwrapping.WrapStruct(ctx, cipher, c.PasswordCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id")) + } + c.KeyId = keyId + if err := c.hmacPassword(ctx, cipher); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func (c *PasswordCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, c.PasswordCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + +func (c *PasswordCredential) hmacPassword(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(PasswordCredential).hmacPassword" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + hm, err := crypto.HmacSha256(ctx, c.Password, cipher, []byte(c.StoreId), nil, crypto.WithEd25519()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + c.PasswordHmac = []byte(hm) + return nil +} + +type deletedPasswordCredential struct { + PublicId string `gorm:"primary_key"` + DeleteTime *timestamp.Timestamp +} + +// TableName returns the tablename to override the default gorm table name +func (s *deletedPasswordCredential) TableName() string { + return "credential_static_password_credential_deleted" +} diff --git a/internal/credential/static/password_credential_test.go b/internal/credential/static/password_credential_test.go new file mode 100644 index 0000000000..9600783a3f --- /dev/null +++ b/internal/credential/static/password_credential_test.go @@ -0,0 +1,164 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package static + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestPasswordCredential_New(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + type args struct { + password credential.Password + storeId string + options []Option + } + + tests := []struct { + name string + args args + want *PasswordCredential + wantCreateErr bool + wantEncryptErr bool + }{ + { + name: "missing-password", + args: args{ + storeId: cs.PublicId, + }, + want: allocPasswordCredential(), + wantEncryptErr: true, + }, + { + name: "missing-store-id", + args: args{ + password: "test-pass", + }, + want: allocPasswordCredential(), + wantCreateErr: true, + }, + { + name: "valid-no-options", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + }, + }, + }, + { + name: "valid-with-name", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + options: []Option{WithName("my-credential")}, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + Name: "my-credential", + }, + }, + }, + { + name: "valid-with-description", + args: args{ + password: "test-pass", + storeId: cs.PublicId, + options: []Option{WithDescription("my-credential-description")}, + }, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-pass"), + StoreId: cs.PublicId, + Description: "my-credential-description", + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + + got, err := NewPasswordCredential(tt.args.storeId, tt.args.password, tt.args.options...) + + require.NoError(err) + require.NotNil(got) + assert.Emptyf(got.PublicId, "PublicId set") + id, err := credential.NewPasswordCredentialId(ctx) + require.NoError(err) + + tt.want.PublicId = id + got.PublicId = id + + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + err = got.encrypt(ctx, databaseWrapper) + if tt.wantEncryptErr { + require.Error(err) + return + } + assert.NoError(err) + + err = rw.Create(context.Background(), got) + if tt.wantCreateErr { + require.Error(err) + return + } + assert.NoError(err) + + got2 := allocPasswordCredential() + got2.PublicId = id + assert.Equal(id, got2.GetPublicId()) + require.NoError(rw.LookupById(ctx, got2)) + + err = got2.decrypt(ctx, databaseWrapper) + require.NoError(err) + + // Timestamps and version are automatically set + tt.want.CreateTime = got2.CreateTime + tt.want.UpdateTime = got2.UpdateTime + tt.want.Version = got2.Version + + // KeyId is allocated via kms no need to validate in this test + tt.want.KeyId = got2.KeyId + got2.CtPassword = nil + + // encrypt also calculates the hmac, validate it is correct + hm, err := crypto.HmacSha256(ctx, got.Password, databaseWrapper, []byte(got.StoreId), nil, crypto.WithEd25519()) + require.NoError(err) + tt.want.PasswordHmac = []byte(hm) + + assert.Empty(cmp.Diff(tt.want, got2.clone(), protocmp.Transform())) + }) + } +} diff --git a/internal/credential/static/query.go b/internal/credential/static/query.go index 9fbbab6fa4..4225b0fa11 100644 --- a/internal/credential/static/query.go +++ b/internal/credential/static/query.go @@ -26,6 +26,16 @@ select distinct upd.public_id, and upd.key_id = ?; ` + credStaticPasswordRewrapQuery = ` +select distinct pass.public_id, + pass.password_encrypted, + pass.key_id + from credential_static_password_credential pass + inner join credential_static_store store + on store.public_id = pass.store_id + where store.project_id = ? + and pass.key_id = ?; +` credStaticSshPrivKeyRewrapQuery = ` select distinct ssh.public_id, ssh.private_key_encrypted, @@ -56,6 +66,7 @@ select sum(reltuples::bigint) as estimate 'credential_static_json_credential'::regclass, 'credential_static_username_password_credential'::regclass, 'credential_static_username_password_domain_credential'::regclass, + 'credential_static_password_credential'::regclass, 'credential_static_ssh_private_key_credential'::regclass ) ` @@ -83,6 +94,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -137,6 +153,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -182,6 +214,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -236,6 +273,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -281,6 +334,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -335,6 +393,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, @@ -381,6 +455,11 @@ upd_creds as ( from credential_static_username_password_domain_credential where public_id in (select public_id from credentials) ), +p_creds as ( + select * + from credential_static_password_credential + where public_id in (select public_id from credentials) +), ssh_creds as ( select * from credential_static_ssh_private_key_credential @@ -435,6 +514,22 @@ final as ( 'upd' as type from upd_creds union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + null as username, -- Add this to make the union uniform + null as domain, -- Add this to make the union uniform + key_id, + password_hmac as hmac1, + null::bytea as hmac2, -- Add this to make the union uniform + 'p' as type + from p_creds + union select public_id, store_id, project_id, diff --git a/internal/credential/static/repository_credential.go b/internal/credential/static/repository_credential.go index 4b851ec5a6..f5ca8d304f 100644 --- a/internal/credential/static/repository_credential.go +++ b/internal/credential/static/repository_credential.go @@ -196,6 +196,89 @@ func (r *Repository) CreateUsernamePasswordDomainCredential( return newCred, nil } +// CreatePasswordCredential inserts c into the repository and returns a new +// PasswordCredential containing the credential's PublicId. c is not +// changed. c must not contain a PublicId. The PublicId is generated and +// assigned by this method. c must contain a valid StoreId. +// +// The password is encrypted and a HmacSha256 of the password is calculated. Only the +// PasswordHmac is returned, the plain-text and encrypted password is not returned. +// +// Both c.Name and c.Description are optional. If c.Name is set, it must +// be unique within c.ProjectId. Both c.CreateTime and c.UpdateTime are +// ignored. +func (r *Repository) CreatePasswordCredential( + ctx context.Context, + projectId string, + c *PasswordCredential, + _ ...Option, +) (*PasswordCredential, error) { + const op = "static.(Repository).CreatePasswordCredential" + if c == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.PasswordCredential == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.Password == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password") + } + if c.StoreId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + if c.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + + c = c.clone() + id, err := credential.NewPasswordCredentialId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + c.PublicId = id + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + // encrypt + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var newCred *PasswordCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newCred = c.clone() + if err := w.Create(ctx, newCred, + db.WithOplog(oplogWrapper, newCred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s: name %s already exists", c.StoreId, c.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s", c.StoreId))) + } + + // Clear password fields, only PasswordHmac should be returned + newCred.CtPassword = nil + newCred.Password = nil + + return newCred, nil +} + // CreateSshPrivateKeyCredential inserts c into the repository and returns a new // SshPrivateKeyCredential containing the credential's PublicId. c is not // changed. c must not contain a PublicId. The PublicId is generated and @@ -411,6 +494,20 @@ func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ .. updCred.Password = nil cred = updCred + case credential.PasswordSubtype: + pCred := allocPasswordCredential() + pCred.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, pCred); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId))) + } + // Clear password fields, only passwordHmac should be returned + pCred.CtPassword = nil + pCred.Password = nil + cred = pCred + case credential.SshPrivateKeySubtype: spkCred := allocSshPrivateKeyCredential() spkCred.PublicId = publicId @@ -676,6 +773,118 @@ func (r *Repository) UpdateUsernamePasswordDomainCredential(ctx context.Context, return returnedCredential, rowsUpdated, nil } +// UpdatePasswordCredential updates the repository entry for c.PublicId with +// the values in c for the fields listed in fieldMaskPaths. It returns a +// new PasswordCredential containing the updated values and a count of the +// number of records updated. c is not changed. +// +// c must contain a valid PublicId. Only Name, Description and Password can be +// changed. If c.Name is set to a non-empty string, it must be unique within c.ProjectId. +// +// An attribute of c will be set to NULL in the database if the attribute +// in c is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdatePasswordCredential(ctx context.Context, + projectId string, + c *PasswordCredential, + version uint32, + fieldMaskPaths []string, + _ ...Option, +) (*PasswordCredential, int, error) { + const op = "static.(Repository).UpdatePasswordCredential" + if c == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.PasswordCredential == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if c.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.StoreId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + c = c.clone() + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(passwordField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + dbMask, nullFields := dbw.BuildUpdatePaths( + map[string]any{ + nameField: c.Name, + descriptionField: c.Description, + passwordField: c.Password, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + for _, f := range fieldMaskPaths { + if strings.EqualFold(passwordField, f) { + // Password has been updated, re-encrypt and recalculate hmac + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + + // Set PasswordHmac and CtPassword masks for update. + dbMask = append(dbMask, "PasswordHmac", "CtPassword", "KeyId") + } + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, + errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsUpdated int + var returnedCredential *PasswordCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedCredential = c.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedCredential, + dbMask, nullFields, + db.WithOplog(oplogWrapper, returnedCredential.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, err + } + + // Clear password fields, only PasswordHmac should be returned + returnedCredential.CtPassword = nil + returnedCredential.Password = nil + + return returnedCredential, rowsUpdated, nil +} + // UpdateSshPrivateKeyCredential updates the repository entry for c.PublicId // with the values in c for the fields listed in fieldMaskPaths. It returns a // new SshPrivateKeyCredential containing the updated values and a count of the @@ -1089,6 +1298,11 @@ func (r *Repository) DeleteCredential(ctx context.Context, projectId, id string, c.PublicId = id input = c md = c.oplog(oplog.OpType_OP_TYPE_DELETE) + case credential.PasswordSubtype: + c := allocPasswordCredential() + c.PublicId = id + input = c + md = c.oplog(oplog.OpType_OP_TYPE_DELETE) case credential.SshPrivateKeySubtype: c := allocSshPrivateKeyCredential() c.PublicId = id @@ -1177,6 +1391,13 @@ func (r *Repository) ListDeletedCredentialIds(ctx context.Context, since time.Ti for _, cl := range deletedUsernamePasswordDomainCredentials { credentialStoreIds = append(credentialStoreIds, cl.PublicId) } + var deletedPasswordCredentials []*deletedPasswordCredential + if err := r.SearchWhere(ctx, &deletedPasswordCredentials, "delete_time >= ?", []any{since}); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted password credentials")) + } + for _, cl := range deletedPasswordCredentials { + credentialStoreIds = append(credentialStoreIds, cl.PublicId) + } var deletedSSHPrivateKeyCredentials []*deletedSSHPrivateKeyCredential if err := r.SearchWhere(ctx, &deletedSSHPrivateKeyCredentials, "delete_time >= ?", []any{since}); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted ssh private key credentials")) diff --git a/internal/credential/static/repository_credential_test.go b/internal/credential/static/repository_credential_test.go index 30293e26b0..ee30655570 100644 --- a/internal/credential/static/repository_credential_test.go +++ b/internal/credential/static/repository_credential_test.go @@ -391,6 +391,162 @@ func TestRepository_CreateUsernamePasswordDomainCredential(t *testing.T) { }) } +func TestRepository_CreatePasswordCredential(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + tests := []struct { + name string + projectId string + cred *PasswordCredential + wantErr bool + wantErrCode errors.Code + }{ + { + name: "missing-store", + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-embedded-cred", + cred: &PasswordCredential{}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-project-id", + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-password", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-store-id", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + projectId: prj.PublicId, + cred: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("secret"), + StoreId: cs.PublicId, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + require.NotNil(repo) + + got, err := repo.CreatePasswordCredential(ctx, tt.projectId, tt.cred) + if tt.wantErr { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assertPublicId(t, globals.PasswordCredentialPrefix, got.PublicId) + assert.Nil(got.Password) + assert.Nil(got.CtPassword) + + // Validate password + lookupCred := allocPasswordCredential() + lookupCred.PublicId = got.PublicId + require.NoError(rw.LookupById(ctx, lookupCred)) + + databaseWrapper, err := kkms.GetWrapper(context.Background(), tt.projectId, kms.KeyPurposeDatabase) + require.NoError(err) + require.NoError(lookupCred.decrypt(ctx, databaseWrapper)) + assert.Equal(tt.cred.Password, lookupCred.Password) + + assert.Empty(got.Password) + assert.Empty(got.CtPassword) + assert.NotEmpty(got.PasswordHmac) + + // Validate hmac + hm, err := crypto.HmacSha256(ctx, tt.cred.Password, databaseWrapper, []byte(tt.cred.StoreId), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) + + // Validate oplog + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + prj2 := iam.TestProject(t, iam.TestRepo(t, conn, wrapper), org.GetPublicId()) + require.NoError(err) + + prjCs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + prj2Cs := TestCredentialStore(t, conn, wrapper, prj2.GetPublicId()) + + in, err := NewPasswordCredential(prjCs.GetPublicId(), "pass", WithName("my-name"), WithDescription("original")) + assert.NoError(err) + + got, err := repo.CreatePasswordCredential(ctx, prj.PublicId, in) + require.NoError(err) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2, err := NewPasswordCredential(prjCs.GetPublicId(), "pass", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got2, err := repo.CreatePasswordCredential(ctx, prj.GetPublicId(), in2) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + + // Creating credential in different project should not conflict + in3, err := NewPasswordCredential(prj2Cs.GetPublicId(), "pass", WithName("my-name"), WithDescription("different")) + require.NoError(err) + got3, err := repo.CreatePasswordCredential(ctx, prj2.GetPublicId(), in3) + require.NoError(err) + assert.Equal(in3.Name, got3.Name) + assert.Equal(in3.Description, got3.Description) + + assert.NotEqual(got.PublicId, got3.PublicId) + }) +} + func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -766,6 +922,7 @@ func TestRepository_LookupCredential(t *testing.T) { store := TestCredentialStore(t, conn, wrapper, prj.PublicId) upCred := TestUsernamePasswordCredential(t, conn, wrapper, "username", "password", store.PublicId, prj.PublicId) updCred := TestUsernamePasswordDomainCredential(t, conn, wrapper, "username", "password", "domain.com", store.PublicId, prj.PublicId) + pCred := TestPasswordCredential(t, conn, wrapper, "password", store.PublicId, prj.PublicId) spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.PublicId, prj.PublicId) spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "username", string(testdata.PEMEncryptedKeys[0].PEMBytes), store.PublicId, prj.PublicId, WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey))) @@ -790,6 +947,11 @@ func TestRepository_LookupCredential(t *testing.T) { id: updCred.GetPublicId(), want: updCred, }, + { + name: "p-valid", + id: pCred.GetPublicId(), + want: pCred, + }, { name: "spk-valid", id: spkCred.GetPublicId(), @@ -849,6 +1011,10 @@ func TestRepository_LookupCredential(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *PasswordCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -884,6 +1050,7 @@ func TestRepository_ListCredentials(t *testing.T) { store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/4) TestUsernamePasswordDomainCredentials(t, conn, wrapper, "user", "pass", "domain.com", store.GetPublicId(), prj.GetPublicId(), total/4) + TestPasswordCredentials(t, conn, wrapper, "pass", store.GetPublicId(), prj.GetPublicId(), total/4) TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/4) obj, _ := TestJsonObject(t) @@ -948,6 +1115,10 @@ func TestRepository_ListCredentials(t *testing.T) { assert.Empty(v.Password) assert.Empty(v.CtPassword) assert.NotEmpty(v.PasswordHmac) + case *PasswordCredential: + assert.Empty(v.Password) + assert.Empty(v.CtPassword) + assert.NotEmpty(v.PasswordHmac) case *SshPrivateKeyCredential: assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) @@ -979,6 +1150,7 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { _ = TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), 2) _ = TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) _ = TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain.com", store.GetPublicId(), prj.GetPublicId(), 1) + _ = TestPasswordCredentials(t, conn, wrapper, "testpassword", store.GetPublicId(), prj.GetPublicId(), 1) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -1015,15 +1187,16 @@ func TestRepository_ListCredentials_Pagination(t *testing.T) { page4, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page3[1])) require.NoError(err) - require.Len(page4, 1) + require.Len(page4, 2) pages = append(pages, page3...) for _, item := range pages { assert.NotEqual(item.GetPublicId(), page4[0].GetPublicId()) + assert.NotEqual(item.GetPublicId(), page4[1].GetPublicId()) } assert.True(time.Now().Before(ttime.Add(10 * time.Second))) assert.True(time.Now().After(ttime.Add(-10 * time.Second))) - page5, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page4[0])) + page5, ttime, err := repo.ListCredentials(ctx, store.GetPublicId(), credential.WithLimit(2), credential.WithStartPageAfterItem(page4[1])) require.NoError(err) require.Empty(page5) assert.True(time.Now().Before(ttime.Add(10 * time.Second))) @@ -2292,133 +2465,68 @@ func TestRepository_UpdateUsernamePasswordDomainCredential(t *testing.T) { } } -func TestRepository_UpdatePasswordCredentialKeyUpdate(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - assert, require := assert.New(t), require.New(t) - ctx := context.Background() - kkms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(ctx, rw, rw, kkms) - require.NoError(err) - - _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - credStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) - orig, err := repo.CreateUsernamePasswordCredential(ctx, prj.GetPublicId(), &UsernamePasswordCredential{ - UsernamePasswordCredential: &store.UsernamePasswordCredential{ - Username: "user", - Password: []byte("pass"), - StoreId: credStore.PublicId, - }, - }) - require.NoError(err) - - err = kkms.RotateKeys(ctx, prj.GetPublicId()) - require.NoError(err) - - orig.Password = []byte("pass1") // Company policy to change password every 3 months - - got, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), orig, orig.GetVersion(), []string{"Password"}) - require.NoError(err) - - // Validate that the KeyId has changed - assert.NotEqual(orig.KeyId, got.KeyId) - - // Validate hmac - databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) - require.NoError(err) - hm, err := crypto.HmacSha256(ctx, orig.Password, databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) - require.NoError(err) - assert.Equal([]byte(hm), got.PasswordHmac) -} - -func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) { - const testSecondarySshPrivateKeyPem = ` ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3 -6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ -AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI -Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= ------END OPENSSH PRIVATE KEY----- -` - +func TestRepository_UpdatePasswordCredential(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - changeName := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + changeName := func(n string) func(credential *PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Name = n return c } } - changeDescription := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + changeDescription := func(d string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Description = d return c } } - makeNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + makeNil := func() func(*PasswordCredential) *PasswordCredential { + return func(_ *PasswordCredential) *PasswordCredential { return nil } } - makeEmbeddedNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return &SshPrivateKeyCredential{} + makeEmbeddedNil := func() func(*PasswordCredential) *PasswordCredential { + return func(_ *PasswordCredential) *PasswordCredential { + return &PasswordCredential{} } } - setPublicId := func(n string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + setPublicId := func(n string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.PublicId = n return c } } - deleteStoreId := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + deleteStoreId := func() func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.StoreId = "" return c } } - deleteVersion := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + deleteVersion := func() func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { c.Version = 0 return c } } - changeUser := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.Username = n - return c - } - } - - changePrivateKey := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.PrivateKey = []byte(d) - return c - } - } - - changePrivateKeyPassphrase := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { - c.PrivateKeyPassphrase = []byte(d) + changePassword := func(d string) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { + c.Password = []byte(d) return c } } - combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { - return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + combine := func(fns ...func(cs *PasswordCredential) *PasswordCredential) func(*PasswordCredential) *PasswordCredential { + return func(c *PasswordCredential) *PasswordCredential { for _, fn := range fns { c = fn(c) } @@ -2428,19 +2536,18 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= tests := []struct { name string - orig *SshPrivateKeyCredential - chgFn func(*SshPrivateKeyCredential) *SshPrivateKeyCredential + orig *PasswordCredential + chgFn func(*PasswordCredential) *PasswordCredential masks []string - want *SshPrivateKeyCredential + want *PasswordCredential wantCount int wantErr errors.Code }{ { name: "nil-credential", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: makeNil(), @@ -2449,10 +2556,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "nil-embedded-credential", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: makeEmbeddedNil(), @@ -2461,10 +2567,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-public-id", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: setPublicId(""), @@ -2473,10 +2578,9 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-store-id", - orig: &SshPrivateKeyCredential{ - SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ - Username: "user", - PrivateKey: []byte(TestSshPrivateKeyPem), + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), }, }, chgFn: deleteStoreId(), @@ -2485,7 +2589,502 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= }, { name: "no-version", - orig: &SshPrivateKeyCredential{ + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + chgFn: deleteVersion(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "updating-non-existent-credential", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")), + masks: []string{"Name"}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "ProjectId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "change-password", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + chgFn: changePassword("test-update-pass"), + masks: []string{"Password"}, + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("test-update-pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-password", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeName("new-name"), changePassword("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "new-name", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-update-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + orig: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &PasswordCredential{ + PasswordCredential: &store.PasswordCredential{ + Name: "test-update-name-repo", + Description: "test-description-repo", + Password: []byte("pass"), + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + tt.orig.StoreId = store.PublicId + + orig, err := repo.CreatePasswordCredential(ctx, prj.GetPublicId(), tt.orig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + var version uint32 + if orig != nil { + version = orig.GetVersion() + } + got, gotCount, err := repo.UpdatePasswordCredential(ctx, prj.GetPublicId(), orig, version, tt.masks) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + require.NotNil(got) + assertPublicId(t, globals.PasswordCredentialPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.StoreId, got.StoreId) + underlyingDB, err := conn.SqlDB(ctx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + if tt.want.Name == "" { + got := got.clone() + dbassert.IsNull(got, "name") + } else { + assert.Equal(tt.want.Name, got.Name) + } + + if tt.want.Description == "" { + got := got.clone() + dbassert.IsNull(got, "description") + } else { + assert.Equal(tt.want.Description, got.Description) + } + + assert.Equal(tt.want.Name, got.Name) + + // Validate only passwordHmac is returned + assert.Empty(got.Password) + assert.Empty(got.CtPassword) + assert.NotEmpty(got.PasswordHmac) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, tt.want.Password, databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) + + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} + +func TestRepository_UpdatePasswordCredentialKeyUpdate(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + credStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + orig, err := repo.CreateUsernamePasswordCredential(ctx, prj.GetPublicId(), &UsernamePasswordCredential{ + UsernamePasswordCredential: &store.UsernamePasswordCredential{ + Username: "user", + Password: []byte("pass"), + StoreId: credStore.PublicId, + }, + }) + require.NoError(err) + + err = kkms.RotateKeys(ctx, prj.GetPublicId()) + require.NoError(err) + + orig.Password = []byte("pass1") // Company policy to change password every 3 months + + got, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), orig, orig.GetVersion(), []string{"Password"}) + require.NoError(err) + + // Validate that the KeyId has changed + assert.NotEqual(orig.KeyId, got.KeyId) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, orig.Password, databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) +} + +func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) { + const testSecondarySshPrivateKeyPem = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3 +6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ +AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI +Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= +-----END OPENSSH PRIVATE KEY----- +` + + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + changeName := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Name = n + return c + } + } + + changeDescription := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Description = d + return c + } + } + + makeNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return nil + } + } + + makeEmbeddedNil := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(_ *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return &SshPrivateKeyCredential{} + } + } + + setPublicId := func(n string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PublicId = n + return c + } + } + + deleteStoreId := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.StoreId = "" + return c + } + } + + deleteVersion := func() func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Version = 0 + return c + } + } + + changeUser := func(n string) func(credential *SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.Username = n + return c + } + } + + changePrivateKey := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PrivateKey = []byte(d) + return c + } + } + + changePrivateKeyPassphrase := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + c.PrivateKeyPassphrase = []byte(d) + return c + } + } + + combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential { + return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential { + for _, fn := range fns { + c = fn(c) + } + return c + } + } + + tests := []struct { + name string + orig *SshPrivateKeyCredential + chgFn func(*SshPrivateKeyCredential) *SshPrivateKeyCredential + masks []string + want *SshPrivateKeyCredential + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: setPublicId(""), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidPublicId, + }, + { + name: "no-store-id", + orig: &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + Username: "user", + PrivateKey: []byte(TestSshPrivateKeyPem), + }, + }, + chgFn: deleteStoreId(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-version", + orig: &SshPrivateKeyCredential{ SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ Username: "user", PrivateKey: []byte(TestSshPrivateKeyPem), @@ -3407,6 +4006,7 @@ func TestRepository_ListDeletedCredentialIds(t *testing.T) { sshCreds := TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), 2) pwCreds := TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) updCreds := TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain", store.GetPublicId(), prj.GetPublicId(), 2) + pCreds := TestPasswordCredentials(t, conn, wrapper, "testpassword", store.GetPublicId(), prj.GetPublicId(), 2) repo, err := NewRepository(ctx, rw, rw, kms) require.NoError(err) @@ -3490,6 +4090,23 @@ func TestRepository_ListDeletedCredentialIds(t *testing.T) { require.True(time.Now().Before(ttime.Add(10 * time.Second))) require.True(time.Now().After(ttime.Add(-10 * time.Second))) + // Delete a p credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pCreds[0].GetPublicId()) + require.NoError(err) + + // Expect five entries + deletedIds, ttime, err = repo.ListDeletedCredentialIds(ctx, time.Now().AddDate(-1, 0, 0)) + require.NoError(err) + assert.Empty( + cmp.Diff( + []string{jsonCreds[0].GetPublicId(), sshCreds[0].GetPublicId(), pwCreds[0].GetPublicId(), updCreds[0].GetPublicId(), pCreds[0].GetPublicId()}, + deletedIds, + cmpopts.SortSlices(func(i, j string) bool { return i < j }), + ), + ) + require.True(time.Now().Before(ttime.Add(10 * time.Second))) + require.True(time.Now().After(ttime.Add(-10 * time.Second))) + // Try again with the time set to now, expect no entries deletedIds, ttime, err = repo.ListDeletedCredentialIds(ctx, time.Now()) require.NoError(err) @@ -3529,13 +4146,14 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { sshCreds := TestSshPrivateKeyCredentials(t, conn, wrapper, "username", TestSshPrivateKeyPem, staticStore.GetPublicId(), prj.GetPublicId(), 2) pwCreds := TestUsernamePasswordCredentials(t, conn, wrapper, "username", "testpassword", staticStore.GetPublicId(), prj.GetPublicId(), 2) updCreds := TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "testpassword", "domain", staticStore.GetPublicId(), prj.GetPublicId(), 2) + pCreds := TestPasswordCredentials(t, conn, wrapper, "testpassword", staticStore.GetPublicId(), prj.GetPublicId(), 2) // Run analyze to update postgres meta tables _, err = sqlDb.ExecContext(ctx, "analyze") require.NoError(err) numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(8, numItems) + assert.Equal(10, numItems) // Delete a json credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), jsonCreds[0].GetPublicId()) @@ -3545,7 +4163,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(7, numItems) + assert.Equal(9, numItems) // Delete a ssh credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), sshCreds[0].GetPublicId()) @@ -3555,7 +4173,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(6, numItems) + assert.Equal(8, numItems) // Delete a pw credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pwCreds[0].GetPublicId()) @@ -3565,7 +4183,7 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(5, numItems) + assert.Equal(7, numItems) // Delete a upd credential _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), updCreds[0].GetPublicId()) @@ -3575,5 +4193,15 @@ func TestRepository_EstimatedCredentialCount(t *testing.T) { numItems, err = repo.EstimatedCredentialCount(ctx) require.NoError(err) - assert.Equal(4, numItems) + assert.Equal(6, numItems) + + // Delete a p credential + _, err = staticRepo.DeleteCredential(ctx, prj.GetPublicId(), pCreds[0].GetPublicId()) + require.NoError(err) + _, err = sqlDb.ExecContext(ctx, "analyze") + require.NoError(err) + + numItems, err = repo.EstimatedCredentialCount(ctx) + require.NoError(err) + assert.Equal(5, numItems) } diff --git a/internal/credential/static/repository_credentials.go b/internal/credential/static/repository_credentials.go index 727cbdba36..7c39fd10a9 100644 --- a/internal/credential/static/repository_credentials.go +++ b/internal/credential/static/repository_credentials.go @@ -30,6 +30,11 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin if err != nil { return nil, errors.Wrap(ctx, err, op) } + var pCreds []*PasswordCredential + err = r.reader.SearchWhere(ctx, &pCreds, "public_id in (?)", []any{ids}) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } var spkCreds []*SshPrivateKeyCredential err = r.reader.SearchWhere(ctx, &spkCreds, "public_id in (?)", []any{ids}) if err != nil { @@ -41,9 +46,9 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin return nil, errors.Wrap(ctx, err, op) } - if len(upCreds)+len(updCreds)+len(spkCreds)+len(jsonCreds) != len(ids) { + if len(upCreds)+len(updCreds)+len(pCreds)+len(spkCreds)+len(jsonCreds) != len(ids) { return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, - fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(spkCreds)+len(jsonCreds))) + fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(updCreds)+len(pCreds)+len(spkCreds)+len(jsonCreds))) } out := make([]credential.Static, 0, len(ids)) @@ -73,6 +78,19 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin out = append(out, c) } + for _, c := range pCreds { + // decrypt credential + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.decrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + out = append(out, c) + } + for _, c := range spkCreds { // decrypt credential databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) diff --git a/internal/credential/static/repository_credentials_test.go b/internal/credential/static/repository_credentials_test.go index 3e0bd358d1..aeae0d342c 100644 --- a/internal/credential/static/repository_credentials_test.go +++ b/internal/credential/static/repository_credentials_test.go @@ -39,6 +39,8 @@ func TestRepository_Retrieve(t *testing.T) { upCred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId()) updCred1 := TestUsernamePasswordDomainCredential(t, conn, wrapper, "user", "pass", "domain.com", staticStore.GetPublicId(), prj.GetPublicId()) updCred2 := TestUsernamePasswordDomainCredential(t, conn, wrapper, "different user", "better password", "new-domain.com", staticStore.GetPublicId(), prj.GetPublicId()) + pCred1 := TestPasswordCredential(t, conn, wrapper, "better password", staticStore.GetPublicId(), prj.GetPublicId()) + pCred2 := TestPasswordCredential(t, conn, wrapper, "another password", staticStore.GetPublicId(), prj.GetPublicId()) spkCred1 := TestSshPrivateKeyCredential(t, conn, wrapper, "final user", string(testdata.PEMBytes["ed25519"]), staticStore.GetPublicId(), prj.GetPublicId()) spkCred2 := TestSshPrivateKeyCredential(t, conn, wrapper, "last user", string(testdata.PEMBytes["rsa-openssh-format"]), staticStore.GetPublicId(), prj.GetPublicId()) spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "another last user", @@ -125,6 +127,26 @@ func TestRepository_Retrieve(t *testing.T) { updCred1, updCred2, }, }, + { + name: "valid-one-p-cred", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{pCred1.GetPublicId()}, + }, + wantCreds: []credential.Static{ + pCred1, + }, + }, + { + name: "valid-multiple-p-creds", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{pCred1.GetPublicId(), pCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + pCred1, pCred2, + }, + }, { name: "valid-ssh-pk-cred", args: args{ @@ -169,10 +191,10 @@ func TestRepository_Retrieve(t *testing.T) { name: "valid-mixed-creds", args: args{ projectId: prj.GetPublicId(), - credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId(), updCred1.GetPublicId(), updCred2.GetPublicId()}, + credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId(), updCred1.GetPublicId(), updCred2.GetPublicId(), pCred1.GetPublicId(), pCred2.GetPublicId()}, }, wantCreds: []credential.Static{ - upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, updCred1, updCred2, + upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, updCred1, updCred2, pCred1, pCred2, }, }, } @@ -193,6 +215,7 @@ func TestRepository_Retrieve(t *testing.T) { cmpopts.IgnoreUnexported( UsernamePasswordCredential{}, store.UsernamePasswordCredential{}, UsernamePasswordDomainCredential{}, store.UsernamePasswordDomainCredential{}, + PasswordCredential{}, store.PasswordCredential{}, SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}, JsonCredential{}, store.JsonCredential{}), cmpopts.IgnoreTypes(×tamp.Timestamp{}), diff --git a/internal/credential/static/rewrapping.go b/internal/credential/static/rewrapping.go index d8f2784801..3d26cdb4fe 100644 --- a/internal/credential/static/rewrapping.go +++ b/internal/credential/static/rewrapping.go @@ -17,6 +17,7 @@ func init() { kms.RegisterTableRewrapFn("credential_static_ssh_private_key_credential", credStaticSshPrivKeyRewrapFn) kms.RegisterTableRewrapFn("credential_static_json_credential", credStaticJsonRewrapFn) kms.RegisterTableRewrapFn("credential_static_username_password_domain_credential", credStaticUsernamePasswordDomainRewrapFn) + kms.RegisterTableRewrapFn("credential_static_password_credential", credStaticPasswordRewrapFn) } func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) string { @@ -128,6 +129,51 @@ func credStaticUsernamePasswordDomainRewrapFn(ctx context.Context, dataKeyVersio return nil } +func credStaticPasswordRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { + const op = "static.credStaticPasswordRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*PasswordCredential + // Indexes exist on (store_id, etc), so we can query static stores via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, credStaticPasswordRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := allocPasswordCredential() + if err := rows.Scan( + &cred.PublicId, + &cred.CtPassword, + &cred.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt password credential")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt password credential")) + } + if _, err := writer.Update(ctx, cred, []string{"CtPassword", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update password credential row with rewrapped fields")) + } + } + return nil +} + func credStaticSshPrivKeyRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { const op = "static.credStaticSshPrivKeyRewrapFn" if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { diff --git a/internal/credential/static/rewrapping_test.go b/internal/credential/static/rewrapping_test.go index 0fa4d43931..da68bcef95 100644 --- a/internal/credential/static/rewrapping_test.go +++ b/internal/credential/static/rewrapping_test.go @@ -321,3 +321,67 @@ func TestRewrap_credStaticUsernamePasswordDomainRewrapFn(t *testing.T) { assert.Equal(t, cred.GetPasswordHmac(), got.GetPasswordHmac()) }) } + +func TestRewrap_credStaticPasswordRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + mock.ExpectQuery( + `SELECT \* FROM "kms_oplog_schema_version" WHERE 1=1 ORDER BY "kms_oplog_schema_version"."version" LIMIT \$1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct pass\.public_id, pass\.password_encrypted, pass\.key_id from credential_static_password_credential pass inner join credential_static_store store on store\.public_id = pass\.store_id where store\.project_id = \$1 and pass\.key_id = \$2;`, + ).WillReturnError(errors.New("Query error")) + err := credStaticPasswordRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + cred, err := NewPasswordCredential(cs.GetPublicId(), "password") + assert.NoError(t, err) + + cred.PublicId, err = credential.NewPasswordCredentialId(ctx) + assert.NoError(t, err) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + + assert.NoError(t, cred.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), cred)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credStaticPasswordRewrapFn(ctx, cred.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocPasswordCredential() + got.PublicId = cred.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, "password", string(got.GetPassword())) + assert.NotEmpty(t, got.GetPasswordHmac()) + assert.Equal(t, cred.GetPasswordHmac(), got.GetPasswordHmac()) + }) +} diff --git a/internal/credential/static/store/static.pb.go b/internal/credential/static/store/static.pb.go index d8b368a1d5..0c4b70c191 100644 --- a/internal/credential/static/store/static.pb.go +++ b/internal/credential/static/store/static.pb.go @@ -136,6 +136,157 @@ func (x *CredentialStore) GetVersion() uint32 { return 0 } +type PasswordCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + StoreId string `protobuf:"bytes,6,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // password is the plain-text of the password associated with the credential. We are + // not storing this plain-text password in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + Password []byte `protobuf:"bytes,8,opt,name=password,proto3" json:"password,omitempty" gorm:"-" wrapping:"pt,password_data"` + // ct_password is the ciphertext of the password. It + // is stored in the database. + // @inject_tag: `gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + CtPassword []byte `protobuf:"bytes,9,opt,name=ct_password,json=ctPassword,proto3" json:"ct_password,omitempty" gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + // password_hmac is a sha256-hmac of the unencrypted password. It is recalculated + // everytime the password is updated. + // @inject_tag: `gorm:"not_null"` + PasswordHmac []byte `protobuf:"bytes,10,opt,name=password_hmac,json=passwordHmac,proto3" json:"password_hmac,omitempty" gorm:"not_null"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,11,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordCredential) Reset() { + *x = PasswordCredential{} + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordCredential) ProtoMessage() {} + +func (x *PasswordCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordCredential.ProtoReflect.Descriptor instead. +func (*PasswordCredential) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{1} +} + +func (x *PasswordCredential) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *PasswordCredential) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *PasswordCredential) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *PasswordCredential) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PasswordCredential) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PasswordCredential) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *PasswordCredential) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *PasswordCredential) GetPassword() []byte { + if x != nil { + return x.Password + } + return nil +} + +func (x *PasswordCredential) GetCtPassword() []byte { + if x != nil { + return x.CtPassword + } + return nil +} + +func (x *PasswordCredential) GetPasswordHmac() []byte { + if x != nil { + return x.PasswordHmac + } + return nil +} + +func (x *PasswordCredential) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + type UsernamePasswordCredential struct { state protoimpl.MessageState `protogen:"open.v1"` // public_id is a surrogate key suitable for use in a public API. @@ -186,7 +337,7 @@ type UsernamePasswordCredential struct { func (x *UsernamePasswordCredential) Reset() { *x = UsernamePasswordCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -198,7 +349,7 @@ func (x *UsernamePasswordCredential) String() string { func (*UsernamePasswordCredential) ProtoMessage() {} func (x *UsernamePasswordCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[1] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -211,7 +362,7 @@ func (x *UsernamePasswordCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordCredential.ProtoReflect.Descriptor instead. func (*UsernamePasswordCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{1} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{2} } func (x *UsernamePasswordCredential) GetPublicId() string { @@ -352,7 +503,7 @@ type UsernamePasswordDomainCredential struct { func (x *UsernamePasswordDomainCredential) Reset() { *x = UsernamePasswordDomainCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -364,7 +515,7 @@ func (x *UsernamePasswordDomainCredential) String() string { func (*UsernamePasswordDomainCredential) ProtoMessage() {} func (x *UsernamePasswordDomainCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[2] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -377,7 +528,7 @@ func (x *UsernamePasswordDomainCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordDomainCredential.ProtoReflect.Descriptor instead. func (*UsernamePasswordDomainCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{2} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{3} } func (x *UsernamePasswordDomainCredential) GetPublicId() string { @@ -534,7 +685,7 @@ type SshPrivateKeyCredential struct { func (x *SshPrivateKeyCredential) Reset() { *x = SshPrivateKeyCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -546,7 +697,7 @@ func (x *SshPrivateKeyCredential) String() string { func (*SshPrivateKeyCredential) ProtoMessage() {} func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -559,7 +710,7 @@ func (x *SshPrivateKeyCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyCredential.ProtoReflect.Descriptor instead. func (*SshPrivateKeyCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{3} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{4} } func (x *SshPrivateKeyCredential) GetPublicId() string { @@ -713,7 +864,7 @@ type JsonCredential struct { func (x *JsonCredential) Reset() { *x = JsonCredential{} - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -725,7 +876,7 @@ func (x *JsonCredential) String() string { func (*JsonCredential) ProtoMessage() {} func (x *JsonCredential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[4] + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -738,7 +889,7 @@ func (x *JsonCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use JsonCredential.ProtoReflect.Descriptor instead. func (*JsonCredential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{4} + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{5} } func (x *JsonCredential) GetPublicId() string { @@ -835,7 +986,27 @@ const file_controller_storage_credential_static_store_v1_static_proto_rawDesc = "\vDescription\x12\vdescriptionR\vdescription\x12\x1d\n" + "\n" + "project_id\x18\x06 \x01(\tR\tprojectId\x12\x18\n" + - "\aversion\x18\a \x01(\rR\aversion\"\xfd\x04\n" + + "\aversion\x18\a \x01(\rR\aversion\"\xb4\x04\n" + + "\x12PasswordCredential\x12\x1b\n" + + "\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12K\n" + + "\vcreate_time\x18\x02 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "createTime\x12K\n" + + "\vupdate_time\x18\x03 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "updateTime\x12$\n" + + "\x04name\x18\x04 \x01(\tB\x10\xc2\xdd)\f\n" + + "\x04Name\x12\x04nameR\x04name\x12@\n" + + "\vdescription\x18\x05 \x01(\tB\x1e\xc2\xdd)\x1a\n" + + "\vDescription\x12\vdescriptionR\vdescription\x12\x19\n" + + "\bstore_id\x18\x06 \x01(\tR\astoreId\x12\x18\n" + + "\aversion\x18\a \x01(\rR\aversion\x12?\n" + + "\bpassword\x18\b \x01(\fB#\xc2\xdd)\x1f\n" + + "\bPassword\x12\x13attributes.passwordR\bpassword\x12\x1f\n" + + "\vct_password\x18\t \x01(\fR\n" + + "ctPassword\x12Q\n" + + "\rpassword_hmac\x18\n" + + " \x01(\fB,\xc2\xdd)(\n" + + "\fPasswordHmac\x12\x18attributes.password_hmacR\fpasswordHmac\x12\x15\n" + + "\x06key_id\x18\v \x01(\tR\x05keyId\"\xfd\x04\n" + "\x1aUsernamePasswordCredential\x12\x1b\n" + "\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12K\n" + "\vcreate_time\x18\x02 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + @@ -944,31 +1115,34 @@ func file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP return file_controller_storage_credential_static_store_v1_static_proto_rawDescData } -var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_storage_credential_static_store_v1_static_proto_goTypes = []any{ (*CredentialStore)(nil), // 0: controller.storage.credential.static.store.v1.CredentialStore - (*UsernamePasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.UsernamePasswordCredential - (*UsernamePasswordDomainCredential)(nil), // 2: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential - (*SshPrivateKeyCredential)(nil), // 3: controller.storage.credential.static.store.v1.SshPrivateKeyCredential - (*JsonCredential)(nil), // 4: controller.storage.credential.static.store.v1.JsonCredential - (*timestamp.Timestamp)(nil), // 5: controller.storage.timestamp.v1.Timestamp + (*PasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.PasswordCredential + (*UsernamePasswordCredential)(nil), // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential + (*UsernamePasswordDomainCredential)(nil), // 3: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential + (*SshPrivateKeyCredential)(nil), // 4: controller.storage.credential.static.store.v1.SshPrivateKeyCredential + (*JsonCredential)(nil), // 5: controller.storage.credential.static.store.v1.JsonCredential + (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_static_store_v1_static_proto_depIdxs = []int32{ - 5, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 4: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 5: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 6: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 7: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 8: controller.storage.credential.static.store.v1.JsonCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 9: controller.storage.credential.static.store.v1.JsonCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 6, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 2: controller.storage.credential.static.store.v1.PasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 3: controller.storage.credential.static.store.v1.PasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 4: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 5: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 6: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 7: controller.storage.credential.static.store.v1.UsernamePasswordDomainCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 8: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 9: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 10: controller.storage.credential.static.store.v1.JsonCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 11: controller.storage.credential.static.store.v1.JsonCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_controller_storage_credential_static_store_v1_static_proto_init() } @@ -982,7 +1156,7 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_credential_static_store_v1_static_proto_rawDesc), len(file_controller_storage_credential_static_store_v1_static_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/static/testing.go b/internal/credential/static/testing.go index 506fce34b1..1f472d81cb 100644 --- a/internal/credential/static/testing.go +++ b/internal/credential/static/testing.go @@ -284,6 +284,77 @@ func TestUsernamePasswordDomainCredentials( return creds } +// TestPasswordCredential creates a password credential in the provided DB with +// the provided project id and any values passed in through. +// If any errors are encountered during the creation of the store, the test will fail. +func TestPasswordCredential( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + password, storeId, projectId string, + opts ...Option, +) *PasswordCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + w := db.New(conn) + + opt := getOpts(opts...) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + cred, err := NewPasswordCredential(storeId, credential.Password(password), opts...) + require.NoError(t, err) + require.NotNil(t, cred) + + id := opt.withPublicId + if id == "" { + id, err = credential.NewPasswordCredentialId(ctx) + require.NoError(t, err) + } + cred.PublicId = id + + err = cred.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + require.NoError(t, iw.Create(ctx, cred)) + return nil + }, + ) + require.NoError(t, err2) + + return cred +} + +// TestPasswordCredentials creates count number of password credentials in +// the provided DB with the provided project id. If any errors are +// encountered during the creation of the credentials, the test will fail. +func TestPasswordCredentials( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + password, storeId, projectId string, + count int, +) []*PasswordCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + creds := make([]*PasswordCredential, 0, count) + for i := 0; i < count; i++ { + creds = append(creds, TestPasswordCredential(t, conn, wrapper, password, storeId, projectId)) + } + return creds +} + // TestSshPrivateKeyCredential creates an ssh private key credential in the // provided DB with the provided project and any values passed in through. If any // errors are encountered during the creation of the store, the test will fail. diff --git a/internal/proto/controller/storage/credential/static/store/v1/static.proto b/internal/proto/controller/storage/credential/static/store/v1/static.proto index 0a2ea01ad4..49c60a5fc3 100644 --- a/internal/proto/controller/storage/credential/static/store/v1/static.proto +++ b/internal/proto/controller/storage/credential/static/store/v1/static.proto @@ -49,6 +49,69 @@ message CredentialStore { uint32 version = 7; } +message PasswordCredential { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string store_id = 6; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // password is the plain-text of the password associated with the credential. We are + // not storing this plain-text password in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + bytes password = 8 [(custom_options.v1.mask_mapping) = { + this: "Password" + that: "attributes.password" + }]; + + // ct_password is the ciphertext of the password. It + // is stored in the database. + // @inject_tag: `gorm:"column:password_encrypted;not_null" wrapping:"ct,password_data"` + bytes ct_password = 9; + + // password_hmac is a sha256-hmac of the unencrypted password. It is recalculated + // everytime the password is updated. + // @inject_tag: `gorm:"not_null"` + bytes password_hmac = 10 [(custom_options.v1.mask_mapping) = { + this: "PasswordHmac" + that: "attributes.password_hmac" + }]; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 11; +} + message UsernamePasswordCredential { // public_id is a surrogate key suitable for use in a public API. // @inject_tag: `gorm:"primary_key"` From 11c4fd758006b58781ee344f8e43211d627dc08a Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:33:44 -0500 Subject: [PATCH 08/11] feat(handlers): CRUDL support for static password credential (#6181) --- api/credentials/option.gen.go | 12 + api/credentials/password_attributes.gen.go | 41 +++ internal/api/genapi/input.go | 16 ++ .../credentials/credential_service.go | 93 +++++- .../credentials/credential_service_test.go | 269 +++++++++++++++++- .../handlers/targets/credentials.go | 16 +- .../handlers/targets/target_service.go | 3 - .../targets/tcp/target_service_test.go | 148 +++++++++- .../resources/credentials/v1/credential.proto | 5 + .../resources/credentials/credential.pb.go | 55 ++-- 10 files changed, 623 insertions(+), 35 deletions(-) create mode 100644 api/credentials/password_attributes.gen.go diff --git a/api/credentials/option.gen.go b/api/credentials/option.gen.go index 9b3d795902..be36627e57 100644 --- a/api/credentials/option.gen.go +++ b/api/credentials/option.gen.go @@ -179,6 +179,18 @@ func WithJsonCredentialObject(inObject map[string]interface{}) Option { } } +func WithPasswordCredentialPassword(inPassword string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = any(map[string]any{}) + } + val := raw.(map[string]any) + val["password"] = inPassword + o.postMap["attributes"] = val + } +} + func WithUsernamePasswordCredentialPassword(inPassword string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/api/credentials/password_attributes.gen.go b/api/credentials/password_attributes.gen.go new file mode 100644 index 0000000000..8bce99467a --- /dev/null +++ b/api/credentials/password_attributes.gen.go @@ -0,0 +1,41 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package credentials + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type PasswordAttributes struct { + Password string `json:"password,omitempty"` + PasswordHmac string `json:"password_hmac,omitempty"` +} + +func AttributesMapToPasswordAttributes(in map[string]any) (*PasswordAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out PasswordAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *Credential) GetPasswordAttributes() (*PasswordAttributes, error) { + if pt.Type != "password" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential is of type %s", "password", pt.Type) + } + return AttributesMapToPasswordAttributes(pt.Attributes) +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index f204c9af12..08d374f2a2 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -736,6 +736,22 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &credentials.PasswordAttributes{}, + outFile: "credentials/password_attributes.gen.go", + subtypeName: "PasswordCredential", + subtype: "password", + fieldOverrides: []fieldInfo{ + { + Name: "Password", + SkipDefault: true, + }, + }, + parentTypeName: "Credential", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentials.SshPrivateKeyAttributes{}, outFile: "credentials/ssh_private_key_attributes.gen.go", diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index d5469a108b..cffe7e0d41 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -49,6 +49,7 @@ var ( upMaskManager handlers.MaskManager spkMaskManager handlers.MaskManager jsonMaskManager handlers.MaskManager + pMaskmanager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -97,6 +98,13 @@ func init() { ); err != nil { panic(err) } + if pMaskmanager, err = handlers.NewMaskManager( + context.Background(), + handlers.MaskDestination{&store.PasswordCredential{}}, + handlers.MaskSource{&pb.Credential{}, &pb.PasswordAttributes{}}, + ); err != nil { + panic(err) + } // TODO: refactor to remove IdActionsMap and CollectionActions package variables action.RegisterResource(resource.Credential, IdActions, CollectionActions) @@ -454,6 +462,23 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.") } return out, nil + case credential.PasswordSubtype.String(): + cred, err := toPasswordStorageCredential(ctx, item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, err := repo.CreatePasswordCredential(ctx, scopeId, cred) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.") + } + return out, nil case credential.SshPrivateKeySubtype.String(): cred, err := toSshPrivateKeyStorageCredential(ctx, item.GetCredentialStoreId(), item) if err != nil { @@ -551,6 +576,29 @@ func (s Service) updateInRepo( return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) } return out, nil + case credential.PasswordSubtype: + dbMasks = append(dbMasks, pMaskmanager.Translate(masks)...) + if len(dbMasks) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + + cred, err := toPasswordStorageCredential(ctx, storeId, in) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to password storage credential")) + } + cred.PublicId = id + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, rowsUpdated, err := repo.UpdatePasswordCredential(ctx, scopeId, cred, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) + } + return out, nil case credential.SshPrivateKeySubtype: dbMasks = append(dbMasks, spkMaskManager.Translate(masks)...) @@ -731,6 +779,8 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro out.Type = credential.UsernamePasswordSubtype.String() case *static.UsernamePasswordDomainCredential: out.Type = credential.UsernamePasswordDomainSubtype.String() + case *static.PasswordCredential: + out.Type = credential.PasswordSubtype.String() case *static.SshPrivateKeyCredential: out.Type = credential.SshPrivateKeySubtype.String() case *static.JsonCredential: @@ -779,6 +829,14 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro }, } } + case *static.PasswordCredential: + if outputFields.Has(globals.AttributesField) { + out.Attrs = &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + PasswordHmac: base64.RawURLEncoding.EncodeToString(cred.GetPasswordHmac()), + }, + } + } case *static.SshPrivateKeyCredential: if outputFields.Has(globals.AttributesField) { out.Attrs = &pb.Credential_SshPrivateKeyAttributes{ @@ -848,6 +906,28 @@ func toUsernamePasswordDomainStorageCredential(ctx context.Context, storeId stri return cs, err } +func toPasswordStorageCredential(ctx context.Context, storeId string, in *pb.Credential) (out *static.PasswordCredential, err error) { + const op = "credentials.toPasswordStorageCredential" + var opts []static.Option + if in.GetName() != nil { + opts = append(opts, static.WithName(in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, static.WithDescription(in.GetDescription().GetValue())) + } + + attrs := in.GetPasswordAttributes() + cs, err := static.NewPasswordCredential( + storeId, + credential.Password(attrs.GetPassword().GetValue()), + opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential")) + } + + return cs, err +} + func toSshPrivateKeyStorageCredential(ctx context.Context, storeId string, in *pb.Credential) (out *static.SshPrivateKeyCredential, err error) { const op = "credentials.toSshPrivateKeyStorageCredential" var opts []static.Option @@ -919,6 +999,7 @@ func validateGetRequest(req *pbs.GetCredentialRequest) error { globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix, ) @@ -949,6 +1030,10 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error { if req.Item.GetUsernamePasswordDomainAttributes().GetDomain().GetValue() == "" { badFields[domainField] = "Field required for creating a username-password-domain credential." } + case credential.PasswordSubtype.String(): + if req.Item.GetPasswordAttributes().GetPassword().GetValue() == "" { + badFields[passwordField] = "Field required for creating a password credential." + } case credential.SshPrivateKeySubtype.String(): if req.Item.GetSshPrivateKeyAttributes().GetUsername().GetValue() == "" { badFields[usernameField] = "Field required for creating an SSH private key credential." @@ -1018,7 +1103,11 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { if handlers.MaskContains(req.GetUpdateMask().GetPaths(), domainField) && attrs.GetDomain().GetValue() == "" { badFields[domainField] = "This is a required field and cannot be set to empty." } - + case credential.PasswordSubtype: + attrs := req.GetItem().GetPasswordAttributes() + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), passwordField) && attrs.GetPassword().GetValue() == "" { + badFields[passwordField] = "This is a required field and cannot be set to empty." + } case credential.SshPrivateKeySubtype: attrs := req.GetItem().GetSshPrivateKeyAttributes() if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" { @@ -1070,6 +1159,7 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix, ) @@ -1082,6 +1172,7 @@ func validateDeleteRequest(req *pbs.DeleteCredentialRequest) error { globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, + globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix, ) diff --git a/internal/daemon/controller/handlers/credentials/credential_service_test.go b/internal/daemon/controller/handlers/credentials/credential_service_test.go index be53e5eac0..92bea251b7 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service_test.go +++ b/internal/daemon/controller/handlers/credentials/credential_service_test.go @@ -68,7 +68,7 @@ func staticJsonCredentialToProto(cred *static.JsonCredential, prj *iam.Scope, hm } } -func staticPasswordCredentialToProto(cred *static.UsernamePasswordCredential, prj *iam.Scope, hmac string) *pb.Credential { +func staticUsernamePasswordCredentialToProto(cred *static.UsernamePasswordCredential, prj *iam.Scope, hmac string) *pb.Credential { return &pb.Credential{ Id: cred.GetPublicId(), CredentialStoreId: cred.GetStoreId(), @@ -87,7 +87,25 @@ func staticPasswordCredentialToProto(cred *static.UsernamePasswordCredential, pr } } -func staticPasswordDomainCredentialToProto(cred *static.UsernamePasswordDomainCredential, prj *iam.Scope, hmac string) *pb.Credential { +func staticPasswordCredentialToProto(cred *static.PasswordCredential, prj *iam.Scope, hmac string) *pb.Credential { + return &pb.Credential{ + Id: cred.GetPublicId(), + CredentialStoreId: cred.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: cred.GetCreateTime().GetTimestamp(), + UpdatedTime: cred.GetUpdateTime().GetTimestamp(), + Version: cred.GetVersion(), + Type: credential.PasswordSubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(hmac)), + }, + }, + } +} + +func staticUsernamePasswordDomainCredentialToProto(cred *static.UsernamePasswordDomainCredential, prj *iam.Scope, hmac string) *pb.Credential { return &pb.Credential{ Id: cred.GetPublicId(), CredentialStoreId: cred.GetStoreId(), @@ -155,13 +173,18 @@ func TestList(t *testing.T) { c := static.TestUsernamePasswordCredential(t, conn, wrapper, user, pass, store.GetPublicId(), prj.GetPublicId()) hm, err := crypto.HmacSha256(ctx, []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) - wantCreds = append(wantCreds, staticPasswordCredentialToProto(c, prj, hm)) + wantCreds = append(wantCreds, staticUsernamePasswordCredentialToProto(c, prj, hm)) domain := fmt.Sprintf("domain-%d", i) upd := static.TestUsernamePasswordDomainCredential(t, conn, wrapper, user, pass, domain, store.GetPublicId(), prj.GetPublicId()) hm, err = crypto.HmacSha256(ctx, []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) - wantCreds = append(wantCreds, staticPasswordDomainCredentialToProto(upd, prj, hm)) + wantCreds = append(wantCreds, staticUsernamePasswordDomainCredentialToProto(upd, prj, hm)) + + p := static.TestPasswordCredential(t, conn, wrapper, pass, store.GetPublicId(), prj.GetPublicId()) + hm, err = crypto.HmacSha256(ctx, []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(t, err) + wantCreds = append(wantCreds, staticPasswordCredentialToProto(p, prj, hm)) spk := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) hm, err = crypto.HmacSha256(ctx, []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) @@ -193,7 +216,7 @@ func TestList(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 40, + EstItemCount: 50, }, anonRes: &pbs.ListCredentialsResponse{ Items: wantCreds, @@ -247,9 +270,9 @@ func TestList(t *testing.T) { }, { name: "Filter on Attribute", - req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[4].GetUsernamePasswordAttributes().GetUsername().Value)}, + req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[0].GetUsernamePasswordAttributes().GetUsername().Value)}, res: &pbs.ListCredentialsResponse{ - Items: wantCreds[4:7], + Items: []*pb.Credential{wantCreds[0], wantCreds[1], wantCreds[3]}, ResponseType: "complete", ListToken: "", SortBy: "created_time", @@ -363,6 +386,10 @@ func TestGet(t *testing.T) { updHm, err := crypto.HmacSha256(context.Background(), []byte("pass"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) + pCred := static.TestPasswordCredential(t, conn, wrapper, "pass", store.GetPublicId(), prj.GetPublicId()) + pHm, err := crypto.HmacSha256(context.Background(), []byte("pass"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(t, err) + spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) spkHm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) @@ -455,7 +482,27 @@ func TestGet(t *testing.T) { }, }, }, - + { + name: "success-password-only-credential", + id: pCred.GetPublicId(), + res: &pbs.GetCredentialResponse{ + Item: &pb.Credential{ + Id: pCred.GetPublicId(), + CredentialStoreId: pCred.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: credential.PasswordSubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: pCred.CreateTime.GetTimestamp(), + UpdatedTime: pCred.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(pHm)), + }, + }, + }, + }, + }, { name: "success-spk", id: spkCred.GetPublicId(), @@ -589,6 +636,7 @@ func TestDelete(t *testing.T) { upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId()) updCred := static.TestUsernamePasswordDomainCredential(t, conn, wrapper, "user", "pass", "domain", store.GetPublicId(), prj.GetPublicId()) + pCred := static.TestPasswordCredential(t, conn, wrapper, "pass", store.GetPublicId(), prj.GetPublicId()) spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) obj, _ := static.TestJsonObject(t) @@ -609,6 +657,10 @@ func TestDelete(t *testing.T) { name: "success-upd", id: updCred.GetPublicId(), }, + { + name: "success-p", + id: pCred.GetPublicId(), + }, { name: "success-spk", id: spkCred.GetPublicId(), @@ -906,6 +958,92 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Can't specify Id P", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Id: globals.PasswordCredentialPrefix + "_notallowed", + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid Credential Store Id P", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: "p_invalidid", + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + CreatedTime: timestamppb.Now(), + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Updated Time", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + UpdatedTime: timestamppb.Now(), + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Must provide type", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Must provide password", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + // Empty password + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "Must provide private key", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ @@ -959,6 +1097,32 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "valid-p", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Type: credential.PasswordSubtype.String(), + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("password"), + }, + }, + }}, + idPrefix: globals.PasswordCredentialPrefix + "_", + res: &pbs.CreateCredentialResponse{ + Uri: fmt.Sprintf("credentials/%s_", globals.PasswordCredentialPrefix), + Item: &pb.Credential{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: credential.PasswordSubtype.String(), + AuthorizedActions: testAuthorizedActions, + }, + }, + }, { name: "valid-spk", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ @@ -1086,6 +1250,15 @@ func TestCreate(t *testing.T) { assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetUsernamePasswordAttributes().GetPasswordHmac()) assert.Empty(got.GetItem().GetUsernamePasswordAttributes().GetPassword()) + case credential.PasswordSubtype.String(): + password := tc.req.GetItem().GetPasswordAttributes().GetPassword().GetValue() + hm, err := crypto.HmacSha256(ctx, []byte(password), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + + // Validate attributes equal + assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetPasswordAttributes().GetPasswordHmac()) + assert.Empty(got.GetItem().GetPasswordAttributes().GetPassword()) + case credential.SshPrivateKeySubtype.String(): pk := tc.req.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey().GetValue() hm, err := crypto.HmacSha256(ctx, []byte(pk), databaseWrapper, []byte(store.GetPublicId()), nil) @@ -1193,6 +1366,16 @@ func TestUpdate(t *testing.T) { return cred, clean } + freshCredP := func(pass string) (*static.PasswordCredential, func()) { + t.Helper() + cred := static.TestPasswordCredential(t, conn, wrapper, pass, store.GetPublicId(), prj.GetPublicId()) + clean := func() { + _, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()}) + require.NoError(t, err) + } + return cred, clean + } + freshCredSpk := func(user string) (*static.SshPrivateKeyCredential, func()) { t.Helper() cred := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) @@ -1405,6 +1588,26 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "update-password-attributes-passwordOnly", + req: &pbs.UpdateCredentialRequest{ + UpdateMask: fieldmask("attributes.password"), + Item: &pb.Credential{ + Attrs: &pb.Credential_PasswordAttributes{ + PasswordAttributes: &pb.PasswordAttributes{ + Password: wrapperspb.String("new-password"), + }, + }, + }, + }, + res: func(in *pb.Credential) *pb.Credential { + out := proto.Clone(in).(*pb.Credential) + hm, err := crypto.HmacSha256(context.Background(), []byte("new-password"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(t, err) + out.GetPasswordAttributes().PasswordHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) + return out + }, + }, { name: "update-spk", req: &pbs.UpdateCredentialRequest{ @@ -1663,6 +1866,8 @@ func TestUpdate(t *testing.T) { cred, cleanup = freshCredJson() } else if strings.Contains(tc.name, "domainupd") { cred, cleanup = freshCredUpd("user", "pass", "domain") + } else if strings.Contains(tc.name, "passwordOnly") { + cred, cleanup = freshCredP("pass") } else { cred, cleanup = freshCredUp("user", "pass") } @@ -1716,6 +1921,14 @@ func TestUpdate(t *testing.T) { credUp, cleanUp := freshCredUp("user", "pass") defer cleanUp() + // cant update read only fields + credUpd, cleanUpd := freshCredUpd("user", "pass", "domain") + defer cleanUpd() + + // cant update read only fields + credP, cleanP := freshCredP("pass") + defer cleanP() + newStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) roCases := []struct { @@ -1781,6 +1994,42 @@ func TestUpdate(t *testing.T) { } matcher(t, gErr) assert.Nil(t, got) + + req = &pbs.UpdateCredentialRequest{ + Id: credUpd.GetPublicId(), + Item: tc.item, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, + } + req.Item.Version = credUpd.Version + + got, gErr = s.UpdateCredential(ctx, req) + assert.Error(t, gErr) + matcher = tc.matcher + if matcher == nil { + matcher = func(t *testing.T, e error) { + assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) + } + } + matcher(t, gErr) + assert.Nil(t, got) + + req = &pbs.UpdateCredentialRequest{ + Id: credP.GetPublicId(), + Item: tc.item, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, + } + req.Item.Version = credP.Version + + got, gErr = s.UpdateCredential(ctx, req) + assert.Error(t, gErr) + matcher = tc.matcher + if matcher == nil { + matcher = func(t *testing.T, e error) { + assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) + } + } + matcher(t, gErr) + assert.Nil(t, got) }) } } @@ -1841,12 +2090,12 @@ func TestListPagination(t *testing.T) { for _, l := range static.TestUsernamePasswordDomainCredentials(t, conn, wrapper, "username", "password", "domain", credStore.GetPublicId(), prj.PublicId, 5) { hm, err := crypto.HmacSha256(ctx, []byte("password"), databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(err) - allCredentials = append(allCredentials, staticPasswordDomainCredentialToProto(l, prj, hm)) + allCredentials = append(allCredentials, staticUsernamePasswordDomainCredentialToProto(l, prj, hm)) } for _, l := range static.TestUsernamePasswordCredentials(t, conn, wrapper, "username", "password", credStore.GetPublicId(), prj.PublicId, 5) { hm, err := crypto.HmacSha256(ctx, []byte("password"), databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(err) - allCredentials = append(allCredentials, staticPasswordCredentialToProto(l, prj, hm)) + allCredentials = append(allCredentials, staticUsernamePasswordCredentialToProto(l, prj, hm)) } // Reverse slices since response is ordered by created_time descending (newest first) diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index ec3a2c2f75..392bd2e19c 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -275,6 +275,21 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for username password domain credential")) } + case *credstatic.PasswordCredential: + var err error + credType = string(globals.PasswordCredentialType) + credData, err = handlers.ProtoToStruct( + ctx, + &pb.PasswordCredential{ + Password: string(c.GetPassword()), + }, + ) + secret = map[string]any{ + "password": string(c.GetPassword()), + } + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for password credential")) + } case *credstatic.SshPrivateKeyCredential: var err error credType = string(globals.SshPrivateKeyCredentialType) @@ -296,7 +311,6 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for ssh private key credential")) } - case *credstatic.JsonCredential: var err error credType = string(globals.JsonCredentialType) diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 22d619c0a9..1768291046 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -2075,7 +2075,6 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, - globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break @@ -2117,7 +2116,6 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, - globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break @@ -2163,7 +2161,6 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, - globals.PasswordCredentialPrefix, globals.SshPrivateKeyCredentialPrefix, globals.JsonCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index a372d713fa..96ab0ad19b 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -3085,6 +3085,7 @@ func TestAddTargetCredentialSources(t *testing.T) { storeStatic := credstatic.TestCredentialStore(t, conn, wrapper, proj.GetPublicId()) creds := credstatic.TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", storeStatic.GetPublicId(), proj.GetPublicId(), 2) updCreds := credstatic.TestUsernamePasswordDomainCredentials(t, conn, wrapper, "user", "pass", "domain", storeStatic.GetPublicId(), proj.GetPublicId(), 2) + pCreds := credstatic.TestPasswordCredentials(t, conn, wrapper, "pass", storeStatic.GetPublicId(), proj.GetPublicId(), 2) addCases := []struct { name string @@ -3110,6 +3111,12 @@ func TestAddTargetCredentialSources(t *testing.T) { addSources: []string{updCreds[1].GetPublicId()}, resultSourceIds: []string{updCreds[1].GetPublicId()}, }, + { + name: "Add static p cred on empty target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "empty for static_p sources"), + addSources: []string{pCreds[1].GetPublicId()}, + resultSourceIds: []string{pCreds[1].GetPublicId()}, + }, { name: "Add library on library populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for lib-lib sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), @@ -3128,6 +3135,12 @@ func TestAddTargetCredentialSources(t *testing.T) { addSources: []string{cls[1].GetPublicId()}, resultSourceIds: []string{updCreds[0].GetPublicId(), cls[1].GetPublicId()}, }, + { + name: "Add library on static p cred populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for lib-static_p sources", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)})), + addSources: []string{cls[1].GetPublicId()}, + resultSourceIds: []string{pCreds[0].GetPublicId(), cls[1].GetPublicId()}, + }, { name: "Add static cred on library populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for static-lib sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), @@ -3140,6 +3153,12 @@ func TestAddTargetCredentialSources(t *testing.T) { addSources: []string{updCreds[1].GetPublicId()}, resultSourceIds: []string{cls[0].GetPublicId(), updCreds[1].GetPublicId()}, }, + { + name: "Add p static cred on library populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for static_p-lib sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), + addSources: []string{pCreds[1].GetPublicId()}, + resultSourceIds: []string{cls[0].GetPublicId(), pCreds[1].GetPublicId()}, + }, { name: "Add static cred on static cred populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for static-static sources", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)})), @@ -3152,6 +3171,12 @@ func TestAddTargetCredentialSources(t *testing.T) { addSources: []string{updCreds[1].GetPublicId()}, resultSourceIds: []string{creds[0].GetPublicId(), updCreds[1].GetPublicId()}, }, + { + name: "Add static p cred on static cred populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated for static_p-static sources", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)})), + addSources: []string{pCreds[1].GetPublicId()}, + resultSourceIds: []string{creds[0].GetPublicId(), pCreds[1].GetPublicId()}, + }, { name: "Add duplicated sources on library populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "duplicated for lib sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), @@ -3170,6 +3195,12 @@ func TestAddTargetCredentialSources(t *testing.T) { addSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId(), updCreds[1].GetPublicId(), updCreds[1].GetPublicId()}, resultSourceIds: []string{updCreds[0].GetPublicId(), cls[1].GetPublicId(), updCreds[1].GetPublicId()}, }, + { + name: "Add duplicated sources on static p cred populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "duplicated for static p sources", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)})), + addSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId(), pCreds[1].GetPublicId(), pCreds[1].GetPublicId()}, + resultSourceIds: []string{pCreds[0].GetPublicId(), cls[1].GetPublicId(), pCreds[1].GetPublicId()}, + }, } for _, tc := range addCases { @@ -3296,6 +3327,7 @@ func TestSetTargetCredentialSources(t *testing.T) { storeStatic := credstatic.TestCredentialStore(t, conn, wrapper, proj.GetPublicId()) creds := credstatic.TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", storeStatic.GetPublicId(), proj.GetPublicId(), 2) updCreds := credstatic.TestUsernamePasswordDomainCredentials(t, conn, wrapper, "user", "pass", "domain", storeStatic.GetPublicId(), proj.GetPublicId(), 2) + pCreds := credstatic.TestPasswordCredentials(t, conn, wrapper, "pass", storeStatic.GetPublicId(), proj.GetPublicId(), 2) setCases := []struct { name string @@ -3321,6 +3353,12 @@ func TestSetTargetCredentialSources(t *testing.T) { setCredentialSources: []string{updCreds[1].GetPublicId()}, resultCredentialSourceIds: []string{updCreds[1].GetPublicId()}, }, + { + name: "Set static_p on empty target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "empty static_p"), + setCredentialSources: []string{pCreds[1].GetPublicId()}, + resultCredentialSourceIds: []string{pCreds[1].GetPublicId()}, + }, { name: "Set library on library populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated library-library", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), @@ -3339,6 +3377,12 @@ func TestSetTargetCredentialSources(t *testing.T) { setCredentialSources: []string{updCreds[1].GetPublicId()}, resultCredentialSourceIds: []string{updCreds[1].GetPublicId()}, }, + { + name: "Set static_p on library populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated static_p-library", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), + setCredentialSources: []string{pCreds[1].GetPublicId()}, + resultCredentialSourceIds: []string{pCreds[1].GetPublicId()}, + }, { name: "Set library on static populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated library-static", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)})), @@ -3351,6 +3395,12 @@ func TestSetTargetCredentialSources(t *testing.T) { setCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, + { + name: "Set library on static_p populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated library-static_p", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)})), + setCredentialSources: []string{cls[1].GetPublicId()}, + resultCredentialSourceIds: []string{cls[1].GetPublicId()}, + }, { name: "Set static on static populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated static-static", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)})), @@ -3363,12 +3413,24 @@ func TestSetTargetCredentialSources(t *testing.T) { setCredentialSources: []string{updCreds[1].GetPublicId()}, resultCredentialSourceIds: []string{updCreds[1].GetPublicId()}, }, + { + name: "Set static_p on static populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated static_p-static", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)})), + setCredentialSources: []string{pCreds[1].GetPublicId()}, + resultCredentialSourceIds: []string{pCreds[1].GetPublicId()}, + }, { name: "Set static on static_upd populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated static-static_upd", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", updCreds[0].GetPublicId(), credential.BrokeredPurpose)})), setCredentialSources: []string{creds[1].GetPublicId()}, resultCredentialSourceIds: []string{creds[1].GetPublicId()}, }, + { + name: "Set static on static_p populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "populated static-static_p", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)})), + setCredentialSources: []string{creds[1].GetPublicId()}, + resultCredentialSourceIds: []string{creds[1].GetPublicId()}, + }, { name: "Set duplicate library on populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "duplicate library", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))})), @@ -3387,12 +3449,19 @@ func TestSetTargetCredentialSources(t *testing.T) { setCredentialSources: []string{updCreds[1].GetPublicId(), updCreds[1].GetPublicId()}, resultCredentialSourceIds: []string{updCreds[1].GetPublicId()}, }, + { + name: "Set duplicate static_p on populated target", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "duplicate static_p", target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)})), + setCredentialSources: []string{pCreds[1].GetPublicId(), pCreds[1].GetPublicId()}, + resultCredentialSourceIds: []string{pCreds[1].GetPublicId()}, + }, { name: "Set empty on populated target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "another populated", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.BrokeredPurpose, string(cls[0].CredentialType()))}), target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", creds[0].GetPublicId(), credential.BrokeredPurpose)}), target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", updCreds[0].GetPublicId(), credential.BrokeredPurpose)}), + target.WithStaticCredentials([]*target.StaticCredential{target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose)}), ), setCredentialSources: []string{}, resultCredentialSourceIds: nil, @@ -3509,6 +3578,7 @@ func TestRemoveTargetCredentialSources(t *testing.T) { csStatic := credstatic.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] creds := credstatic.TestUsernamePasswordCredentials(t, conn, wrapper, "u", "p", csStatic.GetPublicId(), proj.GetPublicId(), 2) updCreds := credstatic.TestUsernamePasswordDomainCredentials(t, conn, wrapper, "user", "pass", "domain", csStatic.GetPublicId(), proj.GetPublicId(), 2) + pCreds := credstatic.TestPasswordCredentials(t, conn, wrapper, "pass", csStatic.GetPublicId(), proj.GetPublicId(), 2) removeCases := []struct { name string @@ -3535,6 +3605,12 @@ func TestRemoveTargetCredentialSources(t *testing.T) { removeCredentialSources: []string{updCreds[1].GetPublicId()}, wantErr: true, }, + { + name: "Remove static_p from empty", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "empty static_p"), + removeCredentialSources: []string{pCreds[1].GetPublicId()}, + wantErr: true, + }, { name: "Remove 1 of 2 libraries", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "remove partial lib", @@ -3565,6 +3641,16 @@ func TestRemoveTargetCredentialSources(t *testing.T) { removeCredentialSources: []string{updCreds[1].GetPublicId()}, resultCredentialSourceIds: []string{updCreds[0].GetPublicId()}, }, + { + name: "Remove 1 of 2 static_p credentials", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "remove partial static_p", + target.WithStaticCredentials([]*target.StaticCredential{ + target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[1].GetPublicId(), credential.BrokeredPurpose), + })), + removeCredentialSources: []string{pCreds[1].GetPublicId()}, + resultCredentialSourceIds: []string{pCreds[0].GetPublicId()}, + }, { name: "Remove 1 duplicate set of 2 libraries", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "remove duplicate lib", @@ -3601,6 +3687,19 @@ func TestRemoveTargetCredentialSources(t *testing.T) { }, resultCredentialSourceIds: []string{updCreds[0].GetPublicId()}, }, + { + name: "Remove 1 duplicate set of 2 static_p credentials", + tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "remove duplicate static_p", + target.WithStaticCredentials([]*target.StaticCredential{ + target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[1].GetPublicId(), credential.BrokeredPurpose), + })), + removeCredentialSources: []string{ + pCreds[1].GetPublicId(), pCreds[1].GetPublicId(), + }, + resultCredentialSourceIds: []string{pCreds[0].GetPublicId()}, + }, + { name: "Remove mixed sources from target", tar: tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "remove mixed", @@ -3613,12 +3712,14 @@ func TestRemoveTargetCredentialSources(t *testing.T) { target.TestNewStaticCredential("", creds[1].GetPublicId(), credential.BrokeredPurpose), target.TestNewStaticCredential("", updCreds[0].GetPublicId(), credential.BrokeredPurpose), target.TestNewStaticCredential("", updCreds[1].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[1].GetPublicId(), credential.BrokeredPurpose), })), removeCredentialSources: []string{ - cls[1].GetPublicId(), creds[0].GetPublicId(), updCreds[0].GetPublicId(), + cls[1].GetPublicId(), creds[0].GetPublicId(), updCreds[0].GetPublicId(), pCreds[0].GetPublicId(), }, resultCredentialSourceIds: []string{ - cls[0].GetPublicId(), creds[1].GetPublicId(), updCreds[1].GetPublicId(), + cls[0].GetPublicId(), creds[1].GetPublicId(), updCreds[1].GetPublicId(), pCreds[1].GetPublicId(), }, }, { @@ -3633,11 +3734,14 @@ func TestRemoveTargetCredentialSources(t *testing.T) { target.TestNewStaticCredential("", creds[1].GetPublicId(), credential.BrokeredPurpose), target.TestNewStaticCredential("", updCreds[0].GetPublicId(), credential.BrokeredPurpose), target.TestNewStaticCredential("", updCreds[1].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[0].GetPublicId(), credential.BrokeredPurpose), + target.TestNewStaticCredential("", pCreds[1].GetPublicId(), credential.BrokeredPurpose), })), removeCredentialSources: []string{ cls[0].GetPublicId(), cls[1].GetPublicId(), creds[0].GetPublicId(), creds[1].GetPublicId(), updCreds[0].GetPublicId(), updCreds[1].GetPublicId(), + pCreds[0].GetPublicId(), pCreds[1].GetPublicId(), }, resultCredentialSourceIds: []string{}, }, @@ -4283,6 +4387,20 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { require.NoError(t, err) require.NotNil(t, updCredResp) + pCredResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{ + CredentialStoreId: staticStore.GetPublicId(), + Type: credential.PasswordSubtype.String(), + Name: wrapperspb.String("P Cred Name"), + Description: wrapperspb.String("P Cred Description"), + Attrs: &credpb.Credential_PasswordAttributes{ + PasswordAttributes: &credpb.PasswordAttributes{ + Password: wrapperspb.String("static-password"), + }, + }, + }}) + require.NoError(t, err) + require.NotNil(t, pCredResp) + sshPkCredResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{ CredentialStoreId: staticStore.GetPublicId(), Type: credential.SshPrivateKeySubtype.String(), @@ -4588,6 +4706,32 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { }, wantedConnectionLimit: 1000, }, + { + name: "static-Password", + hostSourceId: shs.GetPublicId(), + credSourceId: pCredResp.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + wantedCred: &pb.SessionCredential{ + CredentialSource: &pb.CredentialSource{ + Id: pCredResp.GetItem().GetId(), + Name: pCredResp.GetItem().GetName().GetValue(), + Description: pCredResp.GetItem().GetDescription().GetValue(), + CredentialStoreId: staticStore.GetPublicId(), + Type: credstatic.Subtype.String(), + CredentialType: string(globals.PasswordCredentialType), + }, + Credential: func() *structpb.Struct { + data := map[string]any{ + "password": "static-password", + } + st, err := structpb.NewStruct(data) + require.NoError(t, err) + return st + }(), + }, + wantedConnectionLimit: 1000, + }, { name: "static-ssh-private-key", hostSourceId: shs.GetPublicId(), diff --git a/internal/proto/controller/api/resources/credentials/v1/credential.proto b/internal/proto/controller/api/resources/credentials/v1/credential.proto index 0243c7e28a..1111386075 100644 --- a/internal/proto/controller/api/resources/credentials/v1/credential.proto +++ b/internal/proto/controller/api/resources/credentials/v1/credential.proto @@ -82,6 +82,11 @@ message Credential { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "username_password_domain" ]; + PasswordAttributes password_attributes = 105 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "password" + ]; } // Output only. The available actions on this resource for this user. diff --git a/sdk/pbs/controller/api/resources/credentials/credential.pb.go b/sdk/pbs/controller/api/resources/credentials/credential.pb.go index aa382ab11e..84f57abfba 100644 --- a/sdk/pbs/controller/api/resources/credentials/credential.pb.go +++ b/sdk/pbs/controller/api/resources/credentials/credential.pb.go @@ -59,6 +59,7 @@ type Credential struct { // *Credential_SshPrivateKeyAttributes // *Credential_JsonAttributes // *Credential_UsernamePasswordDomainAttributes + // *Credential_PasswordAttributes Attrs isCredential_Attrs `protobuf_oneof:"attrs"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"` @@ -211,6 +212,15 @@ func (x *Credential) GetUsernamePasswordDomainAttributes() *UsernamePasswordDoma return nil } +func (x *Credential) GetPasswordAttributes() *PasswordAttributes { + if x != nil { + if x, ok := x.Attrs.(*Credential_PasswordAttributes); ok { + return x.PasswordAttributes + } + } + return nil +} + func (x *Credential) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -243,6 +253,10 @@ type Credential_UsernamePasswordDomainAttributes struct { UsernamePasswordDomainAttributes *UsernamePasswordDomainAttributes `protobuf:"bytes,104,opt,name=username_password_domain_attributes,json=usernamePasswordDomainAttributes,proto3,oneof"` } +type Credential_PasswordAttributes struct { + PasswordAttributes *PasswordAttributes `protobuf:"bytes,105,opt,name=password_attributes,json=passwordAttributes,proto3,oneof"` +} + func (*Credential_Attributes) isCredential_Attrs() {} func (*Credential_UsernamePasswordAttributes) isCredential_Attrs() {} @@ -253,6 +267,8 @@ func (*Credential_JsonAttributes) isCredential_Attrs() {} func (*Credential_UsernamePasswordDomainAttributes) isCredential_Attrs() {} +func (*Credential_PasswordAttributes) isCredential_Attrs() {} + // The attributes of a UsernamePassword Credential. type UsernamePasswordAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -584,8 +600,7 @@ var File_controller_api_resources_credentials_v1_credential_proto protoreflect.F const file_controller_api_resources_credentials_v1_credential_proto_rawDesc = "" + "\n" + - "8controller/api/resources/credentials/v1/credential.proto\x12'controller.api.resources.credentials.v1\x1a.controller/api/resources/scopes/v1/scope.proto\x1a*controller/custom_options/v1/options.proto\x1a\x1bgoogle/api/visibility.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xa5\n" + - "\n" + + "8controller/api/resources/credentials/v1/credential.proto\x12'controller.api.resources.credentials.v1\x1a.controller/api/resources/scopes/v1/scope.proto\x1a*controller/custom_options/v1/options.proto\x1a\x1bgoogle/api/visibility.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb8\v\n" + "\n" + "Credential\x12\x0e\n" + "\x02id\x18\n" + @@ -610,7 +625,9 @@ const file_controller_api_resources_credentials_v1_credential_proto_rawDesc = "" "\x0fjson_attributes\x18g \x01(\v27.controller.api.resources.credentials.v1.JsonAttributesB\x1c\xa0\xda)\x01\x9a\xe3)\x04json\xfa\xd2\xe4\x93\x02\n" + "\x12\bINTERNALH\x00R\x0ejsonAttributes\x12\xcc\x01\n" + "#username_password_domain_attributes\x18h \x01(\v2I.controller.api.resources.credentials.v1.UsernamePasswordDomainAttributesB0\xa0\xda)\x01\x9a\xe3)\x18username_password_domain\xfa\xd2\xe4\x93\x02\n" + - "\x12\bINTERNALH\x00R usernamePasswordDomainAttributes\x12/\n" + + "\x12\bINTERNALH\x00R usernamePasswordDomainAttributes\x12\x90\x01\n" + + "\x13password_attributes\x18i \x01(\v2;.controller.api.resources.credentials.v1.PasswordAttributesB \xa0\xda)\x01\x9a\xe3)\bpassword\xfa\xd2\xe4\x93\x02\n" + + "\x12\bINTERNALH\x00R\x12passwordAttributes\x12/\n" + "\x12authorized_actions\x18\xac\x02 \x03(\tR\x12authorized_actionsB\a\n" + "\x05attrs\"\xb6\x02\n" + "\x1aUsernamePasswordAttributes\x12a\n" + @@ -694,21 +711,22 @@ var file_controller_api_resources_credentials_v1_credential_proto_depIdxs = []in 4, // 7: controller.api.resources.credentials.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.credentials.v1.SshPrivateKeyAttributes 5, // 8: controller.api.resources.credentials.v1.Credential.json_attributes:type_name -> controller.api.resources.credentials.v1.JsonAttributes 2, // 9: controller.api.resources.credentials.v1.Credential.username_password_domain_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes - 7, // 10: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue - 7, // 11: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue - 7, // 12: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.username:type_name -> google.protobuf.StringValue - 7, // 13: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.password:type_name -> google.protobuf.StringValue - 7, // 14: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.domain:type_name -> google.protobuf.StringValue - 7, // 15: controller.api.resources.credentials.v1.PasswordAttributes.password:type_name -> google.protobuf.StringValue - 7, // 16: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue - 7, // 17: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue - 7, // 18: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue - 9, // 19: controller.api.resources.credentials.v1.JsonAttributes.object:type_name -> google.protobuf.Struct - 20, // [20:20] is the sub-list for method output_type - 20, // [20:20] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 3, // 10: controller.api.resources.credentials.v1.Credential.password_attributes:type_name -> controller.api.resources.credentials.v1.PasswordAttributes + 7, // 11: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue + 7, // 12: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue + 7, // 13: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.username:type_name -> google.protobuf.StringValue + 7, // 14: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.password:type_name -> google.protobuf.StringValue + 7, // 15: controller.api.resources.credentials.v1.UsernamePasswordDomainAttributes.domain:type_name -> google.protobuf.StringValue + 7, // 16: controller.api.resources.credentials.v1.PasswordAttributes.password:type_name -> google.protobuf.StringValue + 7, // 17: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue + 7, // 18: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue + 7, // 19: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue + 9, // 20: controller.api.resources.credentials.v1.JsonAttributes.object:type_name -> google.protobuf.Struct + 21, // [21:21] is the sub-list for method output_type + 21, // [21:21] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_controller_api_resources_credentials_v1_credential_proto_init() } @@ -722,6 +740,7 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() { (*Credential_SshPrivateKeyAttributes)(nil), (*Credential_JsonAttributes)(nil), (*Credential_UsernamePasswordDomainAttributes)(nil), + (*Credential_PasswordAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ From d4f7bd55d14d8e9f2733896d4547dee1cd2bd223 Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:00:09 -0500 Subject: [PATCH 09/11] feat(cli): Add password credential CLI commands (#6117) --- .../password_credential_attributes.gen.go | 40 +++ internal/api/genapi/input.go | 25 ++ internal/cmd/commands.go | 12 + .../password_credentials.gen.go | 278 ++++++++++++++++++ .../commands/credentialscmd/password_funcs.go | 96 ++++++ internal/cmd/gencli/input.go | 16 + .../credentials/credential_service.go | 7 - .../v1/session_recording.proto | 10 + internal/tests/cli/boundary/_credentials.bash | 13 + internal/tests/cli/boundary/credentials.bats | 40 +++ .../session_recording.pb.go | 223 +++++++++----- 11 files changed, 676 insertions(+), 84 deletions(-) create mode 100644 api/sessionrecordings/password_credential_attributes.gen.go create mode 100644 internal/cmd/commands/credentialscmd/password_credentials.gen.go create mode 100644 internal/cmd/commands/credentialscmd/password_funcs.go diff --git a/api/sessionrecordings/password_credential_attributes.gen.go b/api/sessionrecordings/password_credential_attributes.gen.go new file mode 100644 index 0000000000..32a0834db7 --- /dev/null +++ b/api/sessionrecordings/password_credential_attributes.gen.go @@ -0,0 +1,40 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package sessionrecordings + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type PasswordCredentialAttributes struct { + PasswordHmac string `json:"password_hmac,omitempty"` +} + +func AttributesMapToPasswordCredentialAttributes(in map[string]any) (*PasswordCredentialAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out PasswordCredentialAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *Credential) GetPasswordCredentialAttributes() (*PasswordCredentialAttributes, error) { + if pt.Type != "password" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential is of type %s", "password", pt.Type) + } + return AttributesMapToPasswordCredentialAttributes(pt.Attributes) +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 08d374f2a2..ec639c8613 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -692,6 +692,22 @@ var inputStructs = []*structInfo{ versionEnabled: true, createResponseTypes: []string{CreateResponseType, ReadResponseType, UpdateResponseType, DeleteResponseType, ListResponseType}, }, + { + inProto: &credentials.PasswordAttributes{}, + outFile: "credentials/password_attributes.gen.go", + subtypeName: "PasswordCredential", + subtype: "password", + fieldOverrides: []fieldInfo{ + { + Name: "Password", + SkipDefault: true, + }, + }, + parentTypeName: "Credential", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentials.UsernamePasswordAttributes{}, outFile: "credentials/username_password_attributes.gen.go", @@ -1294,6 +1310,15 @@ var inputStructs = []*structInfo{ inProto: &session_recordings.Credential{}, outFile: "sessionrecordings/credential.gen.go", }, + { + inProto: &session_recordings.PasswordCredentialAttributes{}, + outFile: "sessionrecordings/password_credential_attributes.gen.go", + subtype: "password", + parentTypeName: "Credential", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &session_recordings.UsernamePasswordCredentialAttributes{}, outFile: "sessionrecordings/username_password_credential_attributes.gen.go", diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index bbd7cc37c0..66fda0f1c8 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -621,6 +621,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", } }), + "credentials create password": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentialscmd.PasswordCommand{ + Command: base.NewCommand(ui, opts...), + Func: "create", + } + }), "credentials create username-password": wrapper.Wrap(func() wrapper.WrappableCommand { return &credentialscmd.UsernamePasswordCommand{ Command: base.NewCommand(ui, opts...), @@ -651,6 +657,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", } }), + "credentials update password": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentialscmd.PasswordCommand{ + Command: base.NewCommand(ui, opts...), + Func: "update", + } + }), "credentials update username-password": wrapper.Wrap(func() wrapper.WrappableCommand { return &credentialscmd.UsernamePasswordCommand{ Command: base.NewCommand(ui, opts...), diff --git a/internal/cmd/commands/credentialscmd/password_credentials.gen.go b/internal/cmd/commands/credentialscmd/password_credentials.gen.go new file mode 100644 index 0000000000..97b912e304 --- /dev/null +++ b/internal/cmd/commands/credentialscmd/password_credentials.gen.go @@ -0,0 +1,278 @@ +// Code generated by "make cli"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentialscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initPasswordFlags() { + flagsOnce.Do(func() { + extraFlags := extraPasswordActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsPasswordMap[k] = append(flagsPasswordMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*PasswordCommand)(nil) + _ cli.CommandAutocomplete = (*PasswordCommand)(nil) +) + +type PasswordCommand struct { + *base.Command + + Func string + + plural string + + extraPasswordCmdVars +} + +func (c *PasswordCommand) AutocompleteArgs() complete.Predictor { + initPasswordFlags() + return complete.PredictAnything +} + +func (c *PasswordCommand) AutocompleteFlags() complete.Flags { + initPasswordFlags() + return c.Flags().Completions() +} + +func (c *PasswordCommand) Synopsis() string { + if extra := extraPasswordSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential" + + synopsisStr = fmt.Sprintf("%s %s", "password-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *PasswordCommand) Help() string { + initPasswordFlags() + + var helpStr string + helpMap := common.HelpMap("credential") + + switch c.Func { + + default: + + helpStr = c.extraPasswordHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsPasswordMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *PasswordCommand) Flags() *base.FlagSets { + if len(flagsPasswordMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "password-type credential", flagsPasswordMap, c.Func) + + extraPasswordFlagsFunc(c, set, f) + + return set +} + +func (c *PasswordCommand) Run(args []string) int { + initPasswordFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "password-type credential" + switch c.Func { + case "list": + c.plural = "password-type credentials" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsPasswordMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentials.Option + + if strutil.StrListContains(flagsPasswordMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentialsClient := credentials.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentials.DefaultName()) + default: + opts = append(opts, credentials.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentials.DefaultDescription()) + default: + opts = append(opts, credentials.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentials.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentials.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraPasswordFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentials.Credential + + var createResult *credentials.CredentialCreateResult + + var updateResult *credentials.CredentialUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentialsClient.Create(c.Context, "password", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentialsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraPasswordActions(c, resp, item, err, credentialsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomPasswordActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *PasswordCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraPasswordActionsFlagsMapFunc = func() map[string][]string { return nil } + extraPasswordSynopsisFunc = func(*PasswordCommand) string { return "" } + extraPasswordFlagsFunc = func(*PasswordCommand, *base.FlagSets, *base.FlagSet) {} + extraPasswordFlagsHandlingFunc = func(*PasswordCommand, *base.FlagSets, *[]credentials.Option) bool { return true } + executeExtraPasswordActions = func(_ *PasswordCommand, inResp *api.Response, inItem *credentials.Credential, inErr error, _ *credentials.Client, _ uint32, _ []credentials.Option) (*api.Response, *credentials.Credential, error) { + return inResp, inItem, inErr + } + printCustomPasswordActionOutput = func(*PasswordCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentialscmd/password_funcs.go b/internal/cmd/commands/credentialscmd/password_funcs.go new file mode 100644 index 0000000000..4697ee31a9 --- /dev/null +++ b/internal/cmd/commands/credentialscmd/password_funcs.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentialscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" +) + +func init() { + extraPasswordFlagsFunc = extraPasswordFlagsFuncImpl + extraPasswordActionsFlagsMapFunc = extraPasswordActionsFlagsMapFuncImpl + extraPasswordFlagsHandlingFunc = extraPasswordFlagHandlingFuncImpl +} + +type extraPasswordCmdVars struct { + flagPassword string +} + +func extraPasswordActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + passwordFlagName, + }, + } + flags["update"] = flags["create"] + return flags +} + +func extraPasswordFlagsFuncImpl(c *PasswordCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Password Credential Options") + + for _, name := range flagsPasswordMap[c.Func] { + switch name { + case passwordFlagName: + f.StringVar(&base.StringVar{ + Name: passwordFlagName, + Target: &c.flagPassword, + Usage: "The password associated with the credential. This can be a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", + }) + } + } +} + +func extraPasswordFlagHandlingFuncImpl(c *PasswordCommand, _ *base.FlagSets, opts *[]credentials.Option) bool { + switch c.flagPassword { + case "": + default: + password, err := parseutil.MustParsePath(c.flagPassword) + switch { + case err == nil: + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Password flag must be used with env:// or file:// syntax") + return false + default: + c.UI.Error(fmt.Sprintf("Error parsing password flag: %v", err)) + return false + } + *opts = append(*opts, credentials.WithPasswordCredentialPassword(password)) + } + + return true +} + +func (c *PasswordCommand) extraPasswordHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials create password -credential-store-id [options] [args]", + "", + " Create a password credential. Example:", + "", + ` $ boundary credentials create password -credential-store-id csvlt_1234567890 -password pass`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials update password [options] [args]", + "", + " Update a password credential given its ID. Example:", + "", + ` $ boundary credentials update password -id clvlt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index b5c1b68a0f..94ad8f1687 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -383,6 +383,22 @@ var inputStructs = map[string][]*cmdInfo{ Container: "CredentialStore", HasId: true, }, + { + ResourceType: resource.Credential.String(), + Pkg: "credentials", + StdActions: []string{"create", "update"}, + SubActionPrefix: "password", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, { ResourceType: resource.Credential.String(), Pkg: "credentials", diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index cffe7e0d41..d8e6e07cfd 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -599,7 +599,6 @@ func (s Service) updateInRepo( return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) } return out, nil - case credential.SshPrivateKeySubtype: dbMasks = append(dbMasks, spkMaskManager.Translate(masks)...) if len(dbMasks) == 0 { @@ -633,7 +632,6 @@ func (s Service) updateInRepo( return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) } return out, nil - case credential.JsonSubtype: dbMasks = append(dbMasks, jsonMaskManager.Translate(masks, "attributes", "object")...) if len(dbMasks) == 0 { @@ -657,7 +655,6 @@ func (s Service) updateInRepo( return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) } return out, nil - default: return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, fmt.Sprintf("Unsupported credential type %q", item.GetType())) @@ -1062,7 +1059,6 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error { } } } - case credential.JsonSubtype.String(): object := req.GetItem().GetJsonAttributes().GetObject() if object == nil || len(object.AsMap()) <= 0 { @@ -1070,7 +1066,6 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error { } else if _, err := json.Marshal(object); err != nil { badFields[objectField] = "Unable to parse given json value" } - default: badFields[globals.TypeField] = fmt.Sprintf("Unsupported credential type %q", req.Item.GetType()) } @@ -1139,7 +1134,6 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { } } } - case credential.JsonSubtype: if handlers.MaskContainsPrefix(req.GetUpdateMask().GetPaths(), objectField) { object := req.GetItem().GetJsonAttributes().GetObject() @@ -1149,7 +1143,6 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { badFields[objectField] = "Unable to parse given json value" } } - default: badFields[globals.IdField] = "Unknown credential type." } diff --git a/internal/proto/controller/api/resources/sessionrecordings/v1/session_recording.proto b/internal/proto/controller/api/resources/sessionrecordings/v1/session_recording.proto index 90866e07d6..0e8f88201a 100644 --- a/internal/proto/controller/api/resources/sessionrecordings/v1/session_recording.proto +++ b/internal/proto/controller/api/resources/sessionrecordings/v1/session_recording.proto @@ -308,6 +308,10 @@ message Credential { (google.api.field_visibility).restriction = "INTERNAL", (custom_options.v1.subtype) = "json" ]; + PasswordCredentialAttributes password_attributes = 11 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.subtype) = "password" + ]; } } @@ -338,6 +342,12 @@ message JsonCredentialAttributes { string object_hmac = 1; // @gotags: class:"public" } +// The attributes of a Password Credential. +message PasswordCredentialAttributes { + // The hmac value of the password. + string password_hmac = 1; // @gotags: class:"public" +} + // CredentialLibrary contains all fields related to an Credential Library resource message CredentialLibrary { // The ID of the Credential Library. diff --git a/internal/tests/cli/boundary/_credentials.bash b/internal/tests/cli/boundary/_credentials.bash index d29b6910aa..23148168e8 100644 --- a/internal/tests/cli/boundary/_credentials.bash +++ b/internal/tests/cli/boundary/_credentials.bash @@ -63,6 +63,19 @@ function create_json_credential() { $args } +function create_password_credential() { + local name=$1 + local sid=$2 + local pass=$3 + + export BP="${pass}" + boundary credentials create password \ + -name $name \ + -description 'test password credential' \ + -credential-store-id $sid \ + -password env://BP +} + function read_credential() { boundary credentials read -id $1 -format json } diff --git a/internal/tests/cli/boundary/credentials.bats b/internal/tests/cli/boundary/credentials.bats index 5d7f3ffbb2..075564f2f7 100644 --- a/internal/tests/cli/boundary/credentials.bats +++ b/internal/tests/cli/boundary/credentials.bats @@ -12,6 +12,7 @@ export NEW_UPD_AT_CREDENTIAL='test-at-user-domain-pass' export NEW_UPD_SLASH_CREDENTIAL='test-slash-domain-user-pass' export NEW_UPD_CREDENTIAL_DOMAIN='test-domain-user-plus-domain' export NEW_JSON_CREDENTIAL='test-json' +export NEW_PASSWORD_CREDENTIAL='test-pass' @test "boundary/login: can login as default user" { run login $DEFAULT_LOGIN @@ -68,6 +69,20 @@ export NEW_JSON_CREDENTIAL='test-json' } +@test "boundary/credentials: can create $NEW_PASSWORD_CREDENTIAL credential in $NEW_STORE store" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_password_credential $NEW_PASSWORD_CREDENTIAL $csid 'password' + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/credentials: can not create already created $NEW_PASSWORD_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_password_credential $NEW_PASSWORD_CREDENTIAL $csid 'password' + echo "$output" + [ "$status" -eq 1 ] +} + @test "boundary/credentials: can create $NEW_JSON_CREDENTIAL credential in $NEW_STORE store" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) run create_json_credential $NEW_JSON_CREDENTIAL $csid '-string-kv username=admin -string-kv password=pass' @@ -142,6 +157,14 @@ export NEW_JSON_CREDENTIAL='test-json' [ "$status" -eq 0 ] } +@test "boundary/credentials: can read $NEW_PASSWORD_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_PASSWORD_CREDENTIAL $csid) + run read_credential $cid + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/credentials: can read $NEW_JSON_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) @@ -186,6 +209,15 @@ export NEW_JSON_CREDENTIAL='test-json' [ "$status" -eq 0 ] } +@test "boundary/credentials: can delete $NEW_PASSWORD_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_PASSWORD_CREDENTIAL $csid) + run delete_credential $cid + echo "$output" + run has_status_code "$output" "204" + [ "$status" -eq 0 ] +} + @test "boundary/credentials: can delete $NEW_JSON_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) @@ -255,6 +287,14 @@ export NEW_JSON_CREDENTIAL='test-json' [ "$status" -eq 1 ] } +@test "boundary/credential-stores: can not read deleted $NEW_PASSWORD_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_PASSWORD_CREDENTIAL $csid) + run read_credential $cid + echo "$output" + [ "$status" -eq 1 ] +} + @test "boundary/credential-stores: can delete $NEW_STORE static store" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) run delete_credential_store $csid diff --git a/sdk/pbs/controller/api/resources/session_recordings/session_recording.pb.go b/sdk/pbs/controller/api/resources/session_recordings/session_recording.pb.go index 0c1448e884..ad06de68a8 100644 --- a/sdk/pbs/controller/api/resources/session_recordings/session_recording.pb.go +++ b/sdk/pbs/controller/api/resources/session_recordings/session_recording.pb.go @@ -1132,6 +1132,7 @@ type Credential struct { // *Credential_UsernamePasswordAttributes // *Credential_SshPrivateKeyAttributes // *Credential_JsonAttributes + // *Credential_PasswordAttributes Attrs isCredential_Attrs `protobuf_oneof:"attrs"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1252,6 +1253,15 @@ func (x *Credential) GetJsonAttributes() *JsonCredentialAttributes { return nil } +func (x *Credential) GetPasswordAttributes() *PasswordCredentialAttributes { + if x != nil { + if x, ok := x.Attrs.(*Credential_PasswordAttributes); ok { + return x.PasswordAttributes + } + } + return nil +} + type isCredential_Attrs interface { isCredential_Attrs() } @@ -1273,6 +1283,10 @@ type Credential_JsonAttributes struct { JsonAttributes *JsonCredentialAttributes `protobuf:"bytes,10,opt,name=json_attributes,json=jsonAttributes,proto3,oneof"` } +type Credential_PasswordAttributes struct { + PasswordAttributes *PasswordCredentialAttributes `protobuf:"bytes,11,opt,name=password_attributes,json=passwordAttributes,proto3,oneof"` +} + func (*Credential_Attributes) isCredential_Attrs() {} func (*Credential_UsernamePasswordAttributes) isCredential_Attrs() {} @@ -1281,6 +1295,8 @@ func (*Credential_SshPrivateKeyAttributes) isCredential_Attrs() {} func (*Credential_JsonAttributes) isCredential_Attrs() {} +func (*Credential_PasswordAttributes) isCredential_Attrs() {} + // The attributes of a UsernamePassword Credential. type UsernamePasswordCredentialAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1446,6 +1462,52 @@ func (x *JsonCredentialAttributes) GetObjectHmac() string { return "" } +// The attributes of a Password Credential. +type PasswordCredentialAttributes struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The hmac value of the password. + PasswordHmac string `protobuf:"bytes,1,opt,name=password_hmac,json=passwordHmac,proto3" json:"password_hmac,omitempty" class:"public"` // @gotags: class:"public" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PasswordCredentialAttributes) Reset() { + *x = PasswordCredentialAttributes{} + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PasswordCredentialAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordCredentialAttributes) ProtoMessage() {} + +func (x *PasswordCredentialAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordCredentialAttributes.ProtoReflect.Descriptor instead. +func (*PasswordCredentialAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{14} +} + +func (x *PasswordCredentialAttributes) GetPasswordHmac() string { + if x != nil { + return x.PasswordHmac + } + return "" +} + // CredentialLibrary contains all fields related to an Credential Library resource type CredentialLibrary struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1474,7 +1536,7 @@ type CredentialLibrary struct { func (x *CredentialLibrary) Reset() { *x = CredentialLibrary{} - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[14] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1486,7 +1548,7 @@ func (x *CredentialLibrary) String() string { func (*CredentialLibrary) ProtoMessage() {} func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[14] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1499,7 +1561,7 @@ func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialLibrary.ProtoReflect.Descriptor instead. func (*CredentialLibrary) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{14} + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{15} } func (x *CredentialLibrary) GetId() string { @@ -1632,7 +1694,7 @@ type VaultCredentialLibraryAttributes struct { func (x *VaultCredentialLibraryAttributes) Reset() { *x = VaultCredentialLibraryAttributes{} - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[15] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1644,7 +1706,7 @@ func (x *VaultCredentialLibraryAttributes) String() string { func (*VaultCredentialLibraryAttributes) ProtoMessage() {} func (x *VaultCredentialLibraryAttributes) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[15] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1657,7 +1719,7 @@ func (x *VaultCredentialLibraryAttributes) ProtoReflect() protoreflect.Message { // Deprecated: Use VaultCredentialLibraryAttributes.ProtoReflect.Descriptor instead. func (*VaultCredentialLibraryAttributes) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{15} + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{16} } func (x *VaultCredentialLibraryAttributes) GetPath() string { @@ -1706,7 +1768,7 @@ type VaultSSHCertificateCredentialLibraryAttributes struct { func (x *VaultSSHCertificateCredentialLibraryAttributes) Reset() { *x = VaultSSHCertificateCredentialLibraryAttributes{} - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[16] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1718,7 +1780,7 @@ func (x *VaultSSHCertificateCredentialLibraryAttributes) String() string { func (*VaultSSHCertificateCredentialLibraryAttributes) ProtoMessage() {} func (x *VaultSSHCertificateCredentialLibraryAttributes) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[16] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1731,7 +1793,7 @@ func (x *VaultSSHCertificateCredentialLibraryAttributes) ProtoReflect() protoref // Deprecated: Use VaultSSHCertificateCredentialLibraryAttributes.ProtoReflect.Descriptor instead. func (*VaultSSHCertificateCredentialLibraryAttributes) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{16} + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{17} } func (x *VaultSSHCertificateCredentialLibraryAttributes) GetPath() string { @@ -1810,7 +1872,7 @@ type ValuesAtTime struct { func (x *ValuesAtTime) Reset() { *x = ValuesAtTime{} - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[17] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1822,7 +1884,7 @@ func (x *ValuesAtTime) String() string { func (*ValuesAtTime) ProtoMessage() {} func (x *ValuesAtTime) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[17] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1835,7 +1897,7 @@ func (x *ValuesAtTime) ProtoReflect() protoreflect.Message { // Deprecated: Use ValuesAtTime.ProtoReflect.Descriptor instead. func (*ValuesAtTime) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{17} + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{18} } func (x *ValuesAtTime) GetUser() *User { @@ -1936,7 +1998,7 @@ type SessionRecording struct { func (x *SessionRecording) Reset() { *x = SessionRecording{} - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[18] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1948,7 +2010,7 @@ func (x *SessionRecording) String() string { func (*SessionRecording) ProtoMessage() {} func (x *SessionRecording) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[18] + mi := &file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1961,7 +2023,7 @@ func (x *SessionRecording) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRecording.ProtoReflect.Descriptor instead. func (*SessionRecording) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{18} + return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescGZIP(), []int{19} } func (x *SessionRecording) GetId() string { @@ -2220,7 +2282,7 @@ const file_controller_api_resources_sessionrecordings_v1_session_recording_proto "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12(\n" + "\x0ftls_server_name\x18\x03 \x01(\tR\x0ftls_server_name\x12(\n" + "\x0ftls_skip_verify\x18\x04 \x01(\bR\x0ftls_skip_verify\x12$\n" + - "\rworker_filter\x18\x05 \x01(\tR\rworker_filter\"\xc6\x06\n" + + "\rworker_filter\x18\x05 \x01(\tR\rworker_filter\"\xe5\a\n" + "\n" + "Credential\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12j\n" + @@ -2238,7 +2300,9 @@ const file_controller_api_resources_sessionrecordings_v1_session_recording_proto "\x12\bINTERNALH\x00R\x17sshPrivateKeyAttributes\x12\x8c\x01\n" + "\x0fjson_attributes\x18\n" + " \x01(\v2G.controller.api.resources.sessionrecordings.v1.JsonCredentialAttributesB\x18\x9a\xe3)\x04json\xfa\xd2\xe4\x93\x02\n" + - "\x12\bINTERNALH\x00R\x0ejsonAttributesB\a\n" + + "\x12\bINTERNALH\x00R\x0ejsonAttributes\x12\x9c\x01\n" + + "\x13password_attributes\x18\v \x01(\v2K.controller.api.resources.sessionrecordings.v1.PasswordCredentialAttributesB\x1c\x9a\xe3)\bpassword\xfa\xd2\xe4\x93\x02\n" + + "\x12\bINTERNALH\x00R\x12passwordAttributesB\a\n" + "\x05attrs\"g\n" + "$UsernamePasswordCredentialAttributes\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12#\n" + @@ -2249,7 +2313,9 @@ const file_controller_api_resources_sessionrecordings_v1_session_recording_proto "\x1bprivate_key_passphrase_hmac\x18\x03 \x01(\tR\x18privateKeyPassphraseHmac\";\n" + "\x18JsonCredentialAttributes\x12\x1f\n" + "\vobject_hmac\x18\x01 \x01(\tR\n" + - "objectHmac\"\xd3\a\n" + + "objectHmac\"C\n" + + "\x1cPasswordCredentialAttributes\x12#\n" + + "\rpassword_hmac\x18\x01 \x01(\tR\fpasswordHmac\"\xd3\a\n" + "\x11CredentialLibrary\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12j\n" + "\x10credential_store\x18\x02 \x01(\v2>.controller.api.resources.sessionrecordings.v1.CredentialStoreR\x10credential_store\x12\x12\n" + @@ -2340,7 +2406,7 @@ func file_controller_api_resources_sessionrecordings_v1_session_recording_proto_ return file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDescData } -var file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_controller_api_resources_sessionrecordings_v1_session_recording_proto_goTypes = []any{ (*ChannelRecording)(nil), // 0: controller.api.resources.sessionrecordings.v1.ChannelRecording (*ConnectionRecording)(nil), // 1: controller.api.resources.sessionrecordings.v1.ConnectionRecording @@ -2356,73 +2422,75 @@ var file_controller_api_resources_sessionrecordings_v1_session_recording_proto_g (*UsernamePasswordCredentialAttributes)(nil), // 11: controller.api.resources.sessionrecordings.v1.UsernamePasswordCredentialAttributes (*SshPrivateKeyCredentialAttributes)(nil), // 12: controller.api.resources.sessionrecordings.v1.SshPrivateKeyCredentialAttributes (*JsonCredentialAttributes)(nil), // 13: controller.api.resources.sessionrecordings.v1.JsonCredentialAttributes - (*CredentialLibrary)(nil), // 14: controller.api.resources.sessionrecordings.v1.CredentialLibrary - (*VaultCredentialLibraryAttributes)(nil), // 15: controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes - (*VaultSSHCertificateCredentialLibraryAttributes)(nil), // 16: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes - (*ValuesAtTime)(nil), // 17: controller.api.resources.sessionrecordings.v1.ValuesAtTime - (*SessionRecording)(nil), // 18: controller.api.resources.sessionrecordings.v1.SessionRecording - nil, // 19: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry - nil, // 20: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry - (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 22: google.protobuf.Duration - (*scopes.ScopeInfo)(nil), // 23: controller.api.resources.scopes.v1.ScopeInfo - (*structpb.Struct)(nil), // 24: google.protobuf.Struct + (*PasswordCredentialAttributes)(nil), // 14: controller.api.resources.sessionrecordings.v1.PasswordCredentialAttributes + (*CredentialLibrary)(nil), // 15: controller.api.resources.sessionrecordings.v1.CredentialLibrary + (*VaultCredentialLibraryAttributes)(nil), // 16: controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes + (*VaultSSHCertificateCredentialLibraryAttributes)(nil), // 17: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes + (*ValuesAtTime)(nil), // 18: controller.api.resources.sessionrecordings.v1.ValuesAtTime + (*SessionRecording)(nil), // 19: controller.api.resources.sessionrecordings.v1.SessionRecording + nil, // 20: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + nil, // 21: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 23: google.protobuf.Duration + (*scopes.ScopeInfo)(nil), // 24: controller.api.resources.scopes.v1.ScopeInfo + (*structpb.Struct)(nil), // 25: google.protobuf.Struct } var file_controller_api_resources_sessionrecordings_v1_session_recording_proto_depIdxs = []int32{ - 21, // 0: controller.api.resources.sessionrecordings.v1.ChannelRecording.created_time:type_name -> google.protobuf.Timestamp - 21, // 1: controller.api.resources.sessionrecordings.v1.ChannelRecording.updated_time:type_name -> google.protobuf.Timestamp - 21, // 2: controller.api.resources.sessionrecordings.v1.ChannelRecording.start_time:type_name -> google.protobuf.Timestamp - 21, // 3: controller.api.resources.sessionrecordings.v1.ChannelRecording.end_time:type_name -> google.protobuf.Timestamp - 22, // 4: controller.api.resources.sessionrecordings.v1.ChannelRecording.duration:type_name -> google.protobuf.Duration - 21, // 5: controller.api.resources.sessionrecordings.v1.ConnectionRecording.created_time:type_name -> google.protobuf.Timestamp - 21, // 6: controller.api.resources.sessionrecordings.v1.ConnectionRecording.updated_time:type_name -> google.protobuf.Timestamp - 21, // 7: controller.api.resources.sessionrecordings.v1.ConnectionRecording.start_time:type_name -> google.protobuf.Timestamp - 21, // 8: controller.api.resources.sessionrecordings.v1.ConnectionRecording.end_time:type_name -> google.protobuf.Timestamp - 22, // 9: controller.api.resources.sessionrecordings.v1.ConnectionRecording.duration:type_name -> google.protobuf.Duration + 22, // 0: controller.api.resources.sessionrecordings.v1.ChannelRecording.created_time:type_name -> google.protobuf.Timestamp + 22, // 1: controller.api.resources.sessionrecordings.v1.ChannelRecording.updated_time:type_name -> google.protobuf.Timestamp + 22, // 2: controller.api.resources.sessionrecordings.v1.ChannelRecording.start_time:type_name -> google.protobuf.Timestamp + 22, // 3: controller.api.resources.sessionrecordings.v1.ChannelRecording.end_time:type_name -> google.protobuf.Timestamp + 23, // 4: controller.api.resources.sessionrecordings.v1.ChannelRecording.duration:type_name -> google.protobuf.Duration + 22, // 5: controller.api.resources.sessionrecordings.v1.ConnectionRecording.created_time:type_name -> google.protobuf.Timestamp + 22, // 6: controller.api.resources.sessionrecordings.v1.ConnectionRecording.updated_time:type_name -> google.protobuf.Timestamp + 22, // 7: controller.api.resources.sessionrecordings.v1.ConnectionRecording.start_time:type_name -> google.protobuf.Timestamp + 22, // 8: controller.api.resources.sessionrecordings.v1.ConnectionRecording.end_time:type_name -> google.protobuf.Timestamp + 23, // 9: controller.api.resources.sessionrecordings.v1.ConnectionRecording.duration:type_name -> google.protobuf.Duration 0, // 10: controller.api.resources.sessionrecordings.v1.ConnectionRecording.channel_recordings:type_name -> controller.api.resources.sessionrecordings.v1.ChannelRecording - 23, // 11: controller.api.resources.sessionrecordings.v1.User.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 23, // 12: controller.api.resources.sessionrecordings.v1.HostCatalog.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 24, // 13: controller.api.resources.sessionrecordings.v1.HostCatalog.attributes:type_name -> google.protobuf.Struct + 24, // 11: controller.api.resources.sessionrecordings.v1.User.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 24, // 12: controller.api.resources.sessionrecordings.v1.HostCatalog.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 25, // 13: controller.api.resources.sessionrecordings.v1.HostCatalog.attributes:type_name -> google.protobuf.Struct 3, // 14: controller.api.resources.sessionrecordings.v1.Host.host_catalog:type_name -> controller.api.resources.sessionrecordings.v1.HostCatalog - 24, // 15: controller.api.resources.sessionrecordings.v1.Host.attributes:type_name -> google.protobuf.Struct + 25, // 15: controller.api.resources.sessionrecordings.v1.Host.attributes:type_name -> google.protobuf.Struct 5, // 16: controller.api.resources.sessionrecordings.v1.Host.static_host_attributes:type_name -> controller.api.resources.sessionrecordings.v1.StaticHostAttributes - 23, // 17: controller.api.resources.sessionrecordings.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 24, // 18: controller.api.resources.sessionrecordings.v1.Target.attributes:type_name -> google.protobuf.Struct + 24, // 17: controller.api.resources.sessionrecordings.v1.Target.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 25, // 18: controller.api.resources.sessionrecordings.v1.Target.attributes:type_name -> google.protobuf.Struct 7, // 19: controller.api.resources.sessionrecordings.v1.Target.ssh_target_attributes:type_name -> controller.api.resources.sessionrecordings.v1.SshTargetAttributes - 24, // 20: controller.api.resources.sessionrecordings.v1.CredentialStore.attributes:type_name -> google.protobuf.Struct + 25, // 20: controller.api.resources.sessionrecordings.v1.CredentialStore.attributes:type_name -> google.protobuf.Struct 9, // 21: controller.api.resources.sessionrecordings.v1.CredentialStore.vault_credential_store_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultCredentialStoreAttributes 8, // 22: controller.api.resources.sessionrecordings.v1.Credential.credential_store:type_name -> controller.api.resources.sessionrecordings.v1.CredentialStore - 24, // 23: controller.api.resources.sessionrecordings.v1.Credential.attributes:type_name -> google.protobuf.Struct + 25, // 23: controller.api.resources.sessionrecordings.v1.Credential.attributes:type_name -> google.protobuf.Struct 11, // 24: controller.api.resources.sessionrecordings.v1.Credential.username_password_attributes:type_name -> controller.api.resources.sessionrecordings.v1.UsernamePasswordCredentialAttributes 12, // 25: controller.api.resources.sessionrecordings.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.sessionrecordings.v1.SshPrivateKeyCredentialAttributes 13, // 26: controller.api.resources.sessionrecordings.v1.Credential.json_attributes:type_name -> controller.api.resources.sessionrecordings.v1.JsonCredentialAttributes - 8, // 27: controller.api.resources.sessionrecordings.v1.CredentialLibrary.credential_store:type_name -> controller.api.resources.sessionrecordings.v1.CredentialStore - 24, // 28: controller.api.resources.sessionrecordings.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct - 15, // 29: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes - 15, // 30: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_generic_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes - 16, // 31: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_ssh_certificate_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes - 19, // 32: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.critical_options:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry - 20, // 33: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.extensions:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry - 2, // 34: controller.api.resources.sessionrecordings.v1.ValuesAtTime.user:type_name -> controller.api.resources.sessionrecordings.v1.User - 6, // 35: controller.api.resources.sessionrecordings.v1.ValuesAtTime.target:type_name -> controller.api.resources.sessionrecordings.v1.Target - 4, // 36: controller.api.resources.sessionrecordings.v1.ValuesAtTime.host:type_name -> controller.api.resources.sessionrecordings.v1.Host - 10, // 37: controller.api.resources.sessionrecordings.v1.ValuesAtTime.credentials:type_name -> controller.api.resources.sessionrecordings.v1.Credential - 14, // 38: controller.api.resources.sessionrecordings.v1.ValuesAtTime.credential_libraries:type_name -> controller.api.resources.sessionrecordings.v1.CredentialLibrary - 23, // 39: controller.api.resources.sessionrecordings.v1.SessionRecording.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 21, // 40: controller.api.resources.sessionrecordings.v1.SessionRecording.created_time:type_name -> google.protobuf.Timestamp - 21, // 41: controller.api.resources.sessionrecordings.v1.SessionRecording.updated_time:type_name -> google.protobuf.Timestamp - 21, // 42: controller.api.resources.sessionrecordings.v1.SessionRecording.start_time:type_name -> google.protobuf.Timestamp - 21, // 43: controller.api.resources.sessionrecordings.v1.SessionRecording.end_time:type_name -> google.protobuf.Timestamp - 22, // 44: controller.api.resources.sessionrecordings.v1.SessionRecording.duration:type_name -> google.protobuf.Duration - 1, // 45: controller.api.resources.sessionrecordings.v1.SessionRecording.connection_recordings:type_name -> controller.api.resources.sessionrecordings.v1.ConnectionRecording - 17, // 46: controller.api.resources.sessionrecordings.v1.SessionRecording.create_time_values:type_name -> controller.api.resources.sessionrecordings.v1.ValuesAtTime - 21, // 47: controller.api.resources.sessionrecordings.v1.SessionRecording.retain_until:type_name -> google.protobuf.Timestamp - 21, // 48: controller.api.resources.sessionrecordings.v1.SessionRecording.delete_after:type_name -> google.protobuf.Timestamp - 49, // [49:49] is the sub-list for method output_type - 49, // [49:49] is the sub-list for method input_type - 49, // [49:49] is the sub-list for extension type_name - 49, // [49:49] is the sub-list for extension extendee - 0, // [0:49] is the sub-list for field type_name + 14, // 27: controller.api.resources.sessionrecordings.v1.Credential.password_attributes:type_name -> controller.api.resources.sessionrecordings.v1.PasswordCredentialAttributes + 8, // 28: controller.api.resources.sessionrecordings.v1.CredentialLibrary.credential_store:type_name -> controller.api.resources.sessionrecordings.v1.CredentialStore + 25, // 29: controller.api.resources.sessionrecordings.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct + 16, // 30: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes + 16, // 31: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_generic_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultCredentialLibraryAttributes + 17, // 32: controller.api.resources.sessionrecordings.v1.CredentialLibrary.vault_ssh_certificate_credential_library_attributes:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes + 20, // 33: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.critical_options:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + 21, // 34: controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.extensions:type_name -> controller.api.resources.sessionrecordings.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + 2, // 35: controller.api.resources.sessionrecordings.v1.ValuesAtTime.user:type_name -> controller.api.resources.sessionrecordings.v1.User + 6, // 36: controller.api.resources.sessionrecordings.v1.ValuesAtTime.target:type_name -> controller.api.resources.sessionrecordings.v1.Target + 4, // 37: controller.api.resources.sessionrecordings.v1.ValuesAtTime.host:type_name -> controller.api.resources.sessionrecordings.v1.Host + 10, // 38: controller.api.resources.sessionrecordings.v1.ValuesAtTime.credentials:type_name -> controller.api.resources.sessionrecordings.v1.Credential + 15, // 39: controller.api.resources.sessionrecordings.v1.ValuesAtTime.credential_libraries:type_name -> controller.api.resources.sessionrecordings.v1.CredentialLibrary + 24, // 40: controller.api.resources.sessionrecordings.v1.SessionRecording.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 22, // 41: controller.api.resources.sessionrecordings.v1.SessionRecording.created_time:type_name -> google.protobuf.Timestamp + 22, // 42: controller.api.resources.sessionrecordings.v1.SessionRecording.updated_time:type_name -> google.protobuf.Timestamp + 22, // 43: controller.api.resources.sessionrecordings.v1.SessionRecording.start_time:type_name -> google.protobuf.Timestamp + 22, // 44: controller.api.resources.sessionrecordings.v1.SessionRecording.end_time:type_name -> google.protobuf.Timestamp + 23, // 45: controller.api.resources.sessionrecordings.v1.SessionRecording.duration:type_name -> google.protobuf.Duration + 1, // 46: controller.api.resources.sessionrecordings.v1.SessionRecording.connection_recordings:type_name -> controller.api.resources.sessionrecordings.v1.ConnectionRecording + 18, // 47: controller.api.resources.sessionrecordings.v1.SessionRecording.create_time_values:type_name -> controller.api.resources.sessionrecordings.v1.ValuesAtTime + 22, // 48: controller.api.resources.sessionrecordings.v1.SessionRecording.retain_until:type_name -> google.protobuf.Timestamp + 22, // 49: controller.api.resources.sessionrecordings.v1.SessionRecording.delete_after:type_name -> google.protobuf.Timestamp + 50, // [50:50] is the sub-list for method output_type + 50, // [50:50] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name } func init() { file_controller_api_resources_sessionrecordings_v1_session_recording_proto_init() } @@ -2450,8 +2518,9 @@ func file_controller_api_resources_sessionrecordings_v1_session_recording_proto_ (*Credential_UsernamePasswordAttributes)(nil), (*Credential_SshPrivateKeyAttributes)(nil), (*Credential_JsonAttributes)(nil), + (*Credential_PasswordAttributes)(nil), } - file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[14].OneofWrappers = []any{ + file_controller_api_resources_sessionrecordings_v1_session_recording_proto_msgTypes[15].OneofWrappers = []any{ (*CredentialLibrary_Attributes)(nil), (*CredentialLibrary_VaultCredentialLibraryAttributes)(nil), (*CredentialLibrary_VaultGenericCredentialLibraryAttributes)(nil), @@ -2463,7 +2532,7 @@ func file_controller_api_resources_sessionrecordings_v1_session_recording_proto_ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDesc), len(file_controller_api_resources_sessionrecordings_v1_session_recording_proto_rawDesc)), NumEnums: 0, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 0, }, From 1ac444d0a8d952a2194b782932b6a827bdf96c70 Mon Sep 17 00:00:00 2001 From: Justin Nguyen Date: Wed, 5 Nov 2025 14:46:22 -0600 Subject: [PATCH 10/11] feat(cli): Add support for password credential in redis helper (#6213) --- api/proxy/credentials.go | 40 +++++ api/proxy/credentials_test.go | 155 +++++++++++++++++- internal/cmd/commands/connect/redis.go | 16 +- ...edential_static_password_credential.up.sql | 0 ...2_credential_vault_password_library.up.sql | 0 testing/internal/e2e/boundary/credential.go | 35 ++++ testing/internal/e2e/infra/docker.go | 79 --------- testing/internal/e2e/infra/redis_docker.go | 146 +++++++++++++++++ .../target_tcp_connect_redis_password_test.go | 96 +++++++++++ .../base/target_tcp_connect_redis_test.go | 141 ---------------- ...cp_connect_redis_username_password_test.go | 97 +++++++++++ 11 files changed, 581 insertions(+), 224 deletions(-) rename internal/db/schema/migrations/oss/postgres/{99 => 101}/01_credential_static_password_credential.up.sql (100%) rename internal/db/schema/migrations/oss/postgres/{99 => 101}/02_credential_vault_password_library.up.sql (100%) create mode 100644 testing/internal/e2e/infra/redis_docker.go create mode 100644 testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go delete mode 100644 testing/internal/e2e/tests/base/target_tcp_connect_redis_test.go create mode 100644 testing/internal/e2e/tests/base/target_tcp_connect_redis_username_password_test.go diff --git a/api/proxy/credentials.go b/api/proxy/credentials.go index 048c9c8e15..60f0cba5cc 100644 --- a/api/proxy/credentials.go +++ b/api/proxy/credentials.go @@ -12,6 +12,7 @@ import ( const ( usernamePasswordCredentialType = "username_password" + passwordCredentialType = "password" sshPrivateKeyCredentialType = "ssh_private_key" ) @@ -26,6 +27,16 @@ type UsernamePassword struct { Consumed bool } +// Password contains a password credential +type Password struct { + Password string `mapstructure:"password"` + + Raw *targets.SessionCredential + // Consumed can be set by the caller to indicate that the credential has + // been used, e.g. displayed to the user + Consumed bool +} + // SshPrivateKey contains the username and private key with optional passphrase // for the key type SshPrivateKey struct { @@ -41,6 +52,7 @@ type SshPrivateKey struct { type Credentials struct { UsernamePassword []UsernamePassword + Password []Password SshPrivateKey []SshPrivateKey // Unspecified are credentials that do not match one of the types above Unspecified []*targets.SessionCredential @@ -62,6 +74,11 @@ func (c Credentials) UnconsumedSessionCredentials() []*targets.SessionCredential out = append(out, c.Raw) } } + for _, c := range c.Password { + if !c.Consumed { + out = append(out, c.Raw) + } + } return out } @@ -76,6 +93,7 @@ func ParseCredentials(creds []*targets.SessionCredential) (Credentials, error) { } var upCred UsernamePassword + var pCred Password var spkCred SshPrivateKey switch cred.CredentialSource.CredentialType { case usernamePasswordCredentialType: @@ -90,6 +108,18 @@ func ParseCredentials(creds []*targets.SessionCredential) (Credentials, error) { continue } + case passwordCredentialType: + // Decode attributes from credential struct + if err := mapstructure.Decode(cred.Credential, &pCred); err != nil { + return Credentials{}, err + } + + if pCred.Password != "" { + pCred.Raw = cred + out.Password = append(out.Password, pCred) + continue + } + case sshPrivateKeyCredentialType: // Decode attributes from credential struct if err := mapstructure.Decode(cred.Credential, &spkCred); err != nil { @@ -116,6 +146,16 @@ func ParseCredentials(creds []*targets.SessionCredential) (Credentials, error) { continue } + // Attempt unmarshaling into password creds + if err := mapstructure.Decode(cred.Secret.Decoded, &pCred); err != nil { + return Credentials{}, err + } + if pCred.Password != "" { + pCred.Raw = cred + out.Password = append(out.Password, pCred) + continue + } + // Attempt unmarshaling into ssh private key creds if err := mapstructure.Decode(cred.Secret.Decoded, &spkCred); err != nil { return Credentials{}, err diff --git a/api/proxy/credentials_test.go b/api/proxy/credentials_test.go index 5e3d763a70..8a2f68a275 100644 --- a/api/proxy/credentials_test.go +++ b/api/proxy/credentials_test.go @@ -29,6 +29,15 @@ var ( }, } + typedPassword = &targets.SessionCredential{ + CredentialSource: &targets.CredentialSource{ + CredentialType: passwordCredentialType, + }, + Credential: map[string]any{ + "password": "pass", + }, + } + typedSshPrivateKey = &targets.SessionCredential{ CredentialSource: &targets.CredentialSource{ CredentialType: sshPrivateKeyCredentialType, @@ -75,6 +84,17 @@ var ( }, } + vaultPassword = &targets.SessionCredential{ + CredentialSource: &targets.CredentialSource{ + Type: vaultGenericLibrarySubtype, + }, + Secret: &targets.SessionSecret{ + Decoded: map[string]any{ + "password": "vault-decoded-pass", + }, + }, + } + vaultSshPrivateKey = &targets.SessionCredential{ CredentialSource: &targets.CredentialSource{ Type: vaultGenericLibrarySubtype, @@ -119,6 +139,17 @@ var ( }, } + staticPassword = &targets.SessionCredential{ + CredentialSource: &targets.CredentialSource{ + Type: staticSubtype, + }, + Secret: &targets.SessionSecret{ + Decoded: map[string]any{ + "password": "static-decoded-pass", + }, + }, + } + staticSshPrivateKey = &targets.SessionCredential{ CredentialSource: &targets.CredentialSource{ Type: staticSubtype, @@ -215,6 +246,21 @@ func Test_parseCredentials(t *testing.T) { }, wantErr: false, }, + { + name: "password-typed", + creds: []*targets.SessionCredential{ + typedPassword, + }, + wantCreds: Credentials{ + Password: []Password{ + { + Password: "pass", + Raw: typedPassword, + }, + }, + }, + wantErr: false, + }, { name: "ssh-private-key-typed", creds: []*targets.SessionCredential{ @@ -247,6 +293,21 @@ func Test_parseCredentials(t *testing.T) { }, wantErr: false, }, + { + name: "vault-password-decoded", + creds: []*targets.SessionCredential{ + vaultPassword, + }, + wantCreds: Credentials{ + Password: []Password{ + { + Password: "vault-decoded-pass", + Raw: vaultPassword, + }, + }, + }, + wantErr: false, + }, { name: "vault-private-key-decoded", creds: []*targets.SessionCredential{ @@ -343,6 +404,21 @@ func Test_parseCredentials(t *testing.T) { }, wantErr: false, }, + { + name: "static-password-decoded", + creds: []*targets.SessionCredential{ + staticPassword, + }, + wantCreds: Credentials{ + Password: []Password{ + { + Password: "static-decoded-pass", + Raw: staticPassword, + }, + }, + }, + wantErr: false, + }, { name: "static-private-key-decoded", creds: []*targets.SessionCredential{ @@ -388,7 +464,7 @@ func Test_parseCredentials(t *testing.T) { creds: []*targets.SessionCredential{ staticSshPrivateKey, UnspecifiedCred1, vaultSshPrivateKey, typedUsernamePassword, UnspecifiedCred, vaultUsernamePassword, typedSshPrivateKey, staticUsernamePassword, - staticKv, + staticKv, typedPassword, vaultPassword, staticPassword, }, wantCreds: Credentials{ SshPrivateKey: []SshPrivateKey{ @@ -425,6 +501,20 @@ func Test_parseCredentials(t *testing.T) { Raw: typedUsernamePassword, }, }, + Password: []Password{ + { + Password: "static-decoded-pass", + Raw: staticPassword, + }, + { + Password: "vault-decoded-pass", + Raw: vaultPassword, + }, + { + Password: "pass", + Raw: typedPassword, + }, + }, Unspecified: []*targets.SessionCredential{ UnspecifiedCred, UnspecifiedCred1, staticKv, }, @@ -447,6 +537,7 @@ func Test_parseCredentials(t *testing.T) { assert.ElementsMatch(tt.wantCreds.UsernamePassword, creds.UsernamePassword) assert.ElementsMatch(tt.wantCreds.SshPrivateKey, creds.SshPrivateKey) assert.ElementsMatch(tt.wantCreds.Unspecified, creds.Unspecified) + assert.ElementsMatch(tt.wantCreds.Password, creds.Password) }) } } @@ -507,6 +598,29 @@ func Test_unconsumedSessionCredentials(t *testing.T) { }, wantCreds: nil, }, + { + name: "p", + creds: Credentials{ + Password: []Password{ + { + Raw: vaultPassword, + }, + }, + }, + wantCreds: []*targets.SessionCredential{vaultPassword}, + }, + { + name: "p-consumed", + creds: Credentials{ + Password: []Password{ + { + Raw: vaultPassword, + Consumed: true, + }, + }, + }, + wantCreds: nil, + }, { name: "Unspecified", creds: Credentials{ @@ -542,10 +656,23 @@ func Test_unconsumedSessionCredentials(t *testing.T) { Consumed: true, }, }, + Password: []Password{ + { + Raw: staticPassword, + }, + { + Raw: vaultPassword, + }, + { + Raw: typedPassword, + Consumed: true, + }, + }, Unspecified: []*targets.SessionCredential{UnspecifiedCred, UnspecifiedCred1}, }, wantCreds: []*targets.SessionCredential{ vaultSshPrivateKey, typedSshPrivateKey, vaultUsernamePassword, UnspecifiedCred, UnspecifiedCred1, + staticPassword, vaultPassword, }, }, { @@ -579,6 +706,20 @@ func Test_unconsumedSessionCredentials(t *testing.T) { Consumed: true, }, }, + Password: []Password{ + { + Raw: staticPassword, + Consumed: true, + }, + { + Raw: vaultPassword, + Consumed: true, + }, + { + Raw: typedPassword, + Consumed: true, + }, + }, Unspecified: []*targets.SessionCredential{UnspecifiedCred, UnspecifiedCred1}, }, wantCreds: []*targets.SessionCredential{ @@ -610,11 +751,23 @@ func Test_unconsumedSessionCredentials(t *testing.T) { Raw: typedUsernamePassword, }, }, + Password: []Password{ + { + Raw: staticPassword, + }, + { + Raw: vaultPassword, + }, + { + Raw: typedPassword, + }, + }, Unspecified: []*targets.SessionCredential{UnspecifiedCred, UnspecifiedCred1}, }, wantCreds: []*targets.SessionCredential{ staticSshPrivateKey, UnspecifiedCred1, vaultSshPrivateKey, typedUsernamePassword, UnspecifiedCred, vaultUsernamePassword, typedSshPrivateKey, staticUsernamePassword, + staticPassword, vaultPassword, typedPassword, }, }, } diff --git a/internal/cmd/commands/connect/redis.go b/internal/cmd/commands/connect/redis.go index 7a8bde6bdd..0ba47175f3 100644 --- a/internal/cmd/commands/connect/redis.go +++ b/internal/cmd/commands/connect/redis.go @@ -49,13 +49,21 @@ func (r *redisFlags) buildArgs(c *Command, port, ip, _ string, creds proxy.Crede var username, password string retCreds = creds - if len(retCreds.UsernamePassword) > 0 { + switch { + case len(retCreds.UsernamePassword) > 0: // Mark credential as consumed, such that it is not printed to the user retCreds.UsernamePassword[0].Consumed = true // Grab the first available username/password credential brokered username = retCreds.UsernamePassword[0].Username password = retCreds.UsernamePassword[0].Password + + case len(retCreds.Password) > 0: + // Mark credential as consumed, such that it is not printed to the user + retCreds.Password[0].Consumed = true + + // Grab the first available password credential brokered + password = retCreds.Password[0].Password } switch r.flagRedisStyle { @@ -69,12 +77,14 @@ func (r *redisFlags) buildArgs(c *Command, port, ip, _ string, creds proxy.Crede case username != "": args = append(args, "--user", username) case c.flagUsername != "": - args = append(args, "--user", c.flagUsername, "--askpass") + args = append(args, "--user", c.flagUsername) } - // Password is read by redis-cli via environment variable. The password disappears after the command exits. if password != "" { envs = append(envs, fmt.Sprintf("REDISCLI_AUTH=%s", password)) + } else { + // prompt for password if it wasn't provided + envs = append(envs, "--askpass") } } diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql b/internal/db/schema/migrations/oss/postgres/101/01_credential_static_password_credential.up.sql similarity index 100% rename from internal/db/schema/migrations/oss/postgres/99/01_credential_static_password_credential.up.sql rename to internal/db/schema/migrations/oss/postgres/101/01_credential_static_password_credential.up.sql diff --git a/internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql b/internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql similarity index 100% rename from internal/db/schema/migrations/oss/postgres/99/02_credential_vault_password_library.up.sql rename to internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql diff --git a/testing/internal/e2e/boundary/credential.go b/testing/internal/e2e/boundary/credential.go index a933457f9c..59319691aa 100644 --- a/testing/internal/e2e/boundary/credential.go +++ b/testing/internal/e2e/boundary/credential.go @@ -262,6 +262,41 @@ func CreateStaticCredentialUsernamePasswordCli(t testing.TB, ctx context.Context return credentialId, nil } +// CreateStaticCredentialPasswordCli uses the cli to create a new password credential in the +// provided static credential store. +// Returns the id of the new credential +func CreateStaticCredentialPasswordCli(t testing.TB, ctx context.Context, credentialStoreId string, password string) (string, error) { + name, err := base62.Random(16) + if err != nil { + return "", err + } + + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "credentials", "create", "password", + "-credential-store-id", credentialStoreId, + "-password", "env://E2E_CREDENTIALS_PASSWORD", + "-name", fmt.Sprintf("e2e Credential %s", name), + "-description", "e2e", + "-format", "json", + ), + e2e.WithEnv("E2E_CREDENTIALS_PASSWORD", password), + ) + if output.Err != nil { + return "", fmt.Errorf("%w: %s", output.Err, string(output.Stderr)) + } + + var createCredentialsResult credentials.CredentialCreateResult + err = json.Unmarshal(output.Stdout, &createCredentialsResult) + if err != nil { + return "", err + } + + credentialId := createCredentialsResult.Item.Id + t.Logf("Created Password Credential: %s", credentialId) + return credentialId, nil +} + // CreateStaticCredentialUsernamePasswordDomainCli uses the cli to create a new username password domain credential in the // provided static credential store. // Returns the id of the new credential diff --git a/testing/internal/e2e/infra/docker.go b/testing/internal/e2e/infra/docker.go index 7562131a2b..7250980a7f 100644 --- a/testing/internal/e2e/infra/docker.go +++ b/testing/internal/e2e/infra/docker.go @@ -35,12 +35,6 @@ type cassandraConfig struct { NetworkAlias string } -type redisConfig struct { - User string - Password string - NetworkAlias string -} - // StartBoundaryDatabase spins up a postgres database in a docker container. // Returns information about the container func StartBoundaryDatabase(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container { @@ -478,63 +472,6 @@ func StartCassandra(t testing.TB, pool *dockertest.Pool, network *dockertest.Net } } -// StartRedis starts a Redis database in a docker container. -// Returns information about the container -func StartRedis(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container { - t.Log("Starting Redis database...") - c, err := LoadConfig() - require.NoError(t, err) - - err = pool.Client.PullImage(docker.PullImageOptions{ - Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository), - Tag: tag, - }, docker.AuthConfiguration{}) - require.NoError(t, err) - - config := redisConfig{ - User: "e2eboundary", - Password: "e2eboundary", - NetworkAlias: "e2eredis", - } - - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository), - Tag: tag, - ExposedPorts: []string{"6379/tcp"}, - Name: config.NetworkAlias, - Networks: []*dockertest.Network{network}, - }) - require.NoError(t, err) - - err = pool.Retry(func() error { - cmd := exec.Command("docker", "exec", config.NetworkAlias, "redis-cli", "PING") - output, cmdErr := cmd.CombinedOutput() - if cmdErr != nil { - return fmt.Errorf("failed to connect to Redis container '%s': %v\nOutput: %s", config.NetworkAlias, cmdErr, string(output)) - } - return nil - }) - require.NoError(t, err, "Redis container did not start in time or is not healthy") - - err = setupRedisAuthAndUser(t, resource, pool, &config) - require.NoError(t, err) - - return &Container{ - Resource: resource, - UriLocalhost: fmt.Sprintf( - "redis://%s:%s@localhost:6379", - config.User, - config.Password, - ), - UriNetwork: fmt.Sprintf( - "redis://%s:%s@%s:6379", - config.User, - config.Password, - config.NetworkAlias, - ), - } -} - // setupCassandraAuthAndUser enables authentication on a Cassandra container and creates a user with permissions. func setupCassandraAuthAndUser(t testing.TB, resource *dockertest.Resource, pool *dockertest.Pool, config *cassandraConfig) error { t.Helper() @@ -593,19 +530,3 @@ func setupCassandraAuthAndUser(t testing.TB, resource *dockertest.Resource, pool } return nil } - -// setupRedisAuthAndUser configures a Redis container by creating a user with permissions. -func setupRedisAuthAndUser(t testing.TB, resource *dockertest.Resource, pool *dockertest.Pool, config *redisConfig) error { - t.Helper() - t.Log("Configuring Redis authentication and user permissions...") - - err := exec.Command( - "docker", "exec", config.NetworkAlias, "redis-cli", - "ACL", "SETUSER", config.User, "on", fmt.Sprintf(">%s", config.Password), "+@read", "+@write", "allkeys", - ).Run() - if err != nil { - return err - } - - return nil -} diff --git a/testing/internal/e2e/infra/redis_docker.go b/testing/internal/e2e/infra/redis_docker.go new file mode 100644 index 0000000000..f5ba69d82e --- /dev/null +++ b/testing/internal/e2e/infra/redis_docker.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package infra + +import ( + "fmt" + "io" + "net/url" + "os/exec" + "strings" + "testing" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/require" +) + +type RedisContainerInfo struct { + Hostname string + Port string + Username string + Password string +} + +type redisConfig struct { + User string + Password string + NetworkAlias string +} + +// SetupRedisContainer starts a Redis container and returns its connection info +func SetupRedisContainer(t *testing.T) *RedisContainerInfo { + pool, err := dockertest.NewPool("") + require.NoError(t, err) + + network, err := pool.NetworksByName("e2e_cluster") + require.NoError(t, err, "Failed to get e2e_cluster network") + + c := startRedis(t, pool, &network[0], "redis", "latest") + require.NotNil(t, c, "Redis container should not be nil") + t.Cleanup(func() { + if err := pool.Purge(c.Resource); err != nil { + t.Logf("Failed to purge Redis container: %v", err) + } + }) + + u, err := url.Parse(c.UriNetwork) + t.Log(u) + require.NoError(t, err, "Failed to parse Redis URL") + + user, hostname, port := u.User.Username(), u.Hostname(), u.Port() + pw, pwSet := u.User.Password() + + t.Logf("Redis info: user=%s, host=%s, port=%s, password-set:%t", + user, hostname, port, pwSet) + + return &RedisContainerInfo{ + Hostname: hostname, + Port: port, + Username: user, + Password: pw, + } +} + +// startRedis starts a Redis database in a docker container. +// Returns information about the container +func startRedis(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container { + t.Log("Starting Redis database...") + c, err := LoadConfig() + require.NoError(t, err) + + err = pool.Client.PullImage(docker.PullImageOptions{ + Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository), + Tag: tag, + }, docker.AuthConfiguration{}) + require.NoError(t, err) + + config := redisConfig{ + User: "e2eboundary", + Password: "e2eboundary", + NetworkAlias: "e2eredis", + } + + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository), + Tag: tag, + ExposedPorts: []string{"6379/tcp"}, + Name: config.NetworkAlias, + Networks: []*dockertest.Network{network}, + }) + require.NoError(t, err) + + err = pool.Retry(func() error { + cmd := exec.Command("docker", "exec", config.NetworkAlias, "redis-cli", "PING") + output, cmdErr := cmd.CombinedOutput() + if cmdErr != nil { + return fmt.Errorf("failed to connect to Redis container '%s': %v\nOutput: %s", config.NetworkAlias, cmdErr, string(output)) + } + return nil + }) + require.NoError(t, err, "Redis container did not start in time or is not healthy") + + // configure redis users + // e2e user + err = exec.Command( + "docker", "exec", config.NetworkAlias, "redis-cli", + "ACL", "SETUSER", config.User, "on", fmt.Sprintf(">%s", config.Password), "+@all", "allkeys", + ).Run() + require.NoError(t, err) + + // default user + err = exec.Command( + "docker", "exec", config.NetworkAlias, "redis-cli", + "ACL", "SETUSER", "default", fmt.Sprintf(">%s", config.Password), + ).Run() + require.NoError(t, err) + + return &Container{ + Resource: resource, + UriLocalhost: fmt.Sprintf( + "redis://%s:%s@localhost:6379", + config.User, + config.Password, + ), + UriNetwork: fmt.Sprintf( + "redis://%s:%s@%s:6379", + config.User, + config.Password, + config.NetworkAlias, + ), + } +} + +func SendRedisCommand(stdin io.WriteCloser, stdout io.ReadCloser, cmdStr string) (string, error) { + _, err := stdin.Write([]byte(cmdStr)) + if err != nil { + return "", err + } + buf := make([]byte, 1024) + n, err := stdout.Read(buf) + if err != nil { + return "", err + } + return strings.TrimSpace(string(buf[:n])), nil +} diff --git a/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go b/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go new file mode 100644 index 0000000000..9e08f331f0 --- /dev/null +++ b/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_test + +import ( + "context" + "io" + "os/exec" + "testing" + + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/hashicorp/boundary/testing/internal/e2e/infra" + "github.com/stretchr/testify/require" +) + +// TestCliTcpTargetConnectRedisPassword uses the boundary cli to connect to a target +// using `connect redis` and a password credential. +func TestCliTcpTargetConnectRedisPassword(t *testing.T) { + e2e.MaybeSkipTest(t) + + // Setup + ctx := context.Background() + redisInfo := infra.SetupRedisContainer(t) + boundary.AuthenticateAdminCli(t, ctx) + + // Create Boundary resources + orgId, err := boundary.CreateOrgCli(t, ctx) + require.NoError(t, err) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + projectId, err := boundary.CreateProjectCli(t, ctx, orgId) + require.NoError(t, err) + + targetId, err := boundary.CreateTargetCli( + t, + ctx, + projectId, + redisInfo.Port, + target.WithAddress(redisInfo.Hostname), + ) + require.NoError(t, err) + + storeId, err := boundary.CreateCredentialStoreStaticCli(t, ctx, projectId) + require.NoError(t, err) + credentialId, err := boundary.CreateStaticCredentialPasswordCli( + t, + ctx, + storeId, + redisInfo.Password, + ) + require.NoError(t, err) + + err = boundary.AddBrokeredCredentialSourceToTargetCli(t, ctx, targetId, credentialId) + require.NoError(t, err) + + // Validate redis connect + cmd := exec.CommandContext(ctx, + "boundary", + "connect", "redis", + "-target-id", targetId, + ) + + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + require.NoError(t, cmd.Start()) + + output, err := infra.SendRedisCommand(stdin, stdout, "ACL WHOAMI\r\n") + require.NoError(t, err) + require.Equal(t, "default", output) // redis default user + + output, err = infra.SendRedisCommand(stdin, stdout, "SET e2etestkey e2etestvalue\r\n") + require.NoError(t, err) + require.Equal(t, "OK", output) + + output, err = infra.SendRedisCommand(stdin, stdout, "GET e2etestkey\r\n") + require.NoError(t, err) + require.Equal(t, "e2etestvalue", output) + + output, err = infra.SendRedisCommand(stdin, stdout, "QUIT\r\n") + require.Equal(t, io.EOF, err) + require.Empty(t, output) + + // Confirm that boundary connect has closed + err = cmd.Wait() + require.NoError(t, err) +} diff --git a/testing/internal/e2e/tests/base/target_tcp_connect_redis_test.go b/testing/internal/e2e/tests/base/target_tcp_connect_redis_test.go deleted file mode 100644 index 7e6e19f7c7..0000000000 --- a/testing/internal/e2e/tests/base/target_tcp_connect_redis_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package base_test - -import ( - "context" - "io" - "net/url" - "os/exec" - "strings" - "testing" - - "github.com/hashicorp/boundary/internal/target" - "github.com/hashicorp/boundary/testing/internal/e2e" - "github.com/hashicorp/boundary/testing/internal/e2e/boundary" - "github.com/hashicorp/boundary/testing/internal/e2e/infra" - "github.com/ory/dockertest/v3" - "github.com/stretchr/testify/require" -) - -// TestCliTcpTargetConnectRedis uses the boundary cli to connect to a target using `connect redis` -func TestCliTcpTargetConnectRedis(t *testing.T) { - e2e.MaybeSkipTest(t) - - pool, err := dockertest.NewPool("") - require.NoError(t, err) - - ctx := context.Background() - - network, err := pool.NetworksByName("e2e_cluster") - require.NoError(t, err, "Failed to get e2e_cluster network") - - c := infra.StartRedis(t, pool, &network[0], "redis", "latest") - require.NotNil(t, c, "Redis container should not be nil") - t.Cleanup(func() { - if err := pool.Purge(c.Resource); err != nil { - t.Logf("Failed to purge Redis container: %v", err) - } - }) - - u, err := url.Parse(c.UriNetwork) - t.Log(u) - require.NoError(t, err, "Failed to parse Redis URL") - - user, hostname, port := u.User.Username(), u.Hostname(), u.Port() - pw, pwSet := u.User.Password() - - t.Logf("Redis info: user=%s, host=%s, port=%s, password-set:%t", - user, hostname, port, pwSet) - - // Wait for Redis to be ready - err = pool.Retry(func() error { - out, e := exec.CommandContext(ctx, "docker", "exec", hostname, - "redis-cli", "-h", hostname, "-p", port, "PING").CombinedOutput() - t.Logf("Redis PING output: %s", out) - return e - }) - require.NoError(t, err, "Redis container failed to start") - - boundary.AuthenticateAdminCli(t, ctx) - - orgId, err := boundary.CreateOrgCli(t, ctx) - require.NoError(t, err) - t.Cleanup(func() { - ctx := context.Background() - boundary.AuthenticateAdminCli(t, ctx) - output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId)) - require.NoError(t, output.Err, string(output.Stderr)) - }) - - projectId, err := boundary.CreateProjectCli(t, ctx, orgId) - require.NoError(t, err) - - targetId, err := boundary.CreateTargetCli( - t, - ctx, - projectId, - port, - []target.Option{target.WithAddress(hostname)}, - ) - require.NoError(t, err) - - storeId, err := boundary.CreateCredentialStoreStaticCli(t, ctx, projectId) - require.NoError(t, err) - - credentialId, err := boundary.CreateStaticCredentialUsernamePasswordCli( - t, - ctx, - storeId, - user, - pw, - ) - require.NoError(t, err) - - err = boundary.AddBrokeredCredentialSourceToTargetCli(t, ctx, targetId, credentialId) - require.NoError(t, err) - - t.Logf("Attempting to connect to Redis target %s", targetId) - - cmd := exec.CommandContext(ctx, - "boundary", - "connect", "redis", - "-target-id", targetId, - ) - - stdin, err := cmd.StdinPipe() - require.NoError(t, err) - stdout, err := cmd.StdoutPipe() - require.NoError(t, err) - require.NoError(t, cmd.Start()) - - output, err := sendRedisCommand(stdin, stdout, "SET e2etestkey e2etestvalue\r\n") - require.NoError(t, err) - require.Equal(t, "OK", output) - - output, err = sendRedisCommand(stdin, stdout, "GET e2etestkey\r\n") - require.NoError(t, err) - require.Equal(t, "e2etestvalue", output) - - output, err = sendRedisCommand(stdin, stdout, "QUIT\r\n") - require.Equal(t, io.EOF, err) - require.Empty(t, output) - - // Confirm that boundary connect has closed - err = cmd.Wait() - require.NoError(t, err) -} - -func sendRedisCommand(stdin io.WriteCloser, stdout io.ReadCloser, cmdStr string) (string, error) { - _, err := stdin.Write([]byte(cmdStr)) - if err != nil { - return "", err - } - buf := make([]byte, 1024) - n, err := stdout.Read(buf) - if err != nil { - return "", err - } - return strings.TrimSpace(string(buf[:n])), nil -} diff --git a/testing/internal/e2e/tests/base/target_tcp_connect_redis_username_password_test.go b/testing/internal/e2e/tests/base/target_tcp_connect_redis_username_password_test.go new file mode 100644 index 0000000000..b76275b9b0 --- /dev/null +++ b/testing/internal/e2e/tests/base/target_tcp_connect_redis_username_password_test.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_test + +import ( + "context" + "io" + "os/exec" + "testing" + + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/hashicorp/boundary/testing/internal/e2e/infra" + "github.com/stretchr/testify/require" +) + +// TestCliTcpTargetConnectRedisUsernamePassword uses the boundary cli to connect to a target +// using `connect redis` and a username-password credential. +func TestCliTcpTargetConnectRedisUsernamePassword(t *testing.T) { + e2e.MaybeSkipTest(t) + + // Setup + ctx := context.Background() + redisInfo := infra.SetupRedisContainer(t) + boundary.AuthenticateAdminCli(t, ctx) + + // Create Boundary resources + orgId, err := boundary.CreateOrgCli(t, ctx) + require.NoError(t, err) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + projectId, err := boundary.CreateProjectCli(t, ctx, orgId) + require.NoError(t, err) + + targetId, err := boundary.CreateTargetCli( + t, + ctx, + projectId, + redisInfo.Port, + []target.Option{target.WithAddress(redisInfo.Hostname)}, + ) + require.NoError(t, err) + + storeId, err := boundary.CreateCredentialStoreStaticCli(t, ctx, projectId) + require.NoError(t, err) + credentialId, err := boundary.CreateStaticCredentialUsernamePasswordCli( + t, + ctx, + storeId, + redisInfo.Username, + redisInfo.Password, + ) + require.NoError(t, err) + + err = boundary.AddBrokeredCredentialSourceToTargetCli(t, ctx, targetId, credentialId) + require.NoError(t, err) + + // Validate redis connect + cmd := exec.CommandContext(ctx, + "boundary", + "connect", "redis", + "-target-id", targetId, + ) + + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + require.NoError(t, cmd.Start()) + + output, err := infra.SendRedisCommand(stdin, stdout, "ACL WHOAMI\r\n") + require.NoError(t, err) + require.Equal(t, redisInfo.Username, output) + + output, err = infra.SendRedisCommand(stdin, stdout, "SET e2etestkey e2etestvalue\r\n") + require.NoError(t, err) + require.Equal(t, "OK", output) + + output, err = infra.SendRedisCommand(stdin, stdout, "GET e2etestkey\r\n") + require.NoError(t, err) + require.Equal(t, "e2etestvalue", output) + + output, err = infra.SendRedisCommand(stdin, stdout, "QUIT\r\n") + require.Equal(t, io.EOF, err) + require.Empty(t, output) + + // Confirm that boundary connect has closed + err = cmd.Wait() + require.NoError(t, err) +} From c39a50570a77dc4df8dbfb5bce0fb98755ff46f1 Mon Sep 17 00:00:00 2001 From: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:20:53 -0800 Subject: [PATCH 11/11] fix(db): vault password library migration adjustments and mapping overrides (#6229) --- internal/api/genapi/input.go | 16 - .../credential/vault/mapping_overriders.go | 2 +- .../01_credential_vault_ldap_library.up.sql | 1 + ...2_credential_vault_password_library.up.sql | 336 ++++++++++-------- ...username_password_domain_credential.up.sql | 8 +- .../02_username_password_domain_vault.up.sql | 12 +- ...1_credential_vault_library_refactor.up.sql | 1 + .../target_tcp_connect_redis_password_test.go | 2 +- 8 files changed, 203 insertions(+), 175 deletions(-) diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index ec639c8613..24bc1803d4 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -752,22 +752,6 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, - { - inProto: &credentials.PasswordAttributes{}, - outFile: "credentials/password_attributes.gen.go", - subtypeName: "PasswordCredential", - subtype: "password", - fieldOverrides: []fieldInfo{ - { - Name: "Password", - SkipDefault: true, - }, - }, - parentTypeName: "Credential", - templates: []*template.Template{ - mapstructureConversionTemplate, - }, - }, { inProto: &credentials.SshPrivateKeyAttributes{}, outFile: "credentials/ssh_private_key_attributes.gen.go", diff --git a/internal/credential/vault/mapping_overriders.go b/internal/credential/vault/mapping_overriders.go index e7f5e9416f..13f850f502 100644 --- a/internal/credential/vault/mapping_overriders.go +++ b/internal/credential/vault/mapping_overriders.go @@ -224,7 +224,7 @@ func (o *PasswordOverride) TableName() string { if o.tableName != "" { return o.tableName } - return "credential_vault_library_password_mapping_override" + return "credential_vault_generic_library_password_mapping_override" } // SetTableName sets the table name. diff --git a/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql b/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql index 98967e8652..ecbfe2bb11 100644 --- a/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql +++ b/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql @@ -79,6 +79,7 @@ begin; for each row execute procedure default_vault_ldap_credential_type(); -- Replaces view from 99/01_credential_vault_library_refactor.up.sql + -- Replaced in 101/02_credential_vault_password_library.up.sql drop view credential_vault_library_issue_credentials; create view credential_vault_library_issue_credentials as with diff --git a/internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql b/internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql index 80dd3a5f5d..f98f854688 100644 --- a/internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql +++ b/internal/db/schema/migrations/oss/postgres/101/02_credential_vault_password_library.up.sql @@ -3,191 +3,235 @@ begin; - create table credential_vault_library_password_mapping_override ( + create table credential_vault_generic_library_password_mapping_override ( library_id wt_public_id primary key - constraint credential_vault_library_fkey - references credential_vault_library (public_id) + constraint credential_vault_generic_library_fkey + references credential_vault_generic_library (public_id) on delete cascade on update cascade - constraint credential_vault_library_mapping_override_fkey - references credential_vault_library_mapping_override (library_id) + constraint credential_vault_generic_library_mapping_override_fkey + references credential_vault_generic_library_mapping_override (library_id) on delete cascade on update cascade, password_attribute wt_sentinel default wt_to_sentinel('no override') not null ); - comment on table credential_vault_library_password_mapping_override is - 'credential_vault_library_password_mapping_override is a table ' + comment on table credential_vault_generic_library_password_mapping_override is + 'credential_vault_generic_library_password_mapping_override is a table ' 'where each row represents a mapping that overrides the default mapping ' 'from a generic vault secret to a password credential type ' 'for a vault credential library.'; - create trigger insert_credential_vault_library_mapping_override_subtype before insert on credential_vault_library_password_mapping_override - for each row execute procedure insert_credential_vault_library_mapping_override_subtype(); + create trigger insert_credential_vault_generic_library_mapping_override_subtyp before insert on credential_vault_generic_library_password_mapping_override + for each row execute procedure insert_credential_vault_generic_library_mapping_override_subtyp(); - create trigger delete_credential_vault_library_mapping_override_subtype after delete on credential_vault_library_password_mapping_override - for each row execute procedure delete_credential_vault_library_mapping_override_subtype(); + create trigger delete_credential_vault_generic_library_mapping_override_subtyp after delete on credential_vault_generic_library_password_mapping_override + for each row execute procedure delete_credential_vault_generic_library_mapping_override_subtyp(); - - -- Replaces view from 98/02_username_password_domain_vault.up.sql + -- Replaces view from 100/01_credential_vault_ldap_library.up.sql drop view credential_vault_library_issue_credentials; create view credential_vault_library_issue_credentials as - with - username_password_override (library_id, username_attribute, password_attribute) as ( - select library_id, - nullif(username_attribute, wt_to_sentinel('no override')), - nullif(password_attribute, wt_to_sentinel('no override')) - from credential_vault_library_username_password_mapping_override - ), - ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( - select library_id, - nullif(username_attribute, wt_to_sentinel('no override')), - nullif(private_key_attribute, wt_to_sentinel('no override')), - nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) - from credential_vault_library_ssh_private_key_mapping_override - ), - username_password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( - select library_id, - nullif(username_attribute, wt_to_sentinel('no override')), - nullif(password_attribute, wt_to_sentinel('no override')), - nullif(domain_attribute, wt_to_sentinel('no override')) - from credential_vault_library_username_password_domain_mapping_ovrd - ), - password_override (library_id, password_attribute) as ( - select library_id, - nullif(password_attribute, wt_to_sentinel('no override')) - from credential_vault_library_password_mapping_override - ) - select library.public_id as public_id, - library.store_id as store_id, - library.name as name, - library.description as description, - library.create_time as create_time, - library.update_time as update_time, - library.version as version, - library.vault_path as vault_path, - library.http_method as http_method, - library.http_request_body as http_request_body, - library.credential_type as credential_type, - null as key_type, - null as key_bits, - null as username, - null as ttl, - null as key_id, - null as critical_options, - null as extensions, - store.project_id as project_id, - store.vault_address as vault_address, - store.namespace as namespace, - store.ca_cert as ca_cert, - store.tls_server_name as tls_server_name, - store.tls_skip_verify as tls_skip_verify, - store.worker_filter as worker_filter, - store.ct_token as ct_token, -- encrypted - store.token_hmac as token_hmac, - store.token_status as token_status, - store.token_key_id as token_key_id, - store.client_cert as client_cert, - store.ct_client_key as ct_client_key, -- encrypted - store.client_key_id as client_key_id, - coalesce(upasso.username_attribute, sshpk.username_attribute, pd.username_attribute) - as username_attribute, - coalesce(upasso.password_attribute, pd.password_attribute, po.password_attribute) - as password_attribute, - pd.domain_attribute as domain_attribute, - sshpk.private_key_attribute as private_key_attribute, - sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, - 'generic' as cred_lib_type, -- used to switch on - null as additional_valid_principals - from credential_vault_library library - join credential_vault_store_client store - on library.store_id = store.public_id - left join username_password_override upasso - on library.public_id = upasso.library_id - left join ssh_private_key_override sshpk - on library.public_id = sshpk.library_id - left join username_password_domain_override pd - on library.public_id = pd.library_id - left join password_override po - on library.public_id = po.library_id - union - select library.public_id as public_id, - library.store_id as store_id, - library.name as name, - library.description as description, - library.create_time as create_time, - library.update_time as update_time, - library.version as version, - library.vault_path as vault_path, - null as http_method, - null as http_request_body, - library.credential_type as credential_type, - library.key_type as key_type, - library.key_bits as key_bits, - library.username as username, - library.ttl as ttl, - library.key_id as key_id, - library.critical_options as critical_options, - library.extensions as extensions, - store.project_id as project_id, - store.vault_address as vault_address, - store.namespace as namespace, - store.ca_cert as ca_cert, - store.tls_server_name as tls_server_name, - store.tls_skip_verify as tls_skip_verify, - store.worker_filter as worker_filter, - store.ct_token as ct_token, -- encrypted - store.token_hmac as token_hmac, - store.token_status as token_status, - store.token_key_id as token_key_id, - store.client_cert as client_cert, - store.ct_client_key as ct_client_key, -- encrypted - store.client_key_id as client_key_id, - null as username_attribute, - null as password_attribute, - null as domain_attribute, - null as private_key_attribute, - null as private_key_passphrase_attribute, - 'ssh-signed-cert' as cred_lib_type, -- used to switch on - additional_valid_principals as additional_valid_principals - from credential_vault_ssh_cert_library library - join credential_vault_store_client store - on library.store_id = store.public_id; + with + username_password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_username_password_mapping_ovrd + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_ssh_private_key_mapping_ovrd + ), + username_password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd + ), + password_override (library_id, password_attribute) as ( + select library_id, + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_password_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + null as key_type, + null as key_bits, + null as username, + null as ttl, + null as key_id, + null as critical_options, + null as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + coalesce(upasso.username_attribute, sshpk.username_attribute, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute, po.password_attribute) + as password_attribute, + pd.domain_attribute as domain_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, + 'generic' as cred_lib_type, -- used to switch on + null as additional_valid_principals + from credential_vault_generic_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join username_password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id + left join username_password_domain_override pd + on library.public_id = pd.library_id + left join password_override po + on library.public_id = po.library_id + union + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + null as http_method, + null as http_request_body, + library.credential_type as credential_type, + library.key_type as key_type, + library.key_bits as key_bits, + library.username as username, + library.ttl as ttl, + library.key_id as key_id, + library.critical_options as critical_options, + library.extensions as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + null as username_attribute, + null as password_attribute, + null as domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type, -- used to switch on + additional_valid_principals as additional_valid_principals + from credential_vault_ssh_cert_library library + join credential_vault_store_client store + on library.store_id = store.public_id + union + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + null as http_method, + null as http_request_body, + library.credential_type as credential_type, + null as key_type, + null as key_bits, + null as username, + null as ttl, + null as key_id, + null as critical_options, + null as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + pd.username_attribute as username_attribute, + pd.password_attribute as password_attribute, + pd.domain_attribute as domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ldap' as cred_lib_type, -- used to switch on + null as additional_valid_principals + from credential_vault_ldap_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join username_password_domain_override pd + on library.public_id = pd.library_id; + comment on view credential_vault_library_issue_credentials is 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' 'This view should not be used to retrieve data which will be returned external to boundary.'; - - -- Replaces view created in 98/02_username_password_domain_vault.up.sql - drop view credential_vault_library_list_lookup; - create view credential_vault_library_list_lookup as + -- Replaces view created in 99/01_credential_vault_library_refactor.up.sql + drop view credential_vault_generic_library_list_lookup; + create view credential_vault_generic_library_list_lookup as with username_password_override (library_id, username_attribute, password_attribute) as ( select library_id, nullif(username_attribute, wt_to_sentinel('no override')), nullif(password_attribute, wt_to_sentinel('no override')) - from credential_vault_library_username_password_mapping_override + from credential_vault_generic_library_username_password_mapping_ovrd ), ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( select library_id, nullif(username_attribute, wt_to_sentinel('no override')), nullif(private_key_attribute, wt_to_sentinel('no override')), nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) - from credential_vault_library_ssh_private_key_mapping_override + from credential_vault_generic_library_ssh_private_key_mapping_ovrd ), username_password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( select library_id, nullif(username_attribute, wt_to_sentinel('no override')), nullif(password_attribute, wt_to_sentinel('no override')), nullif(domain_attribute, wt_to_sentinel('no override')) - from credential_vault_library_username_password_domain_mapping_ovrd + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd ), password_override (library_id, password_attribute) as ( select library_id, nullif(password_attribute, wt_to_sentinel('no override')) - from credential_vault_library_password_mapping_override + from credential_vault_generic_library_password_mapping_override ) select library.public_id as public_id, library.store_id as store_id, @@ -207,7 +251,7 @@ begin; sshpk.private_key_attribute as private_key_attribute, sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, pd.domain_attribute as domain_attribute - from credential_vault_library library + from credential_vault_generic_library library left join username_password_override upasso on library.public_id = upasso.library_id left join ssh_private_key_override sshpk @@ -216,8 +260,8 @@ begin; on library.public_id = pd.library_id left join password_override po on library.public_id = po.library_id; - comment on view credential_vault_library_list_lookup is - 'credential_vault_library_list_lookup is a view where each row contains a credential library and any of library''s credential mapping overrides. ' + comment on view credential_vault_generic_library_list_lookup is + 'credential_vault_generic_library_list_lookup is a view where each row contains a credential library and any of library''s credential mapping overrides. ' 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; -commit; +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql b/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql index 2054192b02..63b8f5aae9 100644 --- a/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/98/01_credential_static_username_password_domain_credential.up.sql @@ -153,8 +153,8 @@ begin; comment on view credential_static_username_password_domain_credential_hst_agg is 'credential_static_username_password_domain_credential_hst_aggregate contains the username password credential history data along with its store and purpose data.'; - - -- This constraint is replaced in 99/01_credential_static_password_credential.up.sql + -- This constraint replaces the previous constraint created in 63/01_credential_vault_ssh_cert_library.up.sql + -- This constraint is replaced in 101/01_credential_static_password_credential.up.sql alter table credential_type_enm drop constraint only_predefined_credential_types_allowed; @@ -173,8 +173,8 @@ begin; insert into credential_type_enm (name) values ('username_password_domain'); - --- This function is updated in 99/01_credential_static_password_credential.up.sql. +-- This function replaces the previous function created in 71/14_recording_static_credential.up.sql +-- This function is replaced in 101/01_credential_static_password_credential.up.sql create or replace function insert_recording_static_credentials() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql b/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql index 429a3cd0df..631168782c 100644 --- a/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql +++ b/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql @@ -37,9 +37,8 @@ begin; create trigger delete_credential_vault_library_mapping_override_subtype after delete on credential_vault_library_username_password_domain_mapping_ovrd for each row execute procedure delete_credential_vault_library_mapping_override_subtype(); - --- Replaces view from 78/01_ssh_signed_certs_additional_valid_principals.up.sql --- Replaced in 99/01_credential_vault_library_refactor.up.sql + -- Replaces view from 78/01_ssh_signed_certs_additional_valid_principals.up.sql + -- Replaced in 99/01_credential_vault_library_refactor.up.sql drop view credential_vault_library_issue_credentials; create view credential_vault_library_issue_credentials as with @@ -161,10 +160,9 @@ begin; 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' 'This view should not be used to retrieve data which will be returned external to boundary.'; - --- Replaces view created in 49/01_vault_credentials.up.sql --- Replaced in 99/01_credential_vault_library_refactor.up.sql where this --- view's name changed to credential_vault_generic_library_list_lookup. + -- Replaces view created in 49/01_vault_credentials.up.sql + -- Replaced in 99/01_credential_vault_library_refactor.up.sql where this + -- view's name changed to credential_vault_generic_library_list_lookup. drop view credential_vault_library_list_lookup; create view credential_vault_library_list_lookup as with diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql b/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql index 21077d2712..44fe3ea5dc 100644 --- a/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql @@ -503,6 +503,7 @@ begin; 'This view should not be used to retrieve data which will be returned external to boundary.'; -- Replaces and renames view defined in 98/02_username_password_domain_vault.up.sql. + -- Replaced in 101/02_credential_vault_password_library.up.sql alter view credential_vault_library_list_lookup rename to credential_vault_generic_library_list_lookup; drop view credential_vault_generic_library_list_lookup; diff --git a/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go b/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go index 9e08f331f0..03f56971cd 100644 --- a/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go +++ b/testing/internal/e2e/tests/base/target_tcp_connect_redis_password_test.go @@ -44,7 +44,7 @@ func TestCliTcpTargetConnectRedisPassword(t *testing.T) { ctx, projectId, redisInfo.Port, - target.WithAddress(redisInfo.Hostname), + []target.Option{target.WithAddress(redisInfo.Hostname)}, ) require.NoError(t, err)