From 3e3144117e5a202cec0aad7e735b920795ba9c4d Mon Sep 17 00:00:00 2001 From: Jim Date: Mon, 29 Jun 2020 08:18:57 -0400 Subject: [PATCH] iam and auth integration (#144) --- Makefile | 1 + internal/auth/store/auth_method.pb.go | 269 ++++++ internal/db/db_test/db.go | 48 +- internal/db/db_test/db_test.pb.go | 194 +++- internal/db/id.go | 13 +- internal/db/id_test.go | 45 + internal/db/migrations/postgres.gen.go | 50 +- .../db/migrations/postgres/02_oplog.up.sql | 7 +- .../db/migrations/postgres/03_db.down.sql | 7 +- internal/db/migrations/postgres/03_db.up.sql | 35 +- .../db/migrations/postgres/07_auth.up.sql | 1 + internal/db/option.go | 14 +- internal/db/option_test.go | 15 + internal/db/read_writer.go | 246 ++++- internal/db/read_writer_test.go | 905 ++++++++++++++---- internal/iam/auth_account.go | 72 ++ internal/iam/auth_account_test.go | 103 ++ internal/iam/{public_ids.go => ids.go} | 0 .../iam/{public_ids_test.go => ids_test.go} | 0 internal/iam/options.go | 10 + internal/iam/options_test.go | 13 + internal/iam/repository_scope.go | 2 +- internal/iam/repository_user.go | 186 ++++ internal/iam/repository_user_test.go | 148 +++ internal/iam/resource.go | 4 +- internal/iam/resource_test.go | 2 +- internal/iam/store/auth_account.pb.go | 199 ++++ internal/iam/testing.go | 71 ++ .../storage/auth/store/v1/auth_method.proto | 32 + .../storage/db/db_test/v1/db_test.proto | 43 +- .../storage/iam/store/v1/auth_account.proto | 27 + 31 files changed, 2494 insertions(+), 268 deletions(-) create mode 100644 internal/auth/store/auth_method.pb.go create mode 100644 internal/iam/auth_account.go create mode 100644 internal/iam/auth_account_test.go rename internal/iam/{public_ids.go => ids.go} (100%) rename internal/iam/{public_ids_test.go => ids_test.go} (100%) create mode 100644 internal/iam/store/auth_account.pb.go create mode 100644 internal/proto/local/controller/storage/auth/store/v1/auth_method.proto create mode 100644 internal/proto/local/controller/storage/iam/store/v1/auth_account.proto diff --git a/Makefile b/Makefile index 02e98cea3..7ff005b33 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/iam/store/group.pb.go @protoc-go-inject-tag -input=./internal/db/db_test/db_test.pb.go @protoc-go-inject-tag -input=./internal/host/static/store/static.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/auth_account.pb.go @rm -R ${TMP_DIR} protolint: diff --git a/internal/auth/store/auth_method.pb.go b/internal/auth/store/auth_method.pb.go new file mode 100644 index 000000000..43914bad6 --- /dev/null +++ b/internal/auth/store/auth_method.pb.go @@ -0,0 +1,269 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.24.0 +// protoc v3.12.3 +// source: controller/storage/auth/store/v1/auth_method.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + _ "github.com/hashicorp/watchtower/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type AuthMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: gorm:"primary_key" + AuthAccountId string `protobuf:"bytes,1,opt,name=auth_account_id,json=authAccountId,proto3" json:"auth_account_id,omitempty"` + // @inject_tag: `gorm:"default:null"` + IamScopeId string `protobuf:"bytes,2,opt,name=iam_scope_id,json=iamScopeId,proto3" json:"iam_scope_id,omitempty"` +} + +func (x *AuthMethod) Reset() { + *x = AuthMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthMethod) ProtoMessage() {} + +func (x *AuthMethod) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthMethod.ProtoReflect.Descriptor instead. +func (*AuthMethod) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_store_v1_auth_method_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthMethod) GetAuthAccountId() string { + if x != nil { + return x.AuthAccountId + } + return "" +} + +func (x *AuthMethod) GetIamScopeId() string { + if x != nil { + return x.IamScopeId + } + return "" +} + +type AuthAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: gorm:"primary_key" + AuthAccountId string `protobuf:"bytes,1,opt,name=auth_account_id,json=authAccountId,proto3" json:"auth_account_id,omitempty"` + // @inject_tag: `gorm:"default:null"` + AuthMethodId string `protobuf:"bytes,2,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty"` + // @inject_tag: `gorm:"default:null"` + IamScopeId string `protobuf:"bytes,3,opt,name=iam_scope_id,json=iamScopeId,proto3" json:"iam_scope_id,omitempty"` + // @inject_tag: `gorm:"default:null"` + IamUserId string `protobuf:"bytes,4,opt,name=iam_user_id,json=iamUserId,proto3" json:"iam_user_id,omitempty"` +} + +func (x *AuthAccount) Reset() { + *x = AuthAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAccount) ProtoMessage() {} + +func (x *AuthAccount) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAccount.ProtoReflect.Descriptor instead. +func (*AuthAccount) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_store_v1_auth_method_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthAccount) GetAuthAccountId() string { + if x != nil { + return x.AuthAccountId + } + return "" +} + +func (x *AuthAccount) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *AuthAccount) GetIamScopeId() string { + if x != nil { + return x.IamScopeId + } + return "" +} + +func (x *AuthAccount) GetIamUserId() string { + if x != nil { + return x.IamUserId + } + return "" +} + +var File_controller_storage_auth_store_v1_auth_method_proto protoreflect.FileDescriptor + +var file_controller_storage_auth_store_v1_auth_method_proto_rawDesc = []byte{ + 0x0a, 0x32, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x56, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x61, 0x75, 0x74, 0x68, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x20, 0x0a, + 0x0c, 0x69, 0x61, 0x6d, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x61, 0x6d, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x22, + 0x9d, 0x01, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x26, 0x0a, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x20, 0x0a, + 0x0c, 0x69, 0x61, 0x6d, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x61, 0x6d, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, + 0x1e, 0x0a, 0x0b, 0x69, 0x61, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x61, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x42, + 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, + 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_auth_store_v1_auth_method_proto_rawDescOnce sync.Once + file_controller_storage_auth_store_v1_auth_method_proto_rawDescData = file_controller_storage_auth_store_v1_auth_method_proto_rawDesc +) + +func file_controller_storage_auth_store_v1_auth_method_proto_rawDescGZIP() []byte { + file_controller_storage_auth_store_v1_auth_method_proto_rawDescOnce.Do(func() { + file_controller_storage_auth_store_v1_auth_method_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_store_v1_auth_method_proto_rawDescData) + }) + return file_controller_storage_auth_store_v1_auth_method_proto_rawDescData +} + +var file_controller_storage_auth_store_v1_auth_method_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_auth_store_v1_auth_method_proto_goTypes = []interface{}{ + (*AuthMethod)(nil), // 0: controller.storage.auth.store.v1.AuthMethod + (*AuthAccount)(nil), // 1: controller.storage.auth.store.v1.AuthAccount +} +var file_controller_storage_auth_store_v1_auth_method_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_storage_auth_store_v1_auth_method_proto_init() } +func file_controller_storage_auth_store_v1_auth_method_proto_init() { + if File_controller_storage_auth_store_v1_auth_method_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_store_v1_auth_method_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_auth_store_v1_auth_method_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_auth_store_v1_auth_method_proto_goTypes, + DependencyIndexes: file_controller_storage_auth_store_v1_auth_method_proto_depIdxs, + MessageInfos: file_controller_storage_auth_store_v1_auth_method_proto_msgTypes, + }.Build() + File_controller_storage_auth_store_v1_auth_method_proto = out.File + file_controller_storage_auth_store_v1_auth_method_proto_rawDesc = nil + file_controller_storage_auth_store_v1_auth_method_proto_goTypes = nil + file_controller_storage_auth_store_v1_auth_method_proto_depIdxs = nil +} diff --git a/internal/db/db_test/db.go b/internal/db/db_test/db.go index 79097a19c..34cf0489d 100644 --- a/internal/db/db_test/db.go +++ b/internal/db/db_test/db.go @@ -30,7 +30,7 @@ func AllocTestUser() TestUser { } // Clone is useful when you're retrying transactions and you need to send the user several times -func (u *TestUser) Clone() *TestUser { +func (u *TestUser) Clone() interface{} { s := proto.Clone(u.StoreTestUser) return &TestUser{ StoreTestUser: s.(*StoreTestUser), @@ -107,3 +107,49 @@ func (r *TestRental) SetTableName(name string) { r.table = name } } + +type TestScooter struct { + *StoreTestScooter + table string `gorm:"-"` +} + +func NewTestScooter() (*TestScooter, error) { + privateId, err := base62.Random(20) + if err != nil { + return nil, err + } + return &TestScooter{ + StoreTestScooter: &StoreTestScooter{ + PrivateId: privateId, + }, + }, nil +} + +func (t *TestScooter) Clone() interface{} { + s := proto.Clone(t.StoreTestScooter) + return &TestScooter{ + StoreTestScooter: s.(*StoreTestScooter), + } +} +func (t *TestScooter) TableName() string { + if t.table != "" { + return t.table + } + return "db_test_scooter" +} + +func (t *TestScooter) SetTableName(name string) { + if name != "" { + t.table = name + } +} + +type Cloner interface { + Clone() interface{} +} + +type NotIder struct{} + +func (i *NotIder) Clone() interface{} { + return &NotIder{} +} diff --git a/internal/db/db_test/db_test.pb.go b/internal/db/db_test/db_test.pb.go index eec289627..5cb69111b 100644 --- a/internal/db/db_test/db_test.pb.go +++ b/internal/db/db_test/db_test.pb.go @@ -44,10 +44,10 @@ type StoreTestUser struct { // update_time from the RDBMS // @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"` - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the user via an API PublicId string `protobuf:"bytes,4,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty"` // name is the optional friendly name used to - // access the Scope via an API + // access the user via an API // @inject_tag: `gorm:"default:null"` Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` // @inject_tag: `gorm:"default:null"` @@ -160,14 +160,16 @@ type StoreTestCar struct { // update_time from the RDBMS // @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"` - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the car via an API PublicId string `protobuf:"bytes,4,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty"` // name is the optional friendly name used to // access the Scope via an API // @inject_tag: `gorm:"default:null"` - Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` - Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty"` - Mpg int32 `protobuf:"varint,7,opt,name=mpg,proto3" json:"mpg,omitempty"` + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Mpg int32 `protobuf:"varint,7,opt,name=mpg,proto3" json:"mpg,omitempty" gorm:"default:null"` } func (x *StoreTestCar) Reset() { @@ -265,14 +267,16 @@ type StoreTestRental struct { // update_time from the RDBMS // @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"` - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the rental via an API PublicId string `protobuf:"bytes,4,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty"` // name is the optional friendly name used to - // access the Scope via an API + // access the rental via an API // @inject_tag: `gorm:"default:null"` - Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` - UserId uint32 `protobuf:"varint,6,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - CarId uint32 `protobuf:"varint,7,opt,name=car_id,json=carId,proto3" json:"car_id,omitempty"` + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + UserId uint32 `protobuf:"varint,6,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + CarId uint32 `protobuf:"varint,7,opt,name=car_id,json=carId,proto3" json:"car_id,omitempty" gorm:"default:null"` } func (x *StoreTestRental) Reset() { @@ -356,6 +360,104 @@ func (x *StoreTestRental) GetCarId() uint32 { return 0 } +// StoreTestScooter used in the db tests only and provides a gorm resource with +// a private id. +type StoreTestScooter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: gorm:"primary_key" + Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" gorm:"primary_key"` + // create_time from the RDBMS + // @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 from the RDBMS + // @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"` + // private_id is the used to access scooter, but not intended to be available + // via the API + PrivateId string `protobuf:"bytes,4,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty"` + // @inject_tag: `gorm:"default:null"` + Model string `protobuf:"bytes,6,opt,name=model,proto3" json:"model,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Mpg int32 `protobuf:"varint,7,opt,name=mpg,proto3" json:"mpg,omitempty" gorm:"default:null"` +} + +func (x *StoreTestScooter) Reset() { + *x = StoreTestScooter{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_db_db_test_v1_db_test_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StoreTestScooter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoreTestScooter) ProtoMessage() {} + +func (x *StoreTestScooter) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_db_db_test_v1_db_test_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StoreTestScooter.ProtoReflect.Descriptor instead. +func (*StoreTestScooter) Descriptor() ([]byte, []int) { + return file_controller_storage_db_db_test_v1_db_test_proto_rawDescGZIP(), []int{3} +} + +func (x *StoreTestScooter) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *StoreTestScooter) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *StoreTestScooter) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *StoreTestScooter) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *StoreTestScooter) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *StoreTestScooter) GetMpg() int32 { + if x != nil { + return x.Mpg + } + return 0 +} + var File_controller_storage_db_db_test_v1_db_test_proto protoreflect.FileDescriptor var file_controller_storage_db_db_test_v1_db_test_proto_rawDesc = []byte{ @@ -422,11 +524,28 @@ var file_controller_storage_db_db_test_v1_db_test_proto_rawDesc = []byte{ 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x63, 0x61, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x05, 0x63, 0x61, 0x72, 0x49, 0x64, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, - 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x62, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x3b, 0x64, 0x62, - 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x05, 0x63, 0x61, 0x72, 0x49, 0x64, 0x22, 0x83, 0x02, 0x0a, 0x10, 0x53, 0x74, 0x6f, 0x72, 0x65, + 0x54, 0x65, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x6f, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x70, + 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6d, 0x70, 0x67, 0x42, 0x3d, 0x5a, 0x3b, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x62, 0x5f, 0x74, + 0x65, 0x73, 0x74, 0x3b, 0x64, 0x62, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -441,25 +560,28 @@ func file_controller_storage_db_db_test_v1_db_test_proto_rawDescGZIP() []byte { return file_controller_storage_db_db_test_v1_db_test_proto_rawDescData } -var file_controller_storage_db_db_test_v1_db_test_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_db_db_test_v1_db_test_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_storage_db_db_test_v1_db_test_proto_goTypes = []interface{}{ (*StoreTestUser)(nil), // 0: controller.storage.db.db_test.v1.StoreTestUser (*StoreTestCar)(nil), // 1: controller.storage.db.db_test.v1.StoreTestCar (*StoreTestRental)(nil), // 2: controller.storage.db.db_test.v1.StoreTestRental - (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp + (*StoreTestScooter)(nil), // 3: controller.storage.db.db_test.v1.StoreTestScooter + (*timestamp.Timestamp)(nil), // 4: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_db_db_test_v1_db_test_proto_depIdxs = []int32{ - 3, // 0: controller.storage.db.db_test.v1.StoreTestUser.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 1: controller.storage.db.db_test.v1.StoreTestUser.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 2: controller.storage.db.db_test.v1.StoreTestCar.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 3: controller.storage.db.db_test.v1.StoreTestCar.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 4: controller.storage.db.db_test.v1.StoreTestRental.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 5: controller.storage.db.db_test.v1.StoreTestRental.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 4, // 0: controller.storage.db.db_test.v1.StoreTestUser.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 1: controller.storage.db.db_test.v1.StoreTestUser.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 2: controller.storage.db.db_test.v1.StoreTestCar.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 3: controller.storage.db.db_test.v1.StoreTestCar.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 4: controller.storage.db.db_test.v1.StoreTestRental.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 5: controller.storage.db.db_test.v1.StoreTestRental.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 6: controller.storage.db.db_test.v1.StoreTestScooter.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 7: controller.storage.db.db_test.v1.StoreTestScooter.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_storage_db_db_test_v1_db_test_proto_init() } @@ -504,6 +626,18 @@ func file_controller_storage_db_db_test_v1_db_test_proto_init() { return nil } } + file_controller_storage_db_db_test_v1_db_test_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StoreTestScooter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -511,7 +645,7 @@ func file_controller_storage_db_db_test_v1_db_test_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_db_db_test_v1_db_test_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/db/id.go b/internal/db/id.go index a7258f4cf..aed8279a9 100644 --- a/internal/db/id.go +++ b/internal/db/id.go @@ -1,20 +1,27 @@ package db import ( - "errors" "fmt" "github.com/hashicorp/vault/sdk/helper/base62" ) +func NewPrivateId(prefix string) (string, error) { + return newId(prefix) +} + // NewPublicId creates a new public id with the prefix func NewPublicId(prefix string) (string, error) { + return newId(prefix) +} + +func newId(prefix string) (string, error) { if prefix == "" { - return "", errors.New("error no prefix for new public id") + return "", fmt.Errorf("missing prefix %w", ErrInvalidParameter) } publicId, err := base62.Random(10) if err != nil { - return "", fmt.Errorf("unable to generate public id: %w", err) + return "", fmt.Errorf("unable to generate id: %w", err) } return fmt.Sprintf("%s_%s", prefix, publicId), nil } diff --git a/internal/db/id_test.go b/internal/db/id_test.go index 60bcace0f..11313223e 100644 --- a/internal/db/id_test.go +++ b/internal/db/id_test.go @@ -3,6 +3,9 @@ package db import ( "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewPublicId(t *testing.T) { @@ -49,3 +52,45 @@ func TestNewPublicId(t *testing.T) { }) } } + +func TestNewPrivateId(t *testing.T) { + type args struct { + prefix string + } + tests := []struct { + name string + args args + wantErr bool + wantLen int + }{ + { + name: "valid", + args: args{ + prefix: "id", + }, + wantErr: false, + wantLen: 10 + len("id_"), + }, + { + name: "bad-prefix", + args: args{ + prefix: "", + }, + wantErr: true, + wantLen: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewPrivateId(tt.args.prefix) + if tt.wantErr { + assert.Error(err) + return + } + require.NoError(err) + assert.True(strings.HasPrefix(got, tt.args.prefix+"_")) + assert.Equal(tt.wantLen, len(got)) + }) + } +} diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index a3883023e..2c23509f2 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -240,7 +240,6 @@ values ('default', 1), ('iam_scope', 1), ('iam_user', 1), - ('iam_auth_method', 1), ('iam_group', 1), ('iam_group_member_user', 1), ('iam_role', 1), @@ -249,7 +248,11 @@ values ('iam_role_user', 1), ('db_test_user', 1), ('db_test_car', 1), - ('db_test_rental', 1); + ('db_test_rental', 1), + ('db_test_scooter', 1), + ('auth_account', 1); +; + commit; @@ -261,9 +264,10 @@ commit; bytes: []byte(` begin; -drop table db_test_rental; -drop table db_test_car; -drop table db_test_user; +drop table db_test_rental cascade; +drop table db_test_car cascade; +drop table db_test_user cascade; +drop table db_test_scooter cascade; commit; @@ -274,9 +278,10 @@ commit; bytes: []byte(` begin; --- create test tables used in the unit tests for the internal/db package --- these tables (db_test_user, db_test_car, db_test_rental) are not part --- of the watchtower domain model... they are simply used for testing the internal/db package +-- create test tables used in the unit tests for the internal/db package +-- these tables (db_test_user, db_test_car, db_test_rental, db_test_scooter) are +-- not part of the watchtower domain model... they are simply used for testing +-- the internal/db package create table if not exists db_test_user ( id bigint generated always as identity primary key, create_time wt_timestamp, @@ -368,6 +373,34 @@ insert on db_test_rental for each row execute procedure default_create_time(); +create table if not exists db_test_scooter ( + id bigint generated always as identity primary key, + create_time wt_timestamp, + update_time wt_timestamp, + private_id text not null unique, + name text unique, + model text, + mpg smallint +); + +create trigger + update_time_column +before +update on db_test_scooter + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_scooter + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_scooter + for each row execute procedure default_create_time(); + commit; `), @@ -719,6 +752,7 @@ begin; end; $$ language plpgsql; + commit; `), diff --git a/internal/db/migrations/postgres/02_oplog.up.sql b/internal/db/migrations/postgres/02_oplog.up.sql index 6c11dd1d6..dded6bfdb 100644 --- a/internal/db/migrations/postgres/02_oplog.up.sql +++ b/internal/db/migrations/postgres/02_oplog.up.sql @@ -89,7 +89,6 @@ values ('default', 1), ('iam_scope', 1), ('iam_user', 1), - ('iam_auth_method', 1), ('iam_group', 1), ('iam_group_member_user', 1), ('iam_role', 1), @@ -98,7 +97,11 @@ values ('iam_role_user', 1), ('db_test_user', 1), ('db_test_car', 1), - ('db_test_rental', 1); + ('db_test_rental', 1), + ('db_test_scooter', 1), + ('auth_account', 1); +; + commit; diff --git a/internal/db/migrations/postgres/03_db.down.sql b/internal/db/migrations/postgres/03_db.down.sql index 7eab5ab88..4c8371967 100644 --- a/internal/db/migrations/postgres/03_db.down.sql +++ b/internal/db/migrations/postgres/03_db.down.sql @@ -1,7 +1,8 @@ begin; -drop table db_test_rental; -drop table db_test_car; -drop table db_test_user; +drop table db_test_rental cascade; +drop table db_test_car cascade; +drop table db_test_user cascade; +drop table db_test_scooter cascade; commit; diff --git a/internal/db/migrations/postgres/03_db.up.sql b/internal/db/migrations/postgres/03_db.up.sql index c80b900a0..57acbc01e 100644 --- a/internal/db/migrations/postgres/03_db.up.sql +++ b/internal/db/migrations/postgres/03_db.up.sql @@ -1,8 +1,9 @@ begin; --- create test tables used in the unit tests for the internal/db package --- these tables (db_test_user, db_test_car, db_test_rental) are not part --- of the watchtower domain model... they are simply used for testing the internal/db package +-- create test tables used in the unit tests for the internal/db package +-- these tables (db_test_user, db_test_car, db_test_rental, db_test_scooter) are +-- not part of the watchtower domain model... they are simply used for testing +-- the internal/db package create table if not exists db_test_user ( id bigint generated always as identity primary key, create_time wt_timestamp, @@ -94,4 +95,32 @@ insert on db_test_rental for each row execute procedure default_create_time(); +create table if not exists db_test_scooter ( + id bigint generated always as identity primary key, + create_time wt_timestamp, + update_time wt_timestamp, + private_id text not null unique, + name text unique, + model text, + mpg smallint +); + +create trigger + update_time_column +before +update on db_test_scooter + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_scooter + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_scooter + for each row execute procedure default_create_time(); + commit; diff --git a/internal/db/migrations/postgres/07_auth.up.sql b/internal/db/migrations/postgres/07_auth.up.sql index d0bc15dd8..358f9f60e 100644 --- a/internal/db/migrations/postgres/07_auth.up.sql +++ b/internal/db/migrations/postgres/07_auth.up.sql @@ -66,4 +66,5 @@ begin; end; $$ language plpgsql; + commit; diff --git a/internal/db/option.go b/internal/db/option.go index d86f7a446..06cb95745 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -28,6 +28,8 @@ type Options struct { WithFieldMaskPaths []string // WithNullPaths must be accessible from other packages. WithNullPaths []string + + newOplogMsg *oplog.Message // WithVersion must be accessible from other packages WithVersion int } @@ -59,7 +61,8 @@ func WithLookup(enable bool) Option { } } -// WithOplog provides an option to write an oplog entry. +// WithOplog provides an option to write an oplog entry. WithOplog and +// NewOplogMsg cannot be used together. func WithOplog(wrapper wrapping.Wrapper, md oplog.Metadata) Option { return func(o *Options) { o.withOplog = true @@ -70,6 +73,15 @@ func WithOplog(wrapper wrapping.Wrapper, md oplog.Metadata) Option { } } +// NewOplogMsg provides an option to ask for a new in-memory oplog message. The +// new msg will be returned in the provided *oplog.Message parameter. WithOplog +// and NewOplogMsg cannot be used together. +func NewOplogMsg(msg *oplog.Message) Option { + return func(o *Options) { + o.newOplogMsg = msg + } +} + // WithFieldMaskPaths provides an option to provide field mask paths. func WithFieldMaskPaths(paths []string) Option { return func(o *Options) { diff --git a/internal/db/option_test.go b/internal/db/option_test.go index 5a1b3c1c9..0c0eb0ab5 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -94,6 +94,21 @@ func Test_getOpts(t *testing.T) { testOpts.WithLimit = 1 assert.Equal(opts, testOpts) }) + t.Run("NewOplogMsg", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := GetOpts() + testOpts := getDefaultOptions() + testOpts.newOplogMsg = nil + assert.Equal(opts, testOpts) + + msg := oplog.Message{} + // try setting to true + opts = GetOpts(NewOplogMsg(&msg)) + testOpts = getDefaultOptions() + testOpts.newOplogMsg = &msg + assert.Equal(opts, testOpts) + }) t.Run("WithVersion", func(t *testing.T) { assert := assert.New(t) // test default of 0 diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index 3c36c4405..f368bd376 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -9,6 +9,7 @@ import ( "strings" "time" + wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/watchtower/internal/db/common" "github.com/hashicorp/watchtower/internal/oplog" "github.com/hashicorp/watchtower/internal/oplog/store" @@ -28,7 +29,12 @@ type Reader interface { // LookupByName will lookup resource by its friendly name which must be unique LookupByName(ctx context.Context, resource ResourceNamer, opt ...Option) error - // LookupByPublicId will lookup resource by its public_id which must be unique + // LookupById will lookup a resource by its primary key id, which must be + // unique. The resourceWithIder must implement either ResourcePublicIder or + // ResourcePrivateIder interface. + LookupById(ctx context.Context, resourceWithIder interface{}, opt ...Option) error + + // LookupByPublicId will lookup resource by its public_id which must be unique. LookupByPublicId(ctx context.Context, resource ResourcePublicIder, opt ...Option) error // LookupWhere will lookup and return the first resource using a where clause with parameters @@ -79,6 +85,21 @@ type Writer interface { // DB returns the sql.DB DB() (*sql.DB, error) + + // GetTicket returns an oplog ticket for the aggregate root of "i" which can + // be used to WriteOplogEntryWith for that aggregate root. + GetTicket(i interface{}) (*store.Ticket, error) + + // WriteOplogEntryWith will write an oplog entry with the msgs provided for + // the ticket's aggregateName. No options are currently supported. + WriteOplogEntryWith( + ctx context.Context, + wrapper wrapping.Wrapper, + ticket *store.Ticket, + metadata oplog.Metadata, + msgs []*oplog.Message, + opt ...Option, + ) error } const ( @@ -94,11 +115,18 @@ type RetryInfo struct { // TxHandler defines a handler for a func that writes a transaction for use with DoTx type TxHandler func(Reader, Writer) error -// ResourcePublicIder defines an interface that LookupByPublicId() can use to get the resource's public id +// ResourcePublicIder defines an interface that LookupByPublicId() can use to +// get the resource's public id. type ResourcePublicIder interface { GetPublicId() string } +// ResourcePrivateIder defines an interface that LookupById() can use to get the +// resource's private id. +type ResourcePrivateIder interface { + GetPrivateId() string +} + // ResourceNamer defines an interface that LookupByName() can use to get the resource's friendly name type ResourceNamer interface { GetName() string @@ -169,8 +197,10 @@ func (rw *Db) lookupAfterWrite(ctx context.Context, i interface{}, opt ...Option return errors.New("not a resource with an id") } -// Create an object in the db with options: WithOplog and WithLookup (to force a -// lookup after create). +// Create an object in the db with options: WithOplog, NewOplogMsg and +// WithLookup. WithOplog will write an oplog entry for the create. +// NewOplogMsg will return in-memory oplog message. WithOplog and NewOplogMsg +// cannot be used together. WithLookup with to force a lookup after create. func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { if rw.underlying == nil { return fmt.Errorf("create: missing underlying db %w", ErrNilParameter) @@ -180,6 +210,9 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { } opts := GetOpts(opt...) withOplog := opts.withOplog + if withOplog && opts.newOplogMsg != nil { + return fmt.Errorf("create: both WithOplog and NewOplogMsg options have been specified: %w", ErrInvalidParameter) + } if withOplog { // let's validate oplog options before we start writing to the database _, err := validateOplogArgs(i, opts) @@ -199,7 +232,7 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { var ticket *store.Ticket if withOplog { var err error - ticket, err = rw.getTicket(i) + ticket, err = rw.GetTicket(i) if err != nil { return fmt.Errorf("create: unable to get ticket: %w", err) } @@ -212,6 +245,13 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { return err } } + if opts.newOplogMsg != nil { + msg, err := rw.newOplogMessage(ctx, CreateOp, i) + if err != nil { + return fmt.Errorf("create: returning oplog failed %w", err) + } + *opts.newOplogMsg = *msg + } if err := rw.lookupAfterWrite(ctx, i, opt...); err != nil { return fmt.Errorf("create: lookup error %w", err) } @@ -248,7 +288,7 @@ func (rw *Db) CreateItems(ctx context.Context, createItems []interface{}, opt .. if err != nil { return fmt.Errorf("create items: oplog validation failed: %w", err) } - ticket, err = rw.getTicket(createItems[0]) + ticket, err = rw.GetTicket(createItems[0]) if err != nil { return fmt.Errorf("create items: unable to get ticket: %w", err) } @@ -273,11 +313,15 @@ func (rw *Db) CreateItems(ctx context.Context, createItems []interface{}, opt .. // is responsible for the transaction life cycle of the writer and if an // error is returned the caller must decide what to do with the transaction, // which almost always should be to rollback. Update returns the number of -// rows updated. Supported options: WithOplog and WithVersion. If WithVersion -// is used, then the update will include the version number in the update where -// clause, which basically makes the update use optimistic locking and the -// update will only succeed if the existing rows version matches the WithVersion -// option. +// rows updated. +// +// Supported options: WithOplog, NewOplogMsg and WithVersion. +// WithOplog will write an oplog entry for the update. NewOplogMsg +// will return in-memory oplog message. WithOplog and NewOplogMsg cannot be +// used together. If WithVersion is used, then the update will include the +// version number in the update where clause, which basically makes the update +// use optimistic locking and the update will only succeed if the existing rows +// version matches the WithVersion option. func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string, setToNullPaths []string, opt ...Option) (int, error) { if rw.underlying == nil { return NoRowsAffected, fmt.Errorf("update: missing underlying db %w", ErrNilParameter) @@ -288,6 +332,11 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string if len(fieldMaskPaths) == 0 && len(setToNullPaths) == 0 { return NoRowsAffected, errors.New("update: both fieldMaskPaths and setToNullPaths are missing") } + opts := GetOpts(opt...) + withOplog := opts.withOplog + if withOplog && opts.newOplogMsg != nil { + return NoRowsAffected, fmt.Errorf("update: both WithOplog and NewOplogMsg options have been specified: %w", ErrInvalidParameter) + } // we need to filter out some non-updatable fields (like: CreateTime, etc) fieldMaskPaths = filterPaths(fieldMaskPaths) @@ -317,8 +366,6 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string } } - opts := GetOpts(opt...) - withOplog := opts.withOplog if withOplog { // let's validate oplog options before we start writing to the database _, err := validateOplogArgs(i, opts) @@ -334,7 +381,7 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string var ticket *store.Ticket if withOplog { var err error - ticket, err = rw.getTicket(i) + ticket, err = rw.GetTicket(i) if err != nil { return NoRowsAffected, fmt.Errorf("update: unable to get ticket: %w", err) } @@ -356,7 +403,7 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string return NoRowsAffected, fmt.Errorf("update: failed %w", underlying.Error) } rowsUpdated := int(underlying.RowsAffected) - if withOplog && rowsUpdated > 0 { + if rowsUpdated > 0 && (withOplog || opts.newOplogMsg != nil) { // we don't want to change the inbound slices in opts, so we'll make our // own copy to pass to addOplog() oplogFieldMasks := make([]string, len(fieldMaskPaths)) @@ -369,8 +416,17 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string WithFieldMaskPaths: oplogFieldMasks, WithNullPaths: oplogNullPaths, } - if err := rw.addOplog(ctx, UpdateOp, oplogOpts, ticket, i); err != nil { - return rowsUpdated, fmt.Errorf("update: add oplog failed %w", err) + if withOplog { + if err := rw.addOplog(ctx, UpdateOp, oplogOpts, ticket, i); err != nil { + return rowsUpdated, fmt.Errorf("update: add oplog failed %w", err) + } + } + if opts.newOplogMsg != nil { + msg, err := rw.newOplogMessage(ctx, UpdateOp, i, WithFieldMaskPaths(oplogFieldMasks), WithNullPaths(oplogNullPaths)) + if err != nil { + return rowsUpdated, fmt.Errorf("update: returning oplog failed %w", err) + } + *opts.newOplogMsg = *msg } } // we need to force a lookupAfterWrite so the resource returned is correctly initialized @@ -382,9 +438,10 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string return rowsUpdated, nil } -// Delete an object in the db with options: WithOplog (which requires -// WithMetadata, WithWrapper). Delete returns the number of rows deleted and -// any errors. +// Delete an object in the db with options: WithOplog and NewOplogMsg. WithOplog +// will write an oplog entry for the delete. NewOplogMsg will return in-memory +// oplog message. WithOplog and NewOplogMsg cannot be used together. Delete +// returns the number of rows deleted and any errors. func (rw *Db) Delete(ctx context.Context, i interface{}, opt ...Option) (int, error) { if rw.underlying == nil { return NoRowsAffected, fmt.Errorf("delete: missing underlying db %w", ErrNilParameter) @@ -392,14 +449,17 @@ func (rw *Db) Delete(ctx context.Context, i interface{}, opt ...Option) (int, er if isNil(i) { return NoRowsAffected, fmt.Errorf("delete: interface is missing %w", ErrNilParameter) } + opts := GetOpts(opt...) + withOplog := opts.withOplog + if withOplog && opts.newOplogMsg != nil { + return NoRowsAffected, fmt.Errorf("delete: both WithOplog and NewOplogMsg options have been specified: %w", ErrInvalidParameter) + } // This is not a watchtower scope, but rather a gorm Scope: // https://godoc.org/github.com/jinzhu/gorm#DB.NewScope scope := rw.underlying.NewScope(i) if scope.PrimaryKeyZero() { return NoRowsAffected, fmt.Errorf("delete: primary key is not set") } - opts := GetOpts(opt...) - withOplog := opts.withOplog if withOplog { _, err := validateOplogArgs(i, opts) if err != nil { @@ -409,7 +469,7 @@ func (rw *Db) Delete(ctx context.Context, i interface{}, opt ...Option) (int, er var ticket *store.Ticket if withOplog { var err error - ticket, err = rw.getTicket(i) + ticket, err = rw.GetTicket(i) if err != nil { return NoRowsAffected, fmt.Errorf("delete: unable to get ticket: %w", err) } @@ -419,9 +479,18 @@ func (rw *Db) Delete(ctx context.Context, i interface{}, opt ...Option) (int, er return NoRowsAffected, fmt.Errorf("delete: failed %w", underlying.Error) } rowsDeleted := int(underlying.RowsAffected) - if withOplog && rowsDeleted > 0 { - if err := rw.addOplog(ctx, DeleteOp, opts, ticket, i); err != nil { - return rowsDeleted, fmt.Errorf("delete: add oplog failed %w", err) + if rowsDeleted > 0 && (withOplog || opts.newOplogMsg != nil) { + if withOplog { + if err := rw.addOplog(ctx, DeleteOp, opts, ticket, i); err != nil { + return rowsDeleted, fmt.Errorf("delete: add oplog failed %w", err) + } + } + if opts.newOplogMsg != nil { + msg, err := rw.newOplogMessage(ctx, DeleteOp, i) + if err != nil { + return rowsDeleted, fmt.Errorf("delete: returning oplog failed %w", err) + } + *opts.newOplogMsg = *msg } } return rowsDeleted, nil @@ -454,7 +523,7 @@ func (rw *Db) DeleteItems(ctx context.Context, deleteItems []interface{}, opt .. if err != nil { return NoRowsAffected, fmt.Errorf("delete items: oplog validation failed: %w", err) } - ticket, err = rw.getTicket(deleteItems[0]) + ticket, err = rw.GetTicket(deleteItems[0]) if err != nil { return NoRowsAffected, fmt.Errorf("delete items: unable to get ticket: %w", err) } @@ -507,10 +576,16 @@ func (rw *Db) getTicketFor(aggregateName string) (*store.Ticket, error) { } return ticket, nil } -func (rw *Db) getTicket(i interface{}) (*store.Ticket, error) { + +// GetTicket returns an oplog ticket for the aggregate root of "i" which can +// be used to WriteOplogEntryWith for that aggregate root. +func (rw *Db) GetTicket(i interface{}) (*store.Ticket, error) { if rw.underlying == nil { return nil, fmt.Errorf("get ticket: underlying db missing: %w", ErrNilParameter) } + if isNil(i) { + return nil, fmt.Errorf("get ticket: interface is missing %w", ErrNilParameter) + } replayable, ok := i.(oplog.ReplayableMessage) if !ok { return nil, fmt.Errorf("get ticket: not a replayable message %w", ErrInvalidParameter) @@ -643,6 +718,79 @@ func (rw *Db) addOplog(ctx context.Context, opType OpType, opts Options, ticket return nil } +// WriteOplogEntryWith will write an oplog entry with the msgs provided for +// the ticket's aggregateName. No options are currently supported. +func (rw *Db) WriteOplogEntryWith(ctx context.Context, wrapper wrapping.Wrapper, ticket *store.Ticket, metadata oplog.Metadata, msgs []*oplog.Message, opt ...Option) error { + if wrapper == nil { + return fmt.Errorf("write oplog: wrapper is unset %w", ErrNilParameter) + } + if ticket == nil { + return fmt.Errorf("write oplog: ticket is unset %w", ErrNilParameter) + } + if len(msgs) == 0 { + return fmt.Errorf("write oplog: msgs are empty %w", ErrInvalidParameter) + } + if rw.underlying == nil { + return fmt.Errorf("write oplog: underlying is unset %w", ErrNilParameter) + } + if metadata == nil { + return fmt.Errorf("write oplog: metadata is unset %w", ErrNilParameter) + } + if len(metadata) == 0 { + return fmt.Errorf("write oplog: metadata is empty %w", ErrInvalidParameter) + } + + ticketer, err := oplog.NewGormTicketer(rw.underlying, oplog.WithAggregateNames(true)) + if err != nil { + return fmt.Errorf("write oplog: unable to get Ticketer %w", err) + } + + entry, err := oplog.NewEntry( + ticket.Name, + metadata, + wrapper, + ticketer, + ) + if err != nil { + return fmt.Errorf("write oplog: unable to create oplog entry: %w", err) + } + err = entry.WriteEntryWith( + ctx, + &oplog.GormWriter{Tx: rw.underlying}, + ticket, + msgs..., + ) + if err != nil { + return fmt.Errorf("write oplog: unable to write oplog entry: %w", err) + } + return nil +} + +func (rw *Db) newOplogMessage(ctx context.Context, opType OpType, i interface{}, opt ...Option) (*oplog.Message, error) { + opts := GetOpts(opt...) + replayable, ok := i.(oplog.ReplayableMessage) + if !ok { + return nil, errors.New("error not a replayable interface") + } + msg := oplog.Message{ + Message: i.(proto.Message), + TypeName: replayable.TableName(), + } + switch opType { + case CreateOp: + msg.OpType = oplog.OpType_OP_TYPE_CREATE + case UpdateOp: + msg.OpType = oplog.OpType_OP_TYPE_UPDATE + msg.FieldMaskPaths = opts.WithFieldMaskPaths + msg.SetToNullPaths = opts.WithNullPaths + case DeleteOp: + msg.OpType = oplog.OpType_OP_TYPE_DELETE + default: + return nil, fmt.Errorf("add oplog: operation type %v is not supported", opType) + } + return &msg, nil +} + // DoTx will wrap the Handler func passed within a transaction with retries // you should ensure that any objects written to the db in your TxHandler are retryable, which // means that the object may be sent to the db several times (retried), so things like the primary key must @@ -705,18 +853,20 @@ func (rw *Db) LookupByName(ctx context.Context, resource ResourceNamer, opt ...O return nil } -// LookupByPublicId will lookup resource my its public_id which must be unique -func (rw *Db) LookupByPublicId(ctx context.Context, resource ResourcePublicIder, opt ...Option) error { +// LookupByPublicId will lookup resource by its public_id or private_id, which +// must be unique. Options are ignored. +func (rw *Db) LookupById(ctx context.Context, resourceWithIder interface{}, opt ...Option) error { if rw.underlying == nil { - return errors.New("error underlying db nil for lookup by public id") + return fmt.Errorf("lookup by id: underlying db nil %w", ErrNilParameter) } - if reflect.ValueOf(resource).Kind() != reflect.Ptr { - return errors.New("error interface parameter must to be a pointer for lookup by public id") + if reflect.ValueOf(resourceWithIder).Kind() != reflect.Ptr { + return fmt.Errorf("lookup by id: interface parameter must to be a pointer %w", ErrInvalidParameter) } - if resource.GetPublicId() == "" { - return errors.New("error public id empty string for lookup by public id") + primaryKey, where, err := primaryKeyWhere(resourceWithIder) + if err != nil { + return fmt.Errorf("lookup by id: %w", err) } - if err := rw.underlying.Where("public_id = ?", resource.GetPublicId()).First(resource).Error; err != nil { + if err := rw.underlying.Where(where, primaryKey).First(resourceWithIder).Error; err != nil { if err == gorm.ErrRecordNotFound { return ErrRecordNotFound } @@ -725,6 +875,30 @@ func (rw *Db) LookupByPublicId(ctx context.Context, resource ResourcePublicIder, return nil } +func primaryKeyWhere(resourceWithIder interface{}) (pkey string, w string, e error) { + var primaryKey, where string + switch resourceType := resourceWithIder.(type) { + case ResourcePublicIder: + primaryKey = resourceType.GetPublicId() + where = "public_id = ?" + case ResourcePrivateIder: + primaryKey = resourceType.GetPrivateId() + where = "private_id = ?" + default: + return "", "", fmt.Errorf("unsupported interface type %w", ErrInvalidParameter) + } + if primaryKey == "" { + return "", "", fmt.Errorf("primary key unset %w", ErrInvalidParameter) + } + return primaryKey, where, nil +} + +// LookupByPublicId will lookup resource by its public_id, which must be unique. +// Options are ignored. +func (rw *Db) LookupByPublicId(ctx context.Context, resource ResourcePublicIder, opt ...Option) error { + return rw.LookupById(ctx, resource, opt...) +} + // LookupWhere will lookup the first resource using a where clause with parameters (it only returns the first one) func (rw *Db) LookupWhere(ctx context.Context, resource interface{}, where string, args ...interface{}) error { if rw.underlying == nil { diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 58bb41ff6..3ec5d2607 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -16,27 +16,23 @@ import ( "github.com/hashicorp/watchtower/internal/db/db_test" "github.com/hashicorp/watchtower/internal/db/timestamp" "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/oplog/store" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestDb_Update(t *testing.T) { cleanup, db, _ := TestSetup(t, "postgres") now := ×tamp.Timestamp{Timestamp: ptypes.TimestampNow()} publicId, err := NewPublicId("testuser") - if err != nil { - t.Error(err) - } + require.NoError(t, err) defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } - }() - defer func() { - if err := db.Close(); err != nil { - t.Error(err) - } + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) }() id := testId(t) type args struct { @@ -227,7 +223,7 @@ func TestDb_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert := assert.New(t) + assert, require := assert.New(t), require.New(t) rw := &Db{ underlying: db, } @@ -239,18 +235,18 @@ func TestDb_Update(t *testing.T) { } rowsUpdated, err := rw.Update(context.Background(), tt.args.i, tt.args.fieldMaskPaths, tt.args.setToNullPaths, tt.args.opt...) if tt.wantErr { - assert.Error(err) + require.Error(err) assert.Equal(tt.want, rowsUpdated) assert.Equal(tt.wantErrMsg, err.Error()) return } - assert.NoError(err) + require.NoError(err) assert.Equal(tt.want, rowsUpdated) if tt.want == 0 { return } foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = tt.args.i.PublicId where := "public_id = ?" for _, f := range tt.args.setToNullPaths { @@ -261,7 +257,7 @@ func TestDb_Update(t *testing.T) { where = fmt.Sprintf("%s and %s is null", where, f) } err = rw.LookupWhere(context.Background(), foundUser, where, tt.args.i.PublicId) - assert.NoError(err) + require.NoError(err) assert.Equal(tt.args.i.Id, foundUser.Id) assert.Equal(tt.wantName, foundUser.Name) assert.Equal(tt.wantEmail, foundUser.Email) @@ -285,10 +281,10 @@ func TestDb_Update(t *testing.T) { assert.Equal(0, rowsUpdated) }) t.Run("valid-WithOplog", func(t *testing.T) { - assert := assert.New(t) + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user := testUser(t, db, "foo-"+id, id, id) user.Name = "friendly-" + id @@ -304,21 +300,93 @@ func TestDb_Update(t *testing.T) { "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, }), ) - assert.NoError(err) + require.NoError(err) assert.Equal(1, rowsUpdated) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = user.PublicId err = w.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Name, user.Name) err = TestVerifyOplog(t, &w, user.PublicId, WithOperation(oplog.OpType_OP_TYPE_UPDATE), WithCreateNotBefore(10*time.Second)) + require.NoError(err) + }) + t.Run("both-WithOplog-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(err) + + updateMsg := oplog.Message{} + user.Name = "friendly-" + id + rowsUpdated, err := w.Update( + context.Background(), + user, + []string{"Name"}, nil, + NewOplogMsg(&updateMsg), + WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}}), + ) + require.Error(err) + assert.Equal(0, rowsUpdated) + assert.True(errors.Is(err, ErrInvalidParameter)) + }) + t.Run("valid-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + + ticket, err := w.GetTicket(&db_test.TestUser{}) + require.NoError(err) + + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(err) + + updateMsg := oplog.Message{} + user.Name = "friendly-" + id + rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil, NewOplogMsg(&updateMsg)) + require.NoError(err) + assert.Equal(1, rowsUpdated) + + foundUser, err := db_test.NewTestUser() + require.NoError(err) + foundUser.PublicId = user.PublicId + err = w.LookupByPublicId(context.Background(), foundUser) + require.NoError(err) + assert.Equal(foundUser.Name, user.Name) + + metadata := oplog.Metadata{ + "resource-public-id": []string{user.PublicId}, + // "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, + } + err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &updateMsg}) + require.NoError(err) + + err = TestVerifyOplog(t, &w, user.PublicId, WithOperation(oplog.OpType_OP_TYPE_UNSPECIFIED), WithCreateNotBefore(10*time.Second)) assert.NoError(err) }) t.Run("vet-for-write", func(t *testing.T) { - assert := assert.New(t) + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() assert.NoError(err) @@ -329,23 +397,23 @@ func TestDb_Update(t *testing.T) { Email: id, } err = db.Create(user).Error - assert.NoError(err) + require.NoError(err) user.Name = "friendly-" + id rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil) - assert.NoError(err) + require.NoError(err) assert.Equal(1, rowsUpdated) foundUser := &testUserWithVet{PublicId: user.PublicId} - assert.NoError(err) + require.NoError(err) foundUser.PublicId = user.PublicId err = w.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Name, user.Name) user.PublicId = "not-allowed-by-vet-for-write" rowsUpdated, err = w.Update(context.Background(), user, []string{"PublicId"}, nil) - assert.Error(err) + require.Error(err) assert.Equal(0, rowsUpdated) }) t.Run("nil-tx", func(t *testing.T) { @@ -361,10 +429,10 @@ func TestDb_Update(t *testing.T) { assert.Equal("update: missing underlying db nil parameter", err.Error()) }) t.Run("no-wrapper-WithOplog", func(t *testing.T) { - assert := assert.New(t) + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user := testUser(t, db, "foo-"+id, id, id) user.Name = "friendly-" + id @@ -378,15 +446,15 @@ func TestDb_Update(t *testing.T) { "project": []string{"central-info-systems", "local-info-systems"}, }), ) - assert.Error(err) + require.Error(err) assert.Equal(0, rowsUpdated) assert.Equal("update: oplog validation failed error no wrapper WithOplog: nil parameter", err.Error()) }) t.Run("no-metadata-WithOplog", func(t *testing.T) { - assert := assert.New(t) + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user := testUser(t, db, "foo-"+id, id, id) user.Name = "friendly-" + id rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil, @@ -395,7 +463,7 @@ func TestDb_Update(t *testing.T) { nil, ), ) - assert.Error(err) + require.Error(err) assert.Equal(0, rowsUpdated) assert.Equal("update: oplog validation failed error no metadata for WithOplog: invalid parameter", err.Error()) }) @@ -442,42 +510,43 @@ func TestDb_Create(t *testing.T) { // intentionally not run with t.Parallel so we don't need to use DoTx for the Create tests cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) }() - assert := assert.New(t) - defer db.Close() t.Run("simple", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) ts := ×tamp.Timestamp{Timestamp: ptypes.TimestampNow()} user.CreateTime = ts user.UpdateTime = ts user.Name = "foo-" + id err = w.Create(context.Background(), user) - assert.NoError(err) + require.NoError(err) assert.NotEmpty(user.Id) // make sure the database controlled the timestamp values assert.NotEqual(ts, user.GetCreateTime()) assert.NotEqual(ts, user.GetUpdateTime()) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = user.PublicId err = w.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Id, user.Id) }) t.Run("valid-WithOplog", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = w.Create( context.Background(), @@ -491,22 +560,84 @@ func TestDb_Create(t *testing.T) { }, ), ) - assert.NoError(err) - assert.NotEmpty(user.Id) + require.NoError(err) + require.NotEmpty(user.Id) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = user.PublicId err = w.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Id, user.Id) }) - t.Run("no-wrapper-WithOplog", func(t *testing.T) { + t.Run("both-Oplog-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}}), + ) + require.Error(err) + assert.True(errors.Is(err, ErrInvalidParameter)) + }) + t.Run("valid-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + + ticket, err := w.GetTicket(&db_test.TestUser{}) + require.NoError(err) + + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(err) + + updateMsg := oplog.Message{} + user.Name = "friendly-" + id + rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil, NewOplogMsg(&updateMsg)) + require.NoError(err) + assert.Equal(1, rowsUpdated) + + foundUser, err := db_test.NewTestUser() + require.NoError(err) + foundUser.PublicId = user.PublicId + err = w.LookupByPublicId(context.Background(), foundUser) + require.NoError(err) + assert.Equal(foundUser.Name, user.Name) + + metadata := oplog.Metadata{ + "resource-public-id": []string{user.PublicId}, + // "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, + } + err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &updateMsg}) + require.NoError(err) + + err = TestVerifyOplog(t, &w, user.PublicId, WithOperation(oplog.OpType_OP_TYPE_UNSPECIFIED), WithCreateNotBefore(10*time.Second)) + require.NoError(err) + }) + t.Run("no-wrapper-WithOplog", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) user.Name = "foo-" + id err = w.Create( context.Background(), @@ -520,15 +651,16 @@ func TestDb_Create(t *testing.T) { }, ), ) - assert.Error(err) + require.Error(err) assert.Equal("create: oplog validation failed error no wrapper WithOplog: nil parameter", err.Error()) }) t.Run("no-metadata-WithOplog", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = w.Create( context.Background(), @@ -538,18 +670,19 @@ func TestDb_Create(t *testing.T) { nil, ), ) - assert.Error(err) + require.Error(err) assert.Equal("create: oplog validation failed error no metadata for WithOplog: invalid parameter", err.Error()) }) t.Run("nil-tx", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: nil} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = w.Create(context.Background(), user) - assert.Error(err) + require.Error(err) assert.Equal("create: missing underlying db nil parameter", err.Error()) }) } @@ -558,57 +691,60 @@ func TestDb_LookupByName(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) }() - assert := assert.New(t) - defer db.Close() t.Run("simple", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "fn-" + id err = w.Create(context.Background(), user) - assert.NoError(err) + require.NoError(err) assert.NotEmpty(user.Id) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.Name = "fn-" + id err = w.LookupByName(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Id, user.Id) }) t.Run("tx-nil,", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{} foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.Name = "fn-name" err = w.LookupByName(context.Background(), foundUser) - assert.Error(err) + require.Error(err) assert.Equal("error underlying db nil for lookup by name", err.Error()) }) t.Run("no-friendly-name-set", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) err = w.LookupByName(context.Background(), foundUser) - assert.Error(err) + require.Error(err) assert.Equal("error name empty string for lookup by name", err.Error()) }) t.Run("not-found", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.Name = "fn-" + id err = w.LookupByName(context.Background(), foundUser) - assert.Error(err) + require.Error(err) assert.Equal(ErrRecordNotFound, err) }) } @@ -617,57 +753,59 @@ func TestDb_LookupByPublicId(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) }() - assert := assert.New(t) - defer db.Close() t.Run("simple", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = w.Create(context.Background(), user) - assert.NoError(err) + require.NoError(err) assert.NotEmpty(user.PublicId) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = user.PublicId err = w.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Id, user.Id) }) t.Run("tx-nil,", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{} foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) err = w.LookupByPublicId(context.Background(), foundUser) - assert.Error(err) - assert.Equal("error underlying db nil for lookup by public id", err.Error()) + require.Error(err) + assert.Equal("lookup by id: underlying db nil nil parameter", err.Error()) }) t.Run("no-public-id-set", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} foundUser, err := db_test.NewTestUser() foundUser.PublicId = "" - assert.NoError(err) + require.NoError(err) err = w.LookupByPublicId(context.Background(), foundUser) - assert.Error(err) - assert.Equal("error public id empty string for lookup by public id", err.Error()) + require.Error(err) + assert.Equal("lookup by id: primary key unset invalid parameter", err.Error()) }) t.Run("not-found", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) - + require.NoError(err) foundUser, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) foundUser.PublicId = id err = w.LookupByPublicId(context.Background(), foundUser) - assert.Error(err) + require.Error(err) assert.Equal(ErrRecordNotFound, err) }) } @@ -676,54 +814,57 @@ func TestDb_LookupWhere(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) }() - assert := assert.New(t) - defer db.Close() t.Run("simple", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = w.Create(context.Background(), user) - assert.NoError(err) + require.NoError(err) assert.NotEmpty(user.PublicId) var foundUser db_test.TestUser err = w.LookupWhere(context.Background(), &foundUser, "public_id = ?", user.PublicId) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Id, user.Id) }) t.Run("tx-nil,", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{} var foundUser db_test.TestUser err := w.LookupWhere(context.Background(), &foundUser, "public_id = ?", 1) - assert.Error(err) + require.Error(err) assert.Equal("error underlying db nil for lookup by", err.Error()) }) t.Run("not-found", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) var foundUser db_test.TestUser err = w.LookupWhere(context.Background(), &foundUser, "public_id = ?", id) - assert.Error(err) + require.Error(err) assert.Equal(ErrRecordNotFound, err) assert.True(errors.Is(err, ErrRecordNotFound)) }) t.Run("bad-where", func(t *testing.T) { + require := require.New(t) w := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) var foundUser db_test.TestUser err = w.LookupWhere(context.Background(), &foundUser, "? = ?", id) - assert.Error(err) + require.Error(err) }) } @@ -847,24 +988,26 @@ func TestDb_DB(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + require := require.New(t) + err := cleanup() + require.NoError(err) + err = db.Close() + require.NoError(err) }() - assert := assert.New(t) - defer db.Close() t.Run("valid", func(t *testing.T) { + require := require.New(t) w := Db{underlying: db} d, err := w.DB() - assert.NoError(err) - assert.NotNil(d) + require.NoError(err) + require.NotNil(d) err = d.Ping() - assert.NoError(err) + require.NoError(err) }) t.Run("nil-tx", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: nil} d, err := w.DB() - assert.Error(err) + require.Error(err) assert.Nil(d) assert.Equal("missing underlying db: nil parameter", err.Error()) }) @@ -874,14 +1017,14 @@ func TestDb_DoTx(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + require := require.New(t) + err := cleanup() + require.NoError(err) + err = db.Close() + require.NoError(err) }() - assert := assert.New(t) - defer db.Close() - t.Run("valid-with-10-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 10, ExpBackoff{}, @@ -892,11 +1035,12 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) assert.Equal(8, got.Retries) assert.Equal(9, attempts) // attempted 1 + 8 retries }) t.Run("valid-with-1-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 1, ExpBackoff{}, @@ -907,11 +1051,12 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) assert.Equal(1, got.Retries) assert.Equal(2, attempts) // attempted 1 + 8 retries }) t.Run("valid-with-2-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 3, ExpBackoff{}, @@ -922,11 +1067,12 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) assert.Equal(2, got.Retries) assert.Equal(3, attempts) // attempted 1 + 8 retries }) t.Run("valid-with-4-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 4, ExpBackoff{}, @@ -937,51 +1083,56 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) assert.Equal(3, got.Retries) assert.Equal(4, attempts) // attempted 1 + 8 retries }) t.Run("zero-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 0, ExpBackoff{}, func(Reader, Writer) error { attempts += 1; return nil }) - assert.NoError(err) + require.NoError(err) assert.Equal(RetryInfo{}, got) assert.Equal(1, attempts) }) t.Run("nil-tx", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{nil} attempts := 0 got, err := w.DoTx(context.Background(), 1, ExpBackoff{}, func(Reader, Writer) error { attempts += 1; return nil }) - assert.Error(err) + require.Error(err) assert.Equal(RetryInfo{}, got) assert.Equal("do underlying db is nil", err.Error()) }) t.Run("not-a-retry-err", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} got, err := w.DoTx(context.Background(), 1, ExpBackoff{}, func(Reader, Writer) error { return errors.New("not a retry error") }) - assert.Error(err) + require.Error(err) assert.Equal(RetryInfo{}, got) assert.NotEqual(err, oplog.ErrTicketAlreadyRedeemed) }) t.Run("too-many-retries", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := &Db{underlying: db} attempts := 0 got, err := w.DoTx(context.Background(), 2, ExpBackoff{}, func(Reader, Writer) error { attempts += 1; return oplog.ErrTicketAlreadyRedeemed }) - assert.Error(err) + require.Error(err) assert.Equal(3, got.Retries) assert.Equal("Too many retries: 3 of 3", err.Error()) }) t.Run("updating-good-bad-good", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) rw := Db{underlying: db} id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) user.Name = "foo-" + id err = rw.Create(context.Background(), user) - assert.NoError(err) - assert.NotZero(user.Id) + require.NoError(err) + require.NotZero(user.Id) _, err = rw.DoTx(context.Background(), 10, ExpBackoff{}, func(r Reader, w Writer) error { user.Name = "friendly-" + id @@ -994,17 +1145,17 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) foundUser, err := db_test.NewTestUser() assert.NoError(err) foundUser.PublicId = user.PublicId err = rw.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Name, user.Name) user2, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) _, err = rw.DoTx(context.Background(), 10, ExpBackoff{}, func(_ Reader, w Writer) error { user2.Name = "friendly2-" + id rowsUpdated, err := w.Update(context.Background(), user2, []string{"Name"}, nil) @@ -1016,9 +1167,9 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.Error(err) + require.Error(err) err = rw.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.NotEqual(foundUser.Name, user2.Name) _, err = rw.DoTx(context.Background(), 10, ExpBackoff{}, func(r Reader, w Writer) error { @@ -1032,9 +1183,9 @@ func TestDb_DoTx(t *testing.T) { } return nil }) - assert.NoError(err) + require.NoError(err) err = rw.LookupByPublicId(context.Background(), foundUser) - assert.NoError(err) + require.NoError(err) assert.Equal(foundUser.Name, user.Name) }) } @@ -1042,28 +1193,25 @@ func TestDb_DoTx(t *testing.T) { func TestDb_Delete(t *testing.T) { cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } - }() - defer func() { - if err := db.Close(); err != nil { - t.Error(err) - } + require := require.New(t) + err := cleanup() + require.NoError(err) + err = db.Close() + require.NoError(err) }() newUser := func() *db_test.TestUser { w := &Db{ underlying: db, } u, err := db_test.NewTestUser() - assert.NoError(t, err) + require.NoError(t, err) err = w.Create(context.Background(), u) - assert.NoError(t, err) + require.NoError(t, err) return u } notFoundUser := func() *db_test.TestUser { u, err := db_test.NewTestUser() - assert.NoError(t, err) + require.NoError(t, err) u.Id = 1111111 return u } @@ -1185,7 +1333,7 @@ func TestDb_Delete(t *testing.T) { require.NoError(err) assert.Equal(tt.want, got) - foundUser := tt.args.i.Clone() + foundUser := tt.args.i.Clone().(*db_test.TestUser) foundUser.PublicId = tt.args.i.PublicId err = rw.LookupByPublicId(context.Background(), foundUser) assert.Error(err) @@ -1199,6 +1347,69 @@ func TestDb_Delete(t *testing.T) { assert.Error(err) } }) + t.Run("both-WithOplog-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(err) + + deleteMsg := oplog.Message{} + rowsDeleted, err := w.Delete(context.Background(), user, NewOplogMsg(&deleteMsg), WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}})) + require.Error(err) + assert.Equal(0, rowsDeleted) + assert.True(errors.Is(err, ErrInvalidParameter)) + }) + t.Run("valid-NewOplogMsg", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := Db{underlying: db} + + ticket, err := w.GetTicket(&db_test.TestUser{}) + require.NoError(err) + + id, err := uuid.GenerateUUID() + require.NoError(err) + user, err := db_test.NewTestUser() + require.NoError(err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(err) + + deleteMsg := oplog.Message{} + rowsDeleted, err := w.Delete(context.Background(), user, NewOplogMsg(&deleteMsg)) + require.NoError(err) + assert.Equal(1, rowsDeleted) + + foundUser, err := db_test.NewTestUser() + require.NoError(err) + foundUser.PublicId = user.PublicId + err = w.LookupByPublicId(context.Background(), foundUser) + require.Error(err) + assert.True(errors.Is(err, ErrRecordNotFound)) + + metadata := oplog.Metadata{ + "resource-public-id": []string{user.PublicId}, + } + err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &deleteMsg}) + require.NoError(err) + + err = TestVerifyOplog(t, &w, user.PublicId, WithOperation(oplog.OpType_OP_TYPE_UNSPECIFIED), WithCreateNotBefore(10*time.Second)) + require.NoError(err) + }) } } @@ -1206,33 +1417,34 @@ func TestDb_ScanRows(t *testing.T) { t.Parallel() cleanup, db, _ := TestSetup(t, "postgres") defer func() { - if err := cleanup(); err != nil { - t.Error(err) - } + require := require.New(t) + err := cleanup() + require.NoError(err) + err = db.Close() + require.NoError(err) }() - assert := assert.New(t) - defer db.Close() t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) w := Db{underlying: db} user, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) err = w.Create(context.Background(), user) - assert.NoError(err) + require.NoError(err) assert.NotEmpty(user.Id) tx, err := w.DB() - assert.NoError(err) + require.NoError(err) where := "select * from db_test_user where name in ($1, $2)" rows, err := tx.Query(where, "alice", "bob") - assert.NoError(err) + require.NoError(err) defer func() { err := rows.Close(); assert.NoError(err) }() for rows.Next() { u, err := db_test.NewTestUser() - assert.NoError(err) + require.NoError(err) // scan the row into your Gorm struct err = w.ScanRows(rows, &u) - assert.NoError(err) + require.NoError(err) assert.Equal(user.PublicId, u.PublicId) } }) @@ -1579,10 +1791,10 @@ func TestDb_DeleteItems(t *testing.T) { func testUser(t *testing.T, conn *gorm.DB, name, email, phoneNumber string) *db_test.TestUser { t.Helper() - assert := assert.New(t) + require := require.New(t) publicId, err := base62.Random(20) - assert.NoError(err) + require.NoError(err) u := &db_test.TestUser{ StoreTestUser: &db_test.StoreTestUser{ PublicId: publicId, @@ -1593,7 +1805,7 @@ func testUser(t *testing.T, conn *gorm.DB, name, email, phoneNumber string) *db_ } if conn != nil { err = conn.Create(u).Error - assert.NoError(err) + require.NoError(err) } return u } @@ -1620,12 +1832,361 @@ func testCar(t *testing.T, conn *gorm.DB, name, model string, mpg int32) *db_tes func testId(t *testing.T) string { t.Helper() - assert := assert.New(t) + require := require.New(t) id, err := uuid.GenerateUUID() - assert.NoError(err) + require.NoError(err) return id } +func testScooter(t *testing.T, conn *gorm.DB, model string, mpg int32) *db_test.TestScooter { + t.Helper() + require := require.New(t) + + privateId, err := base62.Random(20) + require.NoError(err) + u := &db_test.TestScooter{ + StoreTestScooter: &db_test.StoreTestScooter{ + PrivateId: privateId, + Model: model, + Mpg: mpg, + }, + } + if conn != nil { + err = conn.Create(u).Error + require.NoError(err) + } + return u +} + +func TestDb_LookupById(t *testing.T) { + t.Parallel() + cleanup, db, _ := TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) + }() + scooter := testScooter(t, db, "", 0) + user := testUser(t, db, "", "", "") + type args struct { + resourceWithIder interface{} + opt []Option + } + tests := []struct { + name string + underlying *gorm.DB + args args + wantErr bool + want proto.Message + wantIsErr error + }{ + { + name: "simple-private-id", + underlying: db, + args: args{ + resourceWithIder: scooter, + }, + wantErr: false, + want: scooter, + }, + { + name: "simple-public-id", + underlying: db, + args: args{ + resourceWithIder: user, + }, + wantErr: false, + want: user, + }, + { + name: "missing-public-id", + underlying: db, + args: args{ + resourceWithIder: &db_test.TestUser{ + StoreTestUser: &db_test.StoreTestUser{}, + }, + }, + wantErr: true, + wantIsErr: ErrInvalidParameter, + }, + { + name: "missing-private-id", + underlying: db, + args: args{ + resourceWithIder: &db_test.TestScooter{ + StoreTestScooter: &db_test.StoreTestScooter{}, + }, + }, + wantErr: true, + wantIsErr: ErrInvalidParameter, + }, + { + name: "not-an-ider", + underlying: db, + args: args{ + resourceWithIder: &db_test.NotIder{}, + }, + wantErr: true, + wantIsErr: ErrInvalidParameter, + }, + { + name: "missing-underlying-db", + underlying: nil, + args: args{ + resourceWithIder: user, + }, + wantErr: true, + wantIsErr: ErrNilParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + rw := &Db{ + underlying: tt.underlying, + } + cloner, ok := tt.args.resourceWithIder.(db_test.Cloner) + require.True(ok) + cp := cloner.Clone() + err := rw.LookupById(context.Background(), cp, tt.args.opt...) + if tt.wantErr { + require.Error(err) + require.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.True(proto.Equal(tt.want, cp.(proto.Message))) + }) + } + t.Run("not-ptr", func(t *testing.T) { + u := testUser(t, db, "", "", "") + rw := &Db{ + underlying: db, + } + err := rw.LookupById(context.Background(), *u) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidParameter)) + }) +} + +func TestDb_GetTicket(t *testing.T) { + cleanup, db, _ := TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) + }() + type notReplayable struct{} + tests := []struct { + name string + underlying *gorm.DB + aggregateType interface{} + wantErr bool + wantErrIs error + }{ + { + name: "simple", + underlying: db, + aggregateType: &db_test.TestUser{}, + wantErr: false, + }, + { + name: "not-replayable", + underlying: db, + aggregateType: ¬Replayable{}, + wantErr: true, + wantErrIs: ErrInvalidParameter, + }, + { + name: "nil-aggregate-type", + underlying: db, + aggregateType: nil, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + { + name: "no-underlying", + underlying: nil, + aggregateType: &db_test.TestUser{}, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + rw := &Db{ + underlying: tt.underlying, + } + got, err := rw.GetTicket(tt.aggregateType) + if tt.wantErr { + require.Error(err) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error type: %s", err.Error()) + } + return + } + require.NoError(err) + assert.NotEmpty(got.Name) + assert.NotEmpty(got.Version) + assert.NotEmpty(got.CreateTime) + assert.NotEmpty(got.UpdateTime) + }) + } +} + +func TestDb_WriteOplogEntryWith(t *testing.T) { + cleanup, db, _ := TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = db.Close() + assert.NoError(t, err) + }() + + w := Db{underlying: db} + + ticket, err := w.GetTicket(&db_test.TestUser{}) + require.NoError(t, err) + + id, err := uuid.GenerateUUID() + assert.NoError(t, err) + user, err := db_test.NewTestUser() + assert.NoError(t, err) + user.Name = "foo-" + id + createMsg := oplog.Message{} + err = w.Create( + context.Background(), + user, + NewOplogMsg(&createMsg), + ) + require.NoError(t, err) + metadata := oplog.Metadata{ + "resource-public-id": []string{user.PublicId}, + } + + type args struct { + wrapper wrapping.Wrapper + ticket *store.Ticket + metadata oplog.Metadata + msgs []*oplog.Message + opt []Option + } + tests := []struct { + name string + underlying *gorm.DB + args args + wantErr bool + wantErrIs error + wantErrContains string + }{ + { + name: "valid", + underlying: db, + args: args{ + wrapper: TestWrapper(t), + ticket: ticket, + metadata: metadata, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: false, + }, + { + name: "valid-multiple", + underlying: db, + args: args{ + wrapper: TestWrapper(t), + ticket: ticket, + metadata: metadata, + msgs: []*oplog.Message{&createMsg, &createMsg}, + }, + wantErr: false, + }, + { + name: "missing-ticket", + underlying: db, + args: args{ + wrapper: TestWrapper(t), + ticket: nil, + metadata: metadata, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + { + name: "missing-db", + underlying: nil, + args: args{ + wrapper: TestWrapper(t), + ticket: ticket, + metadata: metadata, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + { + name: "missing-wrapper", + underlying: db, + args: args{ + wrapper: nil, + ticket: ticket, + metadata: metadata, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + { + name: "nil-metadata", + underlying: db, + args: args{ + wrapper: TestWrapper(t), + ticket: ticket, + metadata: nil, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: true, + wantErrIs: ErrNilParameter, + }, + { + name: "empty-metadata", + underlying: db, + args: args{ + wrapper: TestWrapper(t), + ticket: ticket, + metadata: oplog.Metadata{}, + msgs: []*oplog.Message{&createMsg}, + }, + wantErr: true, + wantErrIs: ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + rw := &Db{ + underlying: tt.underlying, + } + err := rw.WriteOplogEntryWith(context.Background(), tt.args.wrapper, tt.args.ticket, tt.args.metadata, tt.args.msgs, tt.args.opt...) + if tt.wantErr { + require.Error(err) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error %s", err.Error()) + } + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + }) + } +} + func TestClear_InputTypes(t *testing.T) { type Z struct { F string diff --git a/internal/iam/auth_account.go b/internal/iam/auth_account.go new file mode 100644 index 000000000..66876442f --- /dev/null +++ b/internal/iam/auth_account.go @@ -0,0 +1,72 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/types/scope" + "google.golang.org/protobuf/proto" +) + +// AuthAccount is from the auth subsystem and iam is only allowed to: lookup and +// update auth accounts. That's why there is no "new" factory for AuthAccounts. +type AuthAccount struct { + *store.AuthAccount + tableName string `gorm:"-"` +} + +var _ Clonable = (*AuthAccount)(nil) +var _ db.VetForWriter = (*AuthAccount)(nil) +var _ oplog.ReplayableMessage = (*AuthAccount)(nil) + +func allocAuthAccount() AuthAccount { + return AuthAccount{ + AuthAccount: &store.AuthAccount{}, + } +} + +// Clone creates a clone of the auth account. +func (a *AuthAccount) Clone() interface{} { + cp := proto.Clone(a.AuthAccount) + return &AuthAccount{ + AuthAccount: cp.(*store.AuthAccount), + } +} + +// VetForWrite implements db.VetForWrite() interface. +func (a *AuthAccount) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if a.PublicId == "" { + return fmt.Errorf("error public id is empty string for auth account write: %w", db.ErrInvalidParameter) + } + if err := validateScopeForWrite(ctx, r, a, opType, opt...); err != nil { + return err + } + return nil +} + +func (a *AuthAccount) validScopeTypes() []scope.Type { + return []scope.Type{scope.Organization} +} + +// GetScope returns the scope for the auth account. +func (a *AuthAccount) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { + return LookupScope(ctx, r, a) +} + +// TableName returns the tablename to override the default gorm table name. +func (a *AuthAccount) TableName() string { + if a.tableName != "" { + return a.tableName + } + return "auth_account" +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage interface. +func (a *AuthAccount) SetTableName(n string) { + if n != "" { + a.tableName = n + } +} diff --git a/internal/iam/auth_account_test.go b/internal/iam/auth_account_test.go new file mode 100644 index 000000000..7e1523824 --- /dev/null +++ b/internal/iam/auth_account_test.go @@ -0,0 +1,103 @@ +package iam + +import ( + "context" + "testing" + + "github.com/hashicorp/watchtower/internal/db" + dbassert "github.com/hashicorp/watchtower/internal/db/assert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// this is a very limited test. we are not testing the auth account triggers +// and integrity checks (we will leave that to the auth subsystem, since that's +// where they are implemented). we just want to make sure that the iam subsystem +// can update the IamUserId field successfully since that's what the iam system +// relies on. +func Test_AuthAccountUpdate(t *testing.T) { + t.Parallel() + cleanup, conn, _ := db.TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + }() + org, _ := TestScopes(t, conn) + rw := db.New(conn) + t.Run("simple-update", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + u := TestUser(t, conn, org.PublicId) + authMethodPublicId := testAuthMethod(t, conn, org.PublicId) + acct := testAuthAccount(t, conn, org.PublicId, authMethodPublicId, "") + + updateAcct := acct.Clone().(*AuthAccount) + updateAcct.IamUserId = u.PublicId + updatedRows, err := rw.Update(context.Background(), updateAcct, []string{"IamUserId"}, nil) + require.NoError(err) + assert.Equal(1, updatedRows) + + foundAcct := allocAuthAccount() + foundAcct.PublicId = acct.PublicId + err = rw.LookupByPublicId(context.Background(), &foundAcct) + require.NoError(err) + assert.Equal(u.PublicId, foundAcct.IamUserId) + }) +} + +func TestAuthAccount_GetScope(t *testing.T) { + t.Parallel() + cleanup, conn, _ := db.TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + }() + org, _ := TestScopes(t, conn) + + t.Run("valid-org", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + u := TestUser(t, conn, org.PublicId) + authMethodPublicId := testAuthMethod(t, conn, org.PublicId) + acct := testAuthAccount(t, conn, org.PublicId, authMethodPublicId, u.PublicId) + scope, err := acct.GetScope(context.Background(), w) + require.NoError(err) + assert.True(proto.Equal(org, scope)) + }) +} + +func TestAuthAccount_Clone(t *testing.T) { + t.Parallel() + cleanup, conn, _ := db.TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + }() + org, _ := TestScopes(t, conn) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + u := TestUser(t, conn, org.PublicId) + authMethodPublicId := testAuthMethod(t, conn, org.PublicId) + acct := testAuthAccount(t, conn, org.PublicId, authMethodPublicId, u.PublicId) + cp := acct.Clone() + assert.True(proto.Equal(cp.(*AuthAccount).AuthAccount, acct.AuthAccount)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + rw := db.New(conn) + u := TestUser(t, conn, org.PublicId) + authMethodPublicId := testAuthMethod(t, conn, org.PublicId) + acct := testAuthAccount(t, conn, org.PublicId, authMethodPublicId, u.PublicId) + acct2 := testAuthAccount(t, conn, org.PublicId, authMethodPublicId, "") + dbassert := dbassert.New(t, rw) + dbassert.IsNull(acct2, "IamUserId") + cp := acct.Clone() + assert.True(!proto.Equal(cp.(*AuthAccount).AuthAccount, acct2.AuthAccount)) + }) +} diff --git a/internal/iam/public_ids.go b/internal/iam/ids.go similarity index 100% rename from internal/iam/public_ids.go rename to internal/iam/ids.go diff --git a/internal/iam/public_ids_test.go b/internal/iam/ids_test.go similarity index 100% rename from internal/iam/public_ids_test.go rename to internal/iam/ids_test.go diff --git a/internal/iam/options.go b/internal/iam/options.go index 323df5d34..957d5e2d5 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -20,6 +20,7 @@ type options struct { withDescription string withParentId *string withLimit int + withAutoVivify bool } func getDefaultOptions() options { @@ -30,6 +31,7 @@ func getDefaultOptions() options { withName: "", withParentId: nil, withLimit: 0, + withAutoVivify: false, } } @@ -69,3 +71,11 @@ func WithLimit(limit int) Option { o.withLimit = limit } } + +// WithAutoVivify provides an option to enable user auto vivification when +// calling repo.LookupUserWithLogin(). +func WithAutoVivify(enable bool) Option { + return func(o *options) { + o.withAutoVivify = enable + } +} diff --git a/internal/iam/options_test.go b/internal/iam/options_test.go index be9077355..e40c13d99 100644 --- a/internal/iam/options_test.go +++ b/internal/iam/options_test.go @@ -52,4 +52,17 @@ func Test_GetOpts(t *testing.T) { testOpts.withLimit = 1 assert.Equal(opts, testOpts) }) + t.Run("WithAutoVivify", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withAutoVivify = false + assert.Equal(opts, testOpts) + + opts = getOpts(WithAutoVivify(true)) + testOpts = getDefaultOptions() + testOpts.withAutoVivify = true + assert.Equal(opts, testOpts) + }) } diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index 5bd92229f..8f20b9537 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -100,7 +100,7 @@ func (r *Repository) LookupScope(ctx context.Context, withPublicId string, opt . if err == db.ErrRecordNotFound { return nil, nil } - return nil, fmt.Errorf("lookup group: failed %w fo %s", err, withPublicId) + return nil, fmt.Errorf("lookup scope: failed %w fo %s", err, withPublicId) } return &scope, nil } diff --git a/internal/iam/repository_user.go b/internal/iam/repository_user.go index b76f831ae..d269b3397 100644 --- a/internal/iam/repository_user.go +++ b/internal/iam/repository_user.go @@ -2,10 +2,13 @@ package iam import ( "context" + "errors" "fmt" "strings" "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/types/scope" ) // CreateUser will create a user in the repository and return the written user @@ -118,3 +121,186 @@ func (r *Repository) ListUsers(ctx context.Context, withOrganizationId string, o } return users, nil } + +// LookupUserWithLogin will attempt to lookup the user with a matching auth +// account id and return the user if found. If a user is not found and the +// WithAutoVivify() option is true, then a new iam User will be +// created in the scope of the auth account, and associated with the auth +// account. If a new user is auto vivified, then the WithName and +// WithDescription options are supported as well. +func (r *Repository) LookupUserWithLogin(ctx context.Context, withAuthAccountId string, opt ...Option) (*User, error) { + opts := getOpts(opt...) + if withAuthAccountId == "" { + return nil, fmt.Errorf("lookup user with login: missing auth account id %w", db.ErrInvalidParameter) + } + u, err := r.getUserWithAuthAccount(ctx, withAuthAccountId) + if err != nil { + return nil, fmt.Errorf("lookup user with login: %w", err) + } + if u != nil { + return u, nil + } + if !opts.withAutoVivify { + return nil, fmt.Errorf("lookup user with login: user not found for auth account %s: %w", withAuthAccountId, db.ErrRecordNotFound) + } + + acct := allocAuthAccount() + acct.PublicId = withAuthAccountId + err = r.reader.LookupByPublicId(context.Background(), &acct) + if err != nil { + return nil, fmt.Errorf("lookup user with login: unable to lookup auth account %s: %w", withAuthAccountId, err) + } + + metadata := oplog.Metadata{ + "resource-public-id": []string{withAuthAccountId}, + "scope-id": []string{acct.ScopeId}, + "scope-type": []string{scope.Organization.String()}, + "resource-type": []string{"auth-account"}, + } + + // We will create a new user and associate the user with the auth account + // within one retryable transaction using writer.DoTx + var obtainedUser *User + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 2) + ticket, err := w.GetTicket(&acct) + if err != nil { + return err + } + obtainedUser, err = NewUser(acct.ScopeId, opt...) + if err != nil { + return err + } + id, err := newUserId() + if err != nil { + return err + } + var createMsg oplog.Message + obtainedUser.PublicId = id + err = w.Create(ctx, obtainedUser, db.NewOplogMsg(&createMsg)) + if err != nil { + return err + } + msgs = append(msgs, &createMsg) + + var updateMsg oplog.Message + updateAcct := acct.Clone().(*AuthAccount) + updateAcct.IamUserId = id + updatedRows, err := w.Update(ctx, updateAcct, []string{"IamUserId"}, nil, db.NewOplogMsg(&updateMsg)) + if err != nil { + return err + } + if updatedRows != 1 { + return fmt.Errorf("auth account update affected %d rows", updatedRows) + } + msgs = append(msgs, &updateMsg) + if err := w.WriteOplogEntryWith(ctx, r.wrapper, ticket, metadata, msgs); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("lookup user with login: unable to associate user and account: %w", err) + } + return obtainedUser, nil +} + +func (r *Repository) getUserWithAuthAccount(ctx context.Context, withAuthAccountId string, opt ...Option) (*User, error) { + if withAuthAccountId == "" { + return nil, fmt.Errorf("missing auth account id %w", db.ErrInvalidParameter) + } + underlying, err := r.reader.DB() + if err != nil { + return nil, fmt.Errorf("unable to get underlying db for auth account search: %w", err) + } + userTable := func() *User { u := allocUser(); return &u }().TableName() + acctTable := func() *AuthAccount { a := allocAuthAccount(); return &a }().TableName() + + where := fmt.Sprintf(` + select %[1]s.* + from %[1]s + inner join %[2]s + on %[1]s.public_id = %[2]s.iam_user_id + where + %[1]s.scope_id = %[2]s.scope_id and + %[2]s.public_id = $1`, userTable, acctTable) + rows, err := underlying.Query(where, withAuthAccountId) + if err != nil { + return nil, fmt.Errorf("unable to query auth account %s", withAuthAccountId) + } + defer rows.Close() + u := allocUser() + if rows.Next() { + err = r.reader.ScanRows(rows, &u) + if err != nil { + return nil, fmt.Errorf("unable to scan rows for auth account %s: %w", withAuthAccountId, err) + } + } else { + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("unable to get next auth account: %w", err) + } + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("unable to get next row for auth accounts %s: %w", withAuthAccountId, err) + } + return &u, nil +} + +func (r *Repository) validateLoginParameters(ctx context.Context, withScope, withAuthMethodId, withAuthAccountId string) (*AuthAccount, error) { + if withScope == "" { + return nil, fmt.Errorf("missing scope id %w", db.ErrInvalidParameter) + } + if withAuthMethodId == "" { + return nil, fmt.Errorf("missing auth method id %w", db.ErrInvalidParameter) + } + if withAuthAccountId == "" { + return nil, fmt.Errorf("missing auth account id %w", db.ErrInvalidParameter) + + } + ok, err := r.validateAuthMethodId(ctx, withScope, withAuthMethodId) + if err != nil { + return nil, fmt.Errorf("unable to validate auth method %s in scope %s: %w", withAuthMethodId, withScope, err) + } + if !ok { + return nil, fmt.Errorf("auth method id %s in scope %s is not valid: %w", withAuthMethodId, withScope, db.ErrInvalidParameter) + } + acct := allocAuthAccount() + err = r.reader.LookupWhere(context.Background(), &acct, "public_id = ? and auth_method_id = ?", withAuthAccountId, withAuthMethodId) + if err != nil { + if errors.Is(err, db.ErrRecordNotFound) { + return nil, fmt.Errorf("unable to search for auth account %s in auth method %s: %w", withAuthAccountId, withAuthMethodId, db.ErrInvalidParameter) + } + return nil, fmt.Errorf("unable to search for auth account %s in auth method %s: %w", withAuthAccountId, withAuthMethodId, err) + } + return &acct, nil +} + +// TODO (jimlambrt 6/2020) replace the raw query with a Lookup using the auth +// method repo. +func (r *Repository) validateAuthMethodId(ctx context.Context, scopeId, authMethodId string) (bool, error) { + if scopeId == "" { + return false, fmt.Errorf("scope id is unset: %w", db.ErrInvalidParameter) + } + if authMethodId == "" { + return false, fmt.Errorf("auth method id is unset: %w", db.ErrInvalidParameter) + } + const where = `select count(*) from auth_method where public_id = $1 and scope_id = $2` + db, err := r.reader.DB() + if err != nil { + return false, err + } + var cnt int + if err := db.QueryRow(where, authMethodId, scopeId).Scan(&cnt); err != nil { + return false, err + } + if cnt == 0 { + return false, nil + } + return true, nil +} diff --git a/internal/iam/repository_user_test.go b/internal/iam/repository_user_test.go index 816e34952..d016d2947 100644 --- a/internal/iam/repository_user_test.go +++ b/internal/iam/repository_user_test.go @@ -531,3 +531,151 @@ func TestRepository_ListUsers(t *testing.T) { }) } } + +func TestRepository_LookupUserWithLogin(t *testing.T) { + t.Parallel() + cleanup, conn, _ := db.TestSetup(t, "postgres") + defer func() { + err := cleanup() + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + }() + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(t, err) + + id := testId(t) + org, _ := TestScopes(t, conn) + authMethodId := testAuthMethod(t, conn, org.PublicId) + newAuthAcct := testAuthAccount(t, conn, org.PublicId, authMethodId, "") + newAuthAcctWithoutVivify := testAuthAccount(t, conn, org.PublicId, authMethodId, "") + // negativeTestAuthAcct := testAuthAccount(t, conn, org.PublicId, authMethodId, "") + + user := TestUser(t, conn, org.PublicId, WithName("existing-"+id)) + existingAuthAcct := testAuthAccount(t, conn, org.PublicId, authMethodId, user.PublicId) + require.Equal(t, user.PublicId, existingAuthAcct.IamUserId) + + type args struct { + withAuthAccountId string + opt []Option + } + tests := []struct { + name string + args args + wantName string + wantDescription string + wantErr bool + wantErrIs error + wantUser *User + }{ + { + name: "valid", + args: args{ + withAuthAccountId: newAuthAcct.PublicId, + opt: []Option{ + WithAutoVivify(true), + WithName("valid-" + id), + WithDescription("valid-" + id), + }, + }, + wantName: "valid-" + id, + wantDescription: "valid-" + id, + wantErr: false, + }, + { + name: "new-acct-without-vivify", + args: args{ + withAuthAccountId: newAuthAcctWithoutVivify.PublicId, + }, + wantErr: true, + wantErrIs: db.ErrRecordNotFound, + }, + { + name: "missing auth acct id", + args: args{ + withAuthAccountId: "", + }, + wantErr: true, + wantErrIs: db.ErrInvalidParameter, + }, + { + name: "existing-auth-account", + args: args{ + withAuthAccountId: existingAuthAcct.PublicId, + }, + wantErr: false, + wantName: "existing-" + id, + wantUser: user, + }, + { + name: "existing-auth-account-with-vivify", + args: args{ + withAuthAccountId: existingAuthAcct.PublicId, + opt: []Option{ + WithAutoVivify(true), + }, + }, + wantErr: false, + wantName: "existing-" + id, + wantUser: user, + }, + { + name: "bad-auth-account-id", + args: args{ + withAuthAccountId: id, + }, + wantErr: true, + wantErrIs: db.ErrRecordNotFound, + }, + { + name: "bad-auth-account-id-with-vivify", + args: args{ + withAuthAccountId: id, + opt: []Option{ + WithAutoVivify(true), + }, + }, + wantErr: true, + wantErrIs: db.ErrRecordNotFound, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + dbassert := dbassert.New(t, rw) + got, err := repo.LookupUserWithLogin(context.Background(), tt.args.withAuthAccountId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.Nil(got) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error %s", err.Error()) + } + if tt.args.withAuthAccountId != "" && tt.args.withAuthAccountId != id { + // need to assert that userid in auth_account is still null + acct := allocAuthAccount() + acct.PublicId = tt.args.withAuthAccountId + dbassert.IsNull(&acct, "IamUserId") + } + return + } + require.NoError(err) + if tt.wantName != "" { + assert.Equal(tt.wantName, got.Name) + } + if tt.wantDescription != "" { + assert.Equal(tt.wantDescription, got.Description) + } + require.NotEmpty(got.PublicId) + if tt.wantUser != nil { + assert.True(proto.Equal(tt.wantUser.User, got.User)) + } + acct := allocAuthAccount() + acct.PublicId = tt.args.withAuthAccountId + err = rw.LookupByPublicId(context.Background(), &acct) + require.NoError(err) + assert.Equal(got.PublicId, acct.IamUserId) + }) + } +} diff --git a/internal/iam/resource.go b/internal/iam/resource.go index ac910f581..3d6e3de2f 100644 --- a/internal/iam/resource.go +++ b/internal/iam/resource.go @@ -55,11 +55,11 @@ func LookupScope(ctx context.Context, reader db.Reader, resource ResourceWithSco return nil, errors.New("error resource is nil for LookupScope") } if resource.GetPublicId() == "" { - return nil, errors.New("error resource has an unset public id") + return nil, fmt.Errorf("LookupScope: scope id is unset %w", db.ErrInvalidParameter) } if resource.GetScopeId() == "" { // try to retrieve it from db with it's scope id - if err := reader.LookupByPublicId(ctx, resource); err != nil { + if err := reader.LookupById(ctx, resource); err != nil { return nil, fmt.Errorf("unable to get resource by public id: %w", err) } // if it's still not set after getting it from the db... diff --git a/internal/iam/resource_test.go b/internal/iam/resource_test.go index d9c203a16..525e1e470 100644 --- a/internal/iam/resource_test.go +++ b/internal/iam/resource_test.go @@ -53,6 +53,6 @@ func Test_LookupScope(t *testing.T) { user2 := allocUser() s, err = LookupScope(context.Background(), w, &user2) assert.Nil(s) - assert.Equal("error resource has an unset public id", err.Error()) + assert.Equal("LookupScope: scope id is unset invalid parameter", err.Error()) }) } diff --git a/internal/iam/store/auth_account.pb.go b/internal/iam/store/auth_account.pb.go new file mode 100644 index 000000000..c842af230 --- /dev/null +++ b/internal/iam/store/auth_account.pb.go @@ -0,0 +1,199 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.24.0 +// protoc v3.12.3 +// source: controller/storage/iam/store/v1/auth_account.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + _ "github.com/hashicorp/watchtower/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +// AuthAccount is a "shared table" between the iam and auth subsystems. The +// auth system is responsible for creating entries. The iam system is only +// responsible/allowed to update the iam_user_id. +type AuthAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // auth_account_id is primary key for the user account + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // auth_method_id is the auth method associated with this user account. + // @inject_tag: `gorm:"default:null"` + AuthMethodId string `protobuf:"bytes,2,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"default:null"` + // scope_id for the user account + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,4,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // user_id is the iam.user associated with this account + // @inject_tag: `gorm:"default:null"` + IamUserId string `protobuf:"bytes,5,opt,name=iam_user_id,json=iamUserId,proto3" json:"iam_user_id,omitempty" gorm:"default:null"` +} + +func (x *AuthAccount) Reset() { + *x = AuthAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_auth_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAccount) ProtoMessage() {} + +func (x *AuthAccount) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_auth_account_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAccount.ProtoReflect.Descriptor instead. +func (*AuthAccount) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_auth_account_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthAccount) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *AuthAccount) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *AuthAccount) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *AuthAccount) GetIamUserId() string { + if x != nil { + return x.IamUserId + } + return "" +} + +var File_controller_storage_iam_store_v1_auth_account_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_auth_account_proto_rawDesc = []byte{ + 0x0a, 0x32, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8b, 0x01, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x69, 0x61, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x61, 0x6d, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, + 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_auth_account_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_auth_account_proto_rawDescData = file_controller_storage_iam_store_v1_auth_account_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_auth_account_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_auth_account_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_auth_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_auth_account_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_auth_account_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_auth_account_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_auth_account_proto_goTypes = []interface{}{ + (*AuthAccount)(nil), // 0: controller.storage.iam.store.v1.AuthAccount +} +var file_controller_storage_iam_store_v1_auth_account_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_auth_account_proto_init() } +func file_controller_storage_iam_store_v1_auth_account_proto_init() { + if File_controller_storage_iam_store_v1_auth_account_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_iam_store_v1_auth_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_auth_account_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_auth_account_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_auth_account_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_auth_account_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_auth_account_proto = out.File + file_controller_storage_iam_store_v1_auth_account_proto_rawDesc = nil + file_controller_storage_iam_store_v1_auth_account_proto_goTypes = nil + file_controller_storage_iam_store_v1_auth_account_proto_depIdxs = nil +} diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 843aada16..8cc67b21e 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -6,6 +6,8 @@ import ( "github.com/hashicorp/go-uuid" "github.com/hashicorp/watchtower/internal/db" + dbassert "github.com/hashicorp/watchtower/internal/db/assert" + "github.com/hashicorp/watchtower/internal/iam/store" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -139,3 +141,72 @@ func TestGroup(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *Grou require.NotEmpty(grp.PublicId) return grp } + +// testAuthAccount is a temporary test function. TODO - replace with an auth +// subsystem testAuthAccount function. If userId is zero value, then an auth +// account will be created with a null IamUserId +func testAuthAccount(t *testing.T, conn *gorm.DB, scopeId, authMethodId, userId string) *AuthAccount { + const ( + authAccountPrefix = "aa_" + where = `select count(*) from auth_method where public_id = $1 and scope_id = $2` + ) + t.Helper() + rw := db.New(conn) + require := require.New(t) + require.NotNil(conn) + require.NotEmpty(scopeId) + require.NotEmpty(authMethodId) + + if userId != "" { + foundUser := allocUser() + foundUser.PublicId = userId + err := rw.LookupByPublicId(context.Background(), &foundUser) + require.NoError(err) + require.Equal(scopeId, foundUser.ScopeId) + } + + var count int + err := conn.DB().QueryRow(where, authMethodId, scopeId).Scan(&count) + require.NoError(err) + require.Equal(1, count) + + id, err := db.NewPublicId(authAccountPrefix) + require.NoError(err) + + acct := &AuthAccount{ + AuthAccount: &store.AuthAccount{ + PublicId: id, + ScopeId: scopeId, + AuthMethodId: authMethodId, + IamUserId: userId, + }, + } + err = rw.Create(context.Background(), acct) + require.NoError(err) + require.NotEmpty(acct.PublicId) + + if userId == "" { + dbassert := dbassert.New(t, rw) + dbassert.IsNull(acct, "IamUserId") + } + return acct +} + +// testAuthMethod is a temporary test function. TODO - replace with an auth +// subsystem testAuthMethod function. +func testAuthMethod(t *testing.T, conn *gorm.DB, scopeId string) string { + const ( + authMethodPrefix = "am_" + insert = `insert into auth_method (public_id, scope_id) values ($1, $2)` + ) + t.Helper() + require := require.New(t) + require.NotNil(conn) + require.NotEmpty(scopeId) + id, err := db.NewPublicId(authMethodPrefix) + require.NoError(err) + + _, err = conn.DB().Exec(insert, id, scopeId) + require.NoError(err) + return id +} diff --git a/internal/proto/local/controller/storage/auth/store/v1/auth_method.proto b/internal/proto/local/controller/storage/auth/store/v1/auth_method.proto new file mode 100644 index 000000000..b5cf3c303 --- /dev/null +++ b/internal/proto/local/controller/storage/auth/store/v1/auth_method.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package controller.storage.auth.store.v1; +option go_package = "github.com/hashicorp/watchtower/internal/auth/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +// TODO - fill out the comments for these protobufs... at this point, I'm just +// trying to get aligned on the basic data structures so we can keep moving +// forward to the prototype at the end of next week. + +message AuthMethod { + // @inject_tag: gorm:"primary_key" + string auth_account_id = 1; + + // @inject_tag: `gorm:"default:null"` + string iam_scope_id = 2; +} + +message AuthAccount { + // @inject_tag: gorm:"primary_key" + string auth_account_id = 1; + + // @inject_tag: `gorm:"default:null"` + string auth_method_id = 2; + + // @inject_tag: `gorm:"default:null"` + string iam_scope_id = 3; + + // @inject_tag: `gorm:"default:null"` + string iam_user_id = 4; +} diff --git a/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto b/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto index 7f277d018..0d0b763a9 100644 --- a/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto +++ b/internal/proto/local/controller/storage/db/db_test/v1/db_test.proto @@ -21,11 +21,11 @@ message StoreTestUser { // @inject_tag: `gorm:"default:CURRENT_TIMESTAMP"` timestamp.v1.Timestamp update_time = 3; - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the user via an API string public_id = 4; // name is the optional friendly name used to - // access the Scope via an API + // access the user via an API // @inject_tag: `gorm:"default:null"` string name = 5; @@ -38,6 +38,7 @@ message StoreTestUser { // @inject_tag: `gorm:"default:null"` uint32 version = 8; } + // TestCar for gorm test car model message StoreTestCar { // @inject_tag: gorm:"primary_key" @@ -51,7 +52,7 @@ message StoreTestCar { // @inject_tag: `gorm:"default:CURRENT_TIMESTAMP"` timestamp.v1.Timestamp update_time = 3; - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the car via an API string public_id = 4; // name is the optional friendly name used to @@ -59,9 +60,13 @@ message StoreTestCar { // @inject_tag: `gorm:"default:null"` string name = 5; + // @inject_tag: `gorm:"default:null"` string model = 6; + + // @inject_tag: `gorm:"default:null"` int32 mpg = 7; } + // TestRental for gorm test rental model message StoreTestRental { // @inject_tag: gorm:"primary_key" @@ -75,14 +80,42 @@ message StoreTestRental { // @inject_tag: `gorm:"default:CURRENT_TIMESTAMP"` timestamp.v1.Timestamp update_time = 3; - // public_id is the used to access the AssignablScope via an API + // public_id is the used to access the rental via an API string public_id = 4; // name is the optional friendly name used to - // access the Scope via an API + // access the rental via an API // @inject_tag: `gorm:"default:null"` string name = 5; + // @inject_tag: `gorm:"default:null"` uint32 user_id = 6; + + // @inject_tag: `gorm:"default:null"` uint32 car_id = 7; } + +// StoreTestScooter used in the db tests only and provides a gorm resource with +// a private id. +message StoreTestScooter { + // @inject_tag: gorm:"primary_key" + uint32 id = 1; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:CURRENT_TIMESTAMP"` + timestamp.v1.Timestamp create_time = 2; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:CURRENT_TIMESTAMP"` + timestamp.v1.Timestamp update_time = 3; + + // private_id is the used to access scooter, but not intended to be available + // via the API + string private_id = 4; + + // @inject_tag: `gorm:"default:null"` + string model = 6; + + // @inject_tag: `gorm:"default:null"` + int32 mpg = 7; +} \ No newline at end of file diff --git a/internal/proto/local/controller/storage/iam/store/v1/auth_account.proto b/internal/proto/local/controller/storage/iam/store/v1/auth_account.proto new file mode 100644 index 000000000..d203299d2 --- /dev/null +++ b/internal/proto/local/controller/storage/iam/store/v1/auth_account.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package controller.storage.iam.store.v1; +option go_package = "github.com/hashicorp/watchtower/internal/iam/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +// AuthAccount is a "shared table" between the iam and auth subsystems. The +// auth system is responsible for creating entries. The iam system is only +// responsible/allowed to update the iam_user_id. +message AuthAccount { + // auth_account_id is primary key for the user account + // @inject_tag: gorm:"primary_key" + string public_id = 1; + + // auth_method_id is the auth method associated with this user account. + // @inject_tag: `gorm:"default:null"` + string auth_method_id = 2; + + // scope_id for the user account + // @inject_tag: `gorm:"default:null"` + string scope_id = 4; + + // user_id is the iam.user associated with this account + // @inject_tag: `gorm:"default:null"` + string iam_user_id = 5; +} \ No newline at end of file