diff --git a/Makefile b/Makefile index 4e884b97d..b7f3782ff 100644 --- a/Makefile +++ b/Makefile @@ -151,6 +151,10 @@ protobuild: @protoc-go-inject-tag -input=./internal/kms/store/token_key.pb.go @protoc-go-inject-tag -input=./internal/kms/store/session_key.pb.go @protoc-go-inject-tag -input=./internal/kms/store/oidc_key.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/data_key_version_destruction_job.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/data_key_version_destruction_job_run.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/data_key_version_destruction_job_progress.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/data_key_version_destruction_job_run_allowed_table_name.pb.go @protoc-go-inject-tag -input=./internal/server/store/controller.pb.go @protoc-go-inject-tag -input=./internal/server/store/worker.pb.go @protoc-go-inject-tag -input=./internal/server/store/root_certificate.pb.go @@ -169,6 +173,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/gen/controller/api/services/auth_method_service.pb.go @protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go @protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/scopes/scope.pb.go + @protoc-go-inject-tag -input=./internal/gen/controller/api/services/scope_service.pb.go @protoc-go-inject-tag -input=./internal/gen/controller/servers/services/session_service.pb.go @protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/targets/target.pb.go @protoc-go-inject-tag -input=./internal/gen/controller/api/services/target_service.pb.go diff --git a/api/scopes/custom.go b/api/scopes/custom.go new file mode 100644 index 000000000..5463f4494 --- /dev/null +++ b/api/scopes/custom.go @@ -0,0 +1,214 @@ +package scopes + +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/api" +) + +type KeyListResult struct { + Items []*Key + response *api.Response +} + +func (n KeyListResult) GetItems() []*Key { + return n.Items +} + +func (n KeyListResult) GetResponse() *api.Response { + return n.response +} + +type KeysRotateResult struct { + response *api.Response +} + +func (n KeysRotateResult) GetResponse() *api.Response { + return n.response +} + +type KeyVersionDestructionJobListResult struct { + Items []*KeyVersionDestructionJob + response *api.Response +} + +func (n KeyVersionDestructionJobListResult) GetItems() []*KeyVersionDestructionJob { + return n.Items +} + +func (n KeyVersionDestructionJobListResult) GetResponse() *api.Response { + return n.response +} + +type KeyVersionDestructionResult struct { + State string + response *api.Response +} + +func (n KeyVersionDestructionResult) GetResponse() *api.Response { + return n.response +} + +func (c *Client) ListKeys(ctx context.Context, scopeId string, opt ...Option) (*KeyListResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into ListKeys request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "GET", "scopes/"+url.PathEscape(scopeId)+":list-keys", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating ListKeys request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during ListKeys call: %w", err) + } + + target := new(KeyListResult) + apiErr, err := resp.Decode(target) + if err != nil { + return nil, fmt.Errorf("error decoding ListKeys response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) RotateKeys(ctx context.Context, scopeId string, rewrapKeys bool, opt ...Option) (*KeysRotateResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into RotateKeys request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + opts.postMap["scope_id"] = scopeId + opts.postMap["rewrap"] = rewrapKeys + + req, err := c.client.NewRequest(ctx, "POST", "scopes:rotate-keys", opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating RotateKeys request: %w", err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during RotateKeys call: %w", err) + } + + apiErr, err := resp.Decode(nil) + if err != nil { + return nil, fmt.Errorf("error decoding RotateKeys response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + + target := &KeysRotateResult{ + response: resp, + } + return target, nil +} + +func (c *Client) ListKeyVersionDestructionJobs(ctx context.Context, scopeId string, opt ...Option) (*KeyVersionDestructionJobListResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into ListKeyVersionDestructionJobs request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "GET", "scopes/"+url.PathEscape(scopeId)+":list-key-version-destruction-jobs", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating ListKeyVersionDestructionJobs request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during ListKeyVersionDestructionJobs call: %w", err) + } + + target := new(KeyVersionDestructionJobListResult) + apiErr, err := resp.Decode(target) + if err != nil { + return nil, fmt.Errorf("error decoding ListKeyVersionDestructionJobs response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) DestroyKeyVersion(ctx context.Context, scopeId string, keyVersionId string, opt ...Option) (*KeyVersionDestructionResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into DestroyKeyVersion request") + } + if keyVersionId == "" { + return nil, fmt.Errorf("empty keyVersionId value passed into DestroyKeyVersion request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + opts.postMap["scope_id"] = scopeId + opts.postMap["key_version_id"] = keyVersionId + + req, err := c.client.NewRequest(ctx, "POST", "scopes:destroy-key-version", opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating RotateKeys request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during DestroyKeyVersion call: %w", err) + } + + target := new(KeyVersionDestructionResult) + apiErr, err := resp.Decode(target) + if err != nil { + return nil, fmt.Errorf("error decoding DestroyKeyVersion response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} diff --git a/api/scopes/key.gen.go b/api/scopes/key.gen.go new file mode 100644 index 000000000..8d3a75196 --- /dev/null +++ b/api/scopes/key.gen.go @@ -0,0 +1,15 @@ +// Code generated by "make api"; DO NOT EDIT. +package scopes + +import ( + "time" +) + +type Key struct { + Id string `json:"id,omitempty"` + Scope *ScopeInfo `json:"scope,omitempty"` + Purpose string `json:"purpose,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` + Type string `json:"type,omitempty"` + Versions []*KeyVersion `json:"versions,omitempty"` +} diff --git a/api/scopes/key_version.gen.go b/api/scopes/key_version.gen.go new file mode 100644 index 000000000..d0ef1d464 --- /dev/null +++ b/api/scopes/key_version.gen.go @@ -0,0 +1,12 @@ +// Code generated by "make api"; DO NOT EDIT. +package scopes + +import ( + "time" +) + +type KeyVersion struct { + Id string `json:"id,omitempty"` + Version uint32 `json:"version,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` +} diff --git a/api/scopes/key_version_destruction_job.gen.go b/api/scopes/key_version_destruction_job.gen.go new file mode 100644 index 000000000..dc4dc9600 --- /dev/null +++ b/api/scopes/key_version_destruction_job.gen.go @@ -0,0 +1,15 @@ +// Code generated by "make api"; DO NOT EDIT. +package scopes + +import ( + "time" +) + +type KeyVersionDestructionJob struct { + KeyVersionId string `json:"key_version_id,omitempty"` + Scope *ScopeInfo `json:"scope,omitempty"` + Status string `json:"status,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` + CompletedCount int64 `json:"completed_count,string,omitempty"` + TotalCount int64 `json:"total_count,string,omitempty"` +} diff --git a/globals/fields.go b/globals/fields.go index edaf5f902..f140926b3 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -86,4 +86,9 @@ const ( ActiveConnectionCountField = "active_connection_count" ControllerGeneratedActivationToken = "controller_generated_activation_token" ReleaseVersionField = "release_version" + KeyPurposeField = "purpose" + KeyVersionsField = "key_versions" + KeyVersionIdField = "key_version_id" + CompletedCountField = "completed_count" + TotalCountField = "total_count" ) diff --git a/go.mod b/go.mod index 725f41066..fba6bde92 100644 --- a/go.mod +++ b/go.mod @@ -73,10 +73,10 @@ require ( github.com/zalando/go-keyring v0.2.1 go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - golang.org/x/tools v0.1.10 + golang.org/x/tools v0.1.12 google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58 google.golang.org/grpc v1.48.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 @@ -87,15 +87,16 @@ require ( nhooyr.io/websocket v1.8.7 ) -require github.com/hashicorp/go-dbw v0.0.0-20220725170111-b7cb3aa3d628 +require github.com/hashicorp/go-dbw v0.0.0-20220910135738-ed4505749995 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/cenkalti/backoff/v4 v4.1.0 - github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220711120347-32232bae6803 + github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431 github.com/hashicorp/nodeenrollment v0.1.18 github.com/kelseyhightower/envconfig v1.4.0 - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e + golang.org/x/exp v0.0.0-20220921164117-439092de6870 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b ) require ( @@ -184,11 +185,10 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/dburl v0.11.0 // indirect go.uber.org/goleak v1.1.10 // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 2a0ab91a8..0da453bb4 100644 --- a/go.sum +++ b/go.sum @@ -665,8 +665,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-dbw v0.0.0-20220725170111-b7cb3aa3d628 h1:+K6QNSsNVaiSWoNKqs1omUzhIWiKLxGuK6sRK3Ch+84= -github.com/hashicorp/go-dbw v0.0.0-20220725170111-b7cb3aa3d628/go.mod h1:kRCoWMcOV6AHTOYq9ZILAD7xPzd5fU2M1QKJnhmOH6g= +github.com/hashicorp/go-dbw v0.0.0-20220910135738-ed4505749995 h1:+cSg/gVZ1RMFeZbZoQTbNu8pnb30bDQ6jysmKBKDSvg= +github.com/hashicorp/go-dbw v0.0.0-20220910135738-ed4505749995/go.mod h1:+GVM0D4y16TzXQWrVZKv3cjNHF2M7cHEBPaDAON3V6I= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -679,8 +679,8 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220711120347-32232bae6803 h1:PdSXkY+aAn/19A+lorRNXkfu5zQ9eZTBMNyLdHpeW0c= -github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220711120347-32232bae6803/go.mod h1:tTUGkZXcXW2y6A2Z8o2CVxHKSPNgDfUHLBhHCJb0Lts= +github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431 h1:2fmBl291s44mKzvBjF1TfqJgw1GHk97jxkGbalp4TEM= +github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431/go.mod h1:qbLdiZhFd182vRSOlhBV/OarUcJVTOF2WklPwHs8mYQ= github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.2 h1:it114Kjpk79JVh6AfxFMP5oi1gb+RPzcSLiG3zF/J0w= github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.2/go.mod h1:b9mq0bG5xJ10ZcTEQJLZ22O9y71hIt2MGTLqROA/SAM= github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20220722192355-a843f53fa48d h1:mOtPXWIp4cWKNt9S55IuYAdyUgNtCfUAEVIjcXDx59E= @@ -1383,6 +1383,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= +golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1416,8 +1418,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1478,8 +1480,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1508,8 +1510,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1618,8 +1621,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -1715,14 +1718,13 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index e11258254..ad660b226 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -157,6 +157,28 @@ var inputStructs = []*structInfo{ outFile: "plugins/plugin_info.gen.go", skipOptions: true, }, + { + inProto: &scopes.Key{}, + outFile: "scopes/key.gen.go", + skipOptions: true, + }, + { + inProto: &scopes.KeyVersion{}, + outFile: "scopes/key_version.gen.go", + skipOptions: true, + }, + { + inProto: &scopes.KeyVersionDestructionJob{}, + outFile: "scopes/key_version_destruction_job.gen.go", + skipOptions: true, + fieldOverrides: []fieldInfo{ + // int64 fields get marshalled by protobuf as strings, so we have + // to tell the json parser that their json representation is a + // string but they go into Go int64 types. + {Name: "CompletedCount", JsonTags: []string{"string"}}, + {Name: "TotalCount", JsonTags: []string{"string"}}, + }, + }, { inProto: &scopes.Scope{}, outFile: "scopes/scope.gen.go", diff --git a/internal/auth/oidc/repository_auth_method_update.go b/internal/auth/oidc/repository_auth_method_update.go index 4598830d1..bb45b6c9e 100644 --- a/internal/auth/oidc/repository_auth_method_update.go +++ b/internal/auth/oidc/repository_auth_method_update.go @@ -36,6 +36,7 @@ const ( AccountClaimMapsField = "AccountClaimMaps" TokenClaimsField = "TokenClaims" UserinfoClaimsField = "UserinfoClaims" + KeyIdField = "KeyId" ) // UpdateAuthMethod will retrieve the auth method from the repository, @@ -226,10 +227,10 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi // ClientSecret is a bit odd, because it uses the Struct wrapping, we need // to add the encrypted fields to the dbMask or nullFields if strutil.StrListContains(filteredDbMask, ClientSecretField) { - filteredDbMask = append(filteredDbMask, CtClientSecretField, ClientSecretHmacField) + filteredDbMask = append(filteredDbMask, CtClientSecretField, ClientSecretHmacField, KeyIdField) } if strutil.StrListContains(filteredNullFields, ClientSecretField) { - filteredNullFields = append(filteredNullFields, CtClientSecretField, ClientSecretHmacField) + filteredNullFields = append(filteredNullFields, CtClientSecretField, ClientSecretHmacField, KeyIdField) } databaseWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeDatabase) diff --git a/internal/auth/oidc/rewrapping.go b/internal/auth/oidc/rewrapping.go new file mode 100644 index 000000000..2fd8a4b8a --- /dev/null +++ b/internal/auth/oidc/rewrapping.go @@ -0,0 +1,55 @@ +package oidc + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn(defaultAuthMethodTableName, authMethodRewrapFn) +} + +func authMethodRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "oidc.authMethodRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var authMethods []*AuthMethod + // There are indexes on (scope id, ), so we can query on scope and refine via key id. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &authMethods, "scope_id=? and key_id=?", []any{scopeId, dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, am := range authMethods { + if err := am.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt auth method")) + } + if err := am.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt auth method")) + } + if _, err := writer.Update(ctx, am, []string{CtClientSecretField, KeyIdField}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update auth method row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/auth/oidc/rewrapping_test.go b/internal/auth/oidc/rewrapping_test.go new file mode 100644 index 000000000..f3d24b287 --- /dev/null +++ b/internal/auth/oidc/rewrapping_test.go @@ -0,0 +1,121 @@ +package oidc + +import ( + "context" + "crypto/x509" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_authMethodRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "auth_oidc_method" WHERE scope_id=\$1 and key_id=\$2`, + ).WillReturnError(errors.New("Query error")) + err := authMethodRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + convertAlg := func(alg ...Alg) []string { + s := make([]string, 0, len(alg)) + for _, a := range alg { + s = append(s, string(a)) + } + return s + } + + // fetch the current key version + kmsWrapper, err := kmsCache.GetWrapper(ctx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase) + assert.NoError(t, err) + + currentKeyVersion, err := kmsWrapper.KeyId(ctx) + assert.NoError(t, err) + + // perform the setup for the authmethod + algs := []Alg{RS256, ES256} + cbs := TestConvertToUrls(t, "https://www.alice.com/callback")[0] + auds := []string{"alice-rp", "bob-rp"} + cert1, pem1 := testGenerateCA(t, "localhost") + cert2, pem2 := testGenerateCA(t, "localhost") + certs := []*x509.Certificate{cert1, cert2} + pems := []string{pem1, pem2} + am, err := NewAuthMethod( + ctx, + org.PublicId, + "alice-rp", + "alice-secret", WithAudClaims("alice-rp"), + WithAudClaims(auds...), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(cbs), + WithSigningAlgs(algs...), + WithCertificates(certs...), + WithName("alice's restaurant"), + WithDescription("it's a good place to eat"), + WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"display_name": ToNameClaim, "oid": ToSubClaim}), + ) + assert.NoError(t, err) + assert.Equal(t, am.SigningAlgs, convertAlg(algs...)) + assert.Equal(t, am.ApiUrl, cbs.String()) + assert.Equal(t, "https://www.alice.com", am.Issuer) + assert.Equal(t, am.AudClaims, auds) + assert.Equal(t, am.Certificates, pems) + assert.Equal(t, am.OperationalState, string(InactiveState)) + + // CreateAuthMethod to store it, both from oidc repo + repo, err := NewRepository(ctx, rw, rw, kmsCache) + assert.NoError(t, err) + + authMethod, err := repo.CreateAuthMethod(ctx, am) + assert.NoError(t, err) + assert.NotNil(t, authMethod) + assert.Equal(t, authMethod.KeyId, currentKeyVersion) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, org.Scope.GetPublicId())) + assert.NoError(t, authMethodRewrapFn(ctx, authMethod.KeyId, org.Scope.GetPublicId(), rw, rw, kmsCache)) + + // fetch the new key version + kmsWrapper, err = kmsCache.GetWrapper(ctx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase) + assert.NoError(t, err) + newKeyVersion, err := kmsWrapper.KeyId(ctx) + assert.NoError(t, err) + + // fetching the latest auth method itself + ams, err := repo.getAuthMethods(ctx, authMethod.GetPublicId(), []string{}) + assert.NoError(t, err) + got := ams[0] + + // since getAuthMethods automatically decrypts the secret, all we need to do is make sure + // that it's correct and that it uses the newest key version id + assert.NotEmpty(t, got.KeyId) + assert.Equal(t, newKeyVersion, got.KeyId) + assert.Equal(t, "alice-secret", got.ClientSecret) + assert.NotEqual(t, authMethod.CtClientSecret, got.CtClientSecret) + assert.NotEmpty(t, got.ClientSecretHmac) + assert.Equal(t, authMethod.ClientSecretHmac, got.ClientSecretHmac) + }) +} diff --git a/internal/auth/oidc/service_callback_test.go b/internal/auth/oidc/service_callback_test.go index 78919a28e..deb1fc5c5 100644 --- a/internal/auth/oidc/service_callback_test.go +++ b/internal/auth/oidc/service_callback_test.go @@ -406,7 +406,7 @@ func Test_Callback(t *testing.T) { foundAcct, foundOidcAcct := false, false for _, e := range entries { cnt += 1 - e.Cipherer = oplogWrapper + e.Wrapper = oplogWrapper err := e.DecryptData(ctx) require.NoError(err) msgs, err := e.UnmarshalData(ctx, types) diff --git a/internal/auth/password/argon2_test.go b/internal/auth/password/argon2_test.go index 362f3d130..7ca784bd4 100644 --- a/internal/auth/password/argon2_test.go +++ b/internal/auth/password/argon2_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -317,14 +318,18 @@ func TestArgon2Credential_New(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) - wrapper := db.TestWrapper(t) + rootWrapper := db.TestWrapper(t) - o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) auts := TestAuthMethods(t, conn, o.GetPublicId(), 1) aut := auts[0] accts := TestMultipleAccounts(t, conn, aut.PublicId, 5) confs := testArgon2Confs(t, conn, accts[0].AuthMethodId, 1) + kmsCache := kms.TestKms(t, conn, rootWrapper) + wrapper, err := kmsCache.GetWrapper(context.Background(), o.GetPublicId(), 1) + require.NoError(t, err) + type args struct { accountId string password string diff --git a/internal/auth/password/repository_password.go b/internal/auth/password/repository_password.go index 3a0e943b8..fc5c49712 100644 --- a/internal/auth/password/repository_password.go +++ b/internal/auth/password/repository_password.go @@ -81,7 +81,7 @@ func (r *Repository) Authenticate(ctx context.Context, scopeId, authMethodId, lo return acct.Account, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("update credential")) } - fields := []string{"CtSalt", "DerivedKey", "PasswordConfId"} + fields := []string{"CtSalt", "DerivedKey", "PasswordConfId", "KeyId"} metadata := cred.oplog(oplog.OpType_OP_TYPE_UPDATE) _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, diff --git a/internal/auth/password/repository_password_test.go b/internal/auth/password/repository_password_test.go index 567a2dc98..4dfe90bb9 100644 --- a/internal/auth/password/repository_password_test.go +++ b/internal/auth/password/repository_password_test.go @@ -213,6 +213,11 @@ func TestRepository_AuthenticateRehash(t *testing.T) { require.True(ok, "want *Argon2Configuration") assert.NotEqual(origConfId, upArgonConf.PrivateId) + // Change the key used to encrypt the password too, to test + // that it gets updated appropriately + err = kmsCache.RotateKeys(ctx, o.GetPublicId()) + require.NoError(err) + // Authenticate and verify the credential ID has not changed auth2Acct, err := repo.Authenticate(ctx, o.GetPublicId(), authMethodId, loginName, passwd) require.NoError(err) @@ -242,6 +247,7 @@ func TestRepository_AuthenticateRehash(t *testing.T) { assert.NotEqual(origCred.PasswordConfId, auth2Cred.PasswordConfId, "the configuration Id should be different") assert.NotEqual(origCred.Salt, auth2Cred.Salt, "a new salt value should be generated") assert.NotEqual(origCred.DerivedKey, auth2Cred.DerivedKey, "the derived key should be different") + assert.NotEqual(origCred.KeyId, auth2Cred.KeyId) assert.NoError(db.TestVerifyOplog(t, rw, auth2Cred.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) } diff --git a/internal/auth/password/rewrapping.go b/internal/auth/password/rewrapping.go new file mode 100644 index 000000000..427148d10 --- /dev/null +++ b/internal/auth/password/rewrapping.go @@ -0,0 +1,55 @@ +package password + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn("auth_password_argon2_cred", argon2ConfigRewrapFn) +} + +func argon2ConfigRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "password.argon2ConfigRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var credentials []*Argon2Credential + // The only index on this table is on private id and there are no references to private id. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &credentials, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range credentials { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt argon2 config")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt argon2 config")) + } + if _, err := writer.Update(ctx, cred, []string{"CtSalt", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update argon2 config row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/auth/password/rewrapping_test.go b/internal/auth/password/rewrapping_test.go new file mode 100644 index 000000000..e9f758855 --- /dev/null +++ b/internal/auth/password/rewrapping_test.go @@ -0,0 +1,84 @@ +package password + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/auth/password/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_argon2ConfigRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "auth_password_argon2_cred" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := argon2ConfigRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + auts := TestAuthMethods(t, conn, org.GetPublicId(), 1) + aut := auts[0] + acct := TestAccount(t, conn, aut.PublicId, "name") + authMethodId := acct.AuthMethodId + conf := testArgon2Confs(t, conn, authMethodId, 1)[0] + + kmsCache := kms.TestKms(t, conn, wrapper) + wrapper, _ = kmsCache.GetWrapper(context.Background(), org.GetPublicId(), 1) + + // actually store it + cred, err := newArgon2Credential(acct.PublicId, "this is a password", conf) + require.NoError(t, err) + + require.NoError(t, cred.encrypt(context.Background(), wrapper)) + assert.NoError(t, rw.Create(context.Background(), cred)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, org.Scope.GetPublicId())) + assert.NoError(t, argon2ConfigRewrapFn(ctx, cred.KeyId, org.Scope.GetPublicId(), rw, rw, kmsCache)) + + // now we pull the config back from the db, decrypt it with the new key, and ensure things match + got := &Argon2Credential{ + Argon2Credential: &store.Argon2Credential{ + PrivateId: cred.PrivateId, + }, + } + assert.NoError(t, rw.LookupById(ctx, got)) + + // fetch the new key version + kmsWrapper, err := kmsCache.GetWrapper(ctx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersion, err := kmsWrapper.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersion, got.GetKeyId()) + assert.Equal(t, cred.GetSalt(), got.GetSalt()) + assert.NotEqual(t, cred.GetCtSalt(), got.GetCtSalt()) + }) +} diff --git a/internal/authtoken/authtoken_test.go b/internal/authtoken/authtoken_test.go index 7f1df4279..81ca270d1 100644 --- a/internal/authtoken/authtoken_test.go +++ b/internal/authtoken/authtoken_test.go @@ -125,9 +125,11 @@ func TestAuthToken_DbUpdate(t *testing.T) { func TestAuthToken_DbCreate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rootWrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + wrapper, err := kms.GetWrapper(context.Background(), org.GetPublicId(), 1) + require.NoError(t, err) am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] acct := password.TestAccount(t, conn, am.GetPublicId(), "name1") createdAuthToken := TestAuthToken(t, conn, kms, org.GetPublicId()) diff --git a/internal/authtoken/rewrapping.go b/internal/authtoken/rewrapping.go new file mode 100644 index 000000000..50b70e194 --- /dev/null +++ b/internal/authtoken/rewrapping.go @@ -0,0 +1,55 @@ +package authtoken + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn(defaultAuthTokenTableName, authTokenRewrapFn) +} + +func authTokenRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "authtoken.authTokenRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var credentials []*AuthToken + // Indexes exist on public id and token, and the only reference to public id is session, which may not exist for all rows we need. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &credentials, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range credentials { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt auth token")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt auth token")) + } + if _, err := writer.Update(ctx, cred, []string{"CtToken", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update auth token row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/authtoken/rewrapping_test.go b/internal/authtoken/rewrapping_test.go new file mode 100644 index 000000000..129d345dc --- /dev/null +++ b/internal/authtoken/rewrapping_test.go @@ -0,0 +1,67 @@ +package authtoken + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_authTokenRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "auth_token" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := authTokenRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + // TestAuthToken will create and store the token for us + at := TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, org.Scope.GetPublicId())) + assert.NoError(t, authTokenRewrapFn(ctx, at.GetKeyId(), org.Scope.GetPublicId(), rw, rw, kmsCache)) + + // now we pull the authToken back from the db, decrypt it with the new key, and ensure things match + got := allocAuthToken() + got.PublicId = at.GetPublicId() + assert.NoError(t, rw.LookupById(ctx, got)) + + // fetch the new key version + kmsWrapper, err := kmsCache.GetWrapper(ctx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, at.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, at.GetToken(), got.GetToken()) + assert.NotEqual(t, at.GetCtToken(), got.GetCtToken()) + }) +} diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index f09bba006..27a0d473c 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -910,6 +910,26 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "list", }, nil }, + "scopes list-keys": func() (cli.Command, error) { + return &scopescmd.ListKeysCommand{ + Command: base.NewCommand(ui), + }, nil + }, + "scopes rotate-keys": func() (cli.Command, error) { + return &scopescmd.RotateKeysCommand{ + Command: base.NewCommand(ui), + }, nil + }, + "scopes list-key-version-destruction-jobs": func() (cli.Command, error) { + return &scopescmd.ListKeyVersionDestructionJobsCommand{ + Command: base.NewCommand(ui), + }, nil + }, + "scopes destroy-key-version": func() (cli.Command, error) { + return &scopescmd.DestroyKeyVersionCommand{ + Command: base.NewCommand(ui), + }, nil + }, "sessions": func() (cli.Command, error) { return &sessionscmd.Command{ diff --git a/internal/cmd/commands/scopescmd/destroy_key_version.go b/internal/cmd/commands/scopescmd/destroy_key_version.go new file mode 100644 index 000000000..20f89e5de --- /dev/null +++ b/internal/cmd/commands/scopescmd/destroy_key_version.go @@ -0,0 +1,128 @@ +package scopescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*DestroyKeyVersionCommand)(nil) + _ cli.CommandAutocomplete = (*DestroyKeyVersionCommand)(nil) +) + +type DestroyKeyVersionCommand struct { + *base.Command + FlagKeyVersionId string +} + +func (c *DestroyKeyVersionCommand) Synopsis() string { + return wordwrap.WrapString("Destroy a key version within a scope", base.TermWidth) +} + +func (c *DestroyKeyVersionCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary scopes destroy-key-version [args]", + "", + " Destroys a key version in the scope. The key version must not be the currently active key version.", + " This may start an asynchronous job to re-encrypt existing data encrypted with the key version.", + " Use `boundary scopes list-key-version-destruction-jobs` to monitor the progress of this job. Example:", + "", + ` $ boundary scopes destroy-key-version -scope-id global -key-version-id krkv_123456789`, + "", + "", + }) + c.Flags().Help() +} + +func (c *DestroyKeyVersionCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "scope-id", + Target: &c.FlagScopeId, + Usage: "The id of the scope in which the key version exists", + }) + + f.StringVar(&base.StringVar{ + Name: "key-version-id", + Target: &c.FlagKeyVersionId, + Usage: "The id of the key version which should be destroyed", + }) + return set +} + +func (c *DestroyKeyVersionCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *DestroyKeyVersionCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *DestroyKeyVersionCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch { + case c.FlagScopeId == "": + c.PrintCliError(errors.New("Scope ID must be provided via -scope-id")) + return base.CommandUserError + case c.FlagKeyVersionId == "": + c.PrintCliError(errors.New("Key Version ID must be provided via -key-version-id")) + return base.CommandUserError + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + + sClient := scopes.NewClient(client) + result, err := sClient.DestroyKeyVersion(c.Context, c.FlagScopeId, c.FlagKeyVersionId) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when destroying key version") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to destroy key version: %w", err)) + return base.CommandCliError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(result.GetResponse()); !ok { + return base.CommandCliError + } + default: + switch result.State { + case "completed": + c.UI.Output("The key version was successfully destroyed.") + case "pending": + c.UI.Output("A key version destruction job has been created, which will re-encrypt existing data in the background. Use `boundary scopes list-key-version-destruction-jobs` to monitor this job. Once it has completed the key version will have been destroyed.") + default: + c.PrintCliError(fmt.Errorf("unexpected state: %q", result.State)) + return base.CommandApiError + } + } + + return base.CommandSuccess +} diff --git a/internal/cmd/commands/scopescmd/list_key_version_destruction_jobs.go b/internal/cmd/commands/scopescmd/list_key_version_destruction_jobs.go new file mode 100644 index 000000000..a675fecbb --- /dev/null +++ b/internal/cmd/commands/scopescmd/list_key_version_destruction_jobs.go @@ -0,0 +1,156 @@ +package scopescmd + +import ( + "errors" + "fmt" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*ListKeyVersionDestructionJobsCommand)(nil) + _ cli.CommandAutocomplete = (*ListKeyVersionDestructionJobsCommand)(nil) +) + +type ListKeyVersionDestructionJobsCommand struct { + *base.Command +} + +func (c *ListKeyVersionDestructionJobsCommand) Synopsis() string { + return wordwrap.WrapString("List all pending key version destruction jobs within a scope", base.TermWidth) +} + +func (c *ListKeyVersionDestructionJobsCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary scopes list-key-version-destruction-jobs [args]", + "", + " List all pending key version destruction jobs within a scope. A key version", + " destruction job asynchronously re-encrypts any existing data with the latest", + " key version before destroying the key version. Example:", + "", + ` $ boundary scopes list-key-version-destruction-jobs -scope-id global`, + "", + "", + }) + c.Flags().Help() +} + +func (c *ListKeyVersionDestructionJobsCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "scope-id", + Target: &c.FlagScopeId, + Usage: "The id of the scope in which to list key version destruction jobs", + }) + + return set +} + +func (c *ListKeyVersionDestructionJobsCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *ListKeyVersionDestructionJobsCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *ListKeyVersionDestructionJobsCommand) printListTable(items []*scopes.KeyVersionDestructionJob) string { + if len(items) == 0 { + return "No keys version destruction jobs found" + } + output := []string{ + "", + "Key version destruction job information:", + } + for i, item := range items { + if i > 0 { + output = append(output, "") + } + if true { + output = append(output, + fmt.Sprintf(" Key version ID: %s", item.KeyVersionId), + fmt.Sprintf(" Scope ID: %s", item.Scope.Id), + ) + } + if !item.CreatedTime.IsZero() { + output = append(output, + fmt.Sprintf(" Started: %s", item.CreatedTime.Local().Format(time.RFC1123)), + ) + } + if item.Status != "" { + output = append(output, + fmt.Sprintf(" Status: %s", item.Status), + ) + } + if true { + output = append(output, + fmt.Sprintf(" Rows completed: %d", item.CompletedCount), + ) + } + if true { + output = append(output, + fmt.Sprintf(" Total rows: %d", item.TotalCount), + ) + } + } + + return base.WrapForHelpText(output) +} + +func (c *ListKeyVersionDestructionJobsCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch { + case c.FlagScopeId == "": + c.PrintCliError(errors.New("Scope ID must be provided via -scope-id")) + return base.CommandUserError + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + + sClient := scopes.NewClient(client) + result, err := sClient.ListKeyVersionDestructionJobs(c.Context, c.FlagScopeId) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when listing key version destruction jobs") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to list key version destruction jobs: %w", err)) + return base.CommandCliError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItems(result.GetResponse()); !ok { + return base.CommandCliError + } + + default: + c.UI.Output(c.printListTable(result.GetItems())) + } + + return base.CommandSuccess +} diff --git a/internal/cmd/commands/scopescmd/list_keys.go b/internal/cmd/commands/scopescmd/list_keys.go new file mode 100644 index 000000000..29a92c5c1 --- /dev/null +++ b/internal/cmd/commands/scopescmd/list_keys.go @@ -0,0 +1,163 @@ +package scopescmd + +import ( + "errors" + "fmt" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*ListKeysCommand)(nil) + _ cli.CommandAutocomplete = (*ListKeysCommand)(nil) +) + +type ListKeysCommand struct { + *base.Command +} + +func (c *ListKeysCommand) Synopsis() string { + return wordwrap.WrapString("List keys within a scope", base.TermWidth) +} + +func (c *ListKeysCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary scopes list-keys [args]", + "", + " List keys within a scope. Example:", + "", + ` $ boundary scopes list-keys -scope-id global`, + "", + "", + }) + c.Flags().Help() +} + +func (c *ListKeysCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "scope-id", + Target: &c.FlagScopeId, + Usage: "The id of the scope in which to list the keys", + }) + + return set +} + +func (c *ListKeysCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *ListKeysCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *ListKeysCommand) printListTable(items []*scopes.Key) string { + if len(items) == 0 { + return "No keys found" + } + var output []string + output = []string{ + "", + "Key information:", + } + for i, item := range items { + if i > 0 { + output = append(output, "") + } + if true { + output = append(output, + fmt.Sprintf(" ID: %s", item.Id), + fmt.Sprintf(" Scope ID: %s", item.Scope.Id), + ) + } + if item.Type != "" { + output = append(output, + fmt.Sprintf(" Type: %s", item.Type), + ) + } + if item.Purpose != "" { + output = append(output, + fmt.Sprintf(" Purpose: %s", item.Purpose), + ) + } + output = append(output, " Key Versions:") + for _, keyVersion := range item.Versions { + if true { + output = append(output, + fmt.Sprintf(" Version: %d", keyVersion.Version), + ) + } + if keyVersion.Id != "" { + output = append(output, + fmt.Sprintf(" ID: %s", keyVersion.Id), + ) + } + if !keyVersion.CreatedTime.IsZero() { + output = append(output, + fmt.Sprintf(" Created: %s", keyVersion.CreatedTime.Local().Format(time.RFC1123)), + ) + } + } + } + + return base.WrapForHelpText(output) +} + +func (c *ListKeysCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch { + case c.FlagScopeId == "": + c.PrintCliError(errors.New("Scope ID must be provided via -scope-id")) + return base.CommandUserError + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + + sClient := scopes.NewClient(client) + result, err := sClient.ListKeys(c.Context, c.FlagScopeId) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when listing scope keys") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to list scope keys: %w", err)) + return base.CommandCliError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItems(result.GetResponse()); !ok { + return base.CommandCliError + } + + default: + c.UI.Output(c.printListTable(result.GetItems())) + } + + return base.CommandSuccess +} diff --git a/internal/cmd/commands/scopescmd/rotate_keys.go b/internal/cmd/commands/scopescmd/rotate_keys.go new file mode 100644 index 000000000..28c859564 --- /dev/null +++ b/internal/cmd/commands/scopescmd/rotate_keys.go @@ -0,0 +1,117 @@ +package scopescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*RotateKeysCommand)(nil) + _ cli.CommandAutocomplete = (*RotateKeysCommand)(nil) +) + +type RotateKeysCommand struct { + *base.Command + FlagRewrap bool +} + +func (c *RotateKeysCommand) Synopsis() string { + return wordwrap.WrapString("Rotate the keys within a scope", base.TermWidth) +} + +func (c *RotateKeysCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary scopes rotate-keys [args]", + "", + " Deactivates a given scope's current KEK and DEKs and activates a new set of keys for the scope. Example:", + "", + ` $ boundary scopes rotate-keys -scope-id global`, + "", + "", + }) + c.Flags().Help() +} + +func (c *RotateKeysCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "scope-id", + Target: &c.FlagScopeId, + Usage: "The id of the scope in which to rotate keys", + }) + + f.BoolVar((&base.BoolVar{ + Name: "rewrap", + Target: &c.FlagRewrap, + Usage: "Whether or not to re-encrypt DEKs with the new KEK", + })) + + return set +} + +func (c *RotateKeysCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *RotateKeysCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *RotateKeysCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch { + case c.FlagScopeId == "": + c.PrintCliError(errors.New("Scope ID must be provided via -scope-id")) + return base.CommandUserError + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + + sClient := scopes.NewClient(client) + result, err := sClient.RotateKeys(c.Context, c.FlagScopeId, c.FlagRewrap) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when rotating scope keys") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to rotate scope keys: %w", err)) + return base.CommandCliError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(result.GetResponse()); !ok { + return base.CommandCliError + } + + default: + c.UI.Output("The rotate operation completed successfully.") + } + + return base.CommandSuccess +} diff --git a/internal/cmd/commands/sessionscmd/funcs.go b/internal/cmd/commands/sessionscmd/funcs.go index 16a32a50a..c821f07d5 100644 --- a/internal/cmd/commands/sessionscmd/funcs.go +++ b/internal/cmd/commands/sessionscmd/funcs.go @@ -232,9 +232,9 @@ func printItemTable(item *sessions.Session, resp *api.Response) string { } } - var connectionsMaps []map[string]interface{} + var connectionsMaps []map[string]any for _, sc := range item.Connections { - cm := map[string]interface{}{ + cm := map[string]any{ "Client Address": net.JoinHostPort(sc.ClientTcpAddress, strconv.FormatUint(uint64(sc.ClientTcpPort), 10)), "Endpoint Address": net.JoinHostPort(sc.EndpointTcpAddress, strconv.FormatUint(uint64(sc.EndpointTcpPort), 10)), "Bytes Up": sc.BytesUp, diff --git a/internal/credential/static/query.go b/internal/credential/static/query.go new file mode 100644 index 000000000..0da040cff --- /dev/null +++ b/internal/credential/static/query.go @@ -0,0 +1,40 @@ +package static + +const ( + credStaticUsernamePasswordRewrapQuery = ` +select distinct + userpass.public_id, + userpass.password_encrypted, + userpass.key_id +from credential_static_username_password_credential userpass + inner join credential_static_store store + on store.public_id = userpass.store_id +where store.project_id = ? + and userpass.key_id = ?; +` + + credStaticSshPrivKeyRewrapQuery = ` +select distinct + ssh.public_id, + ssh.private_key_encrypted, + ssh.private_key_passphrase_encrypted, + ssh.key_id +from credential_static_ssh_private_key_credential ssh + inner join credential_static_store store + on store.public_id = ssh.store_id +where store.project_id = ? + and ssh.key_id = ?; +` + + credStaticJsonRewrapQuery = ` +select distinct + json.public_id, + json.object_encrypted, + json.key_id +from credential_static_json_credential json + inner join credential_static_store store + on store.public_id = json.store_id +where store.project_id = ? + and json.key_id = ?; +` +) diff --git a/internal/credential/static/repository_credential.go b/internal/credential/static/repository_credential.go index b1d90f14a..d7651b229 100644 --- a/internal/credential/static/repository_credential.go +++ b/internal/credential/static/repository_credential.go @@ -410,7 +410,7 @@ func (r *Repository) UpdateUsernamePasswordCredential(ctx context.Context, } // Set PasswordHmac and CtPassword masks for update. - dbMask = append(dbMask, "PasswordHmac", "CtPassword") + dbMask = append(dbMask, "PasswordHmac", "CtPassword", "KeyId") } } @@ -531,6 +531,7 @@ func (r *Repository) UpdateSshPrivateKeyCredential(ctx context.Context, } } + dbMask = append(dbMask, "KeyId") if strings.EqualFold(privateKeyField, f) { // Set PrivateKeyHmac and PrivateKeyEncrypted masks for update. dbMask = append(dbMask, "PrivateKeyHmac", "PrivateKeyEncrypted") @@ -672,7 +673,7 @@ func (r *Repository) UpdateJsonCredential(ctx context.Context, } // Set ObjectHmac and ObjectEncrypted masks for update. - dbMask = append(dbMask, "ObjectHmac", "ObjectEncrypted") + dbMask = append(dbMask, "ObjectHmac", "ObjectEncrypted", "KeyId") } oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) diff --git a/internal/credential/static/repository_credential_test.go b/internal/credential/static/repository_credential_test.go index b17075575..17a426a0c 100644 --- a/internal/credential/static/repository_credential_test.go +++ b/internal/credential/static/repository_credential_test.go @@ -1342,6 +1342,47 @@ func TestRepository_UpdateUsernamePasswordCredential(t *testing.T) { } } +func TestRepository_UpdatePasswordCredentialKeyUpdate(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + credStore := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + orig, err := repo.CreateUsernamePasswordCredential(ctx, prj.GetPublicId(), &UsernamePasswordCredential{ + UsernamePasswordCredential: &store.UsernamePasswordCredential{ + Username: "user", + Password: []byte("pass"), + StoreId: credStore.PublicId, + }, + }) + require.NoError(err) + + err = kkms.RotateKeys(ctx, prj.GetPublicId()) + require.NoError(err) + + orig.Password = []byte("pass1") // Company policy to change password every 3 months + + got, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), orig, orig.GetVersion(), []string{"Password"}) + require.NoError(err) + + // Validate that the KeyId has changed + assert.NotEqual(orig.KeyId, got.KeyId) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, orig.Password, databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) + require.NoError(err) + assert.Equal([]byte(hm), got.PasswordHmac) +} + func TestRepository_UpdateSshPrivateKeyCredential(t *testing.T) { const testSecondarySshPrivateKeyPem = ` -----BEGIN OPENSSH PRIVATE KEY----- diff --git a/internal/credential/static/rewrapping.go b/internal/credential/static/rewrapping.go new file mode 100644 index 000000000..3cf527d90 --- /dev/null +++ b/internal/credential/static/rewrapping.go @@ -0,0 +1,171 @@ +package static + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn("credential_static_username_password_credential", credStaticUsernamePasswordRewrapFn) + kms.RegisterTableRewrapFn("credential_static_ssh_private_key_credential", credStaticSshPrivKeyRewrapFn) + kms.RegisterTableRewrapFn("credential_static_json_credential", credStaticJsonRewrapFn) +} + +func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) string { + if dataKeyVersionId == "" { + return "missing data key version id" + } + if scopeId == "" { + return "missing scope id" + } + if util.IsNil(reader) { + return "missing database reader" + } + if util.IsNil(writer) { + return "missing database writer" + } + if kmsRepo == nil { + return "missing kms repository" + } + return "" +} + +func credStaticUsernamePasswordRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "static.credStaticUsernamePasswordRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*UsernamePasswordCredential + // Indexes exist on (store_id, etc), so we can query static stores via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, credStaticUsernamePasswordRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := allocUsernamePasswordCredential() + if err := rows.Scan( + &cred.PublicId, + &cred.CtPassword, + &cred.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt username/password credential")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt username/password credential")) + } + if _, err := writer.Update(ctx, cred, []string{"CtPassword", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update username/password credential row with rewrapped fields")) + } + } + return nil +} + +func credStaticSshPrivKeyRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "static.credStaticSshPrivKeyRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*SshPrivateKeyCredential + // Indexes exist on (store_id, etc), so we can query static stores via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, credStaticSshPrivKeyRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := allocSshPrivateKeyCredential() + if err := rows.Scan( + &cred.PublicId, + &cred.PrivateKeyEncrypted, + &cred.PrivateKeyPassphraseEncrypted, + &cred.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt ssh private key")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt ssh private key")) + } + if _, err := writer.Update(ctx, cred, []string{"PrivateKeyEncrypted", "PrivateKeyPassphraseEncrypted", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update ssh private key row with rewrapped fields")) + } + } + return nil +} + +func credStaticJsonRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "static.credStaticJsonRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*JsonCredential + // Indexes exist on (store_id, etc), so we can query static stores via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, credStaticJsonRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := allocJsonCredential() + if err := rows.Scan( + &cred.PublicId, + &cred.ObjectEncrypted, + &cred.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt json credential")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt json credential")) + } + if _, err := writer.Update(ctx, cred, []string{"ObjectEncrypted", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update json credential row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/credential/static/rewrapping_test.go b/internal/credential/static/rewrapping_test.go new file mode 100644 index 000000000..5cc78cc4e --- /dev/null +++ b/internal/credential/static/rewrapping_test.go @@ -0,0 +1,247 @@ +package static + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_credStaticUsernamePasswordRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct userpass\.public_id, userpass\.password_encrypted, userpass\.key_id from credential_static_username_password_credential userpass inner join credential_static_store store on store\.public_id = userpass\.store_id where store\.project_id = \$1 and userpass\.key_id = \$2;`, + ).WillReturnError(errors.New("Query error")) + err := credStaticUsernamePasswordRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + cred, err := NewUsernamePasswordCredential(cs.GetPublicId(), "username", "password") + assert.NoError(t, err) + + cred.PublicId, err = credential.NewUsernamePasswordCredentialId(ctx) + assert.NoError(t, err) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + + assert.NoError(t, cred.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), cred)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credStaticUsernamePasswordRewrapFn(ctx, cred.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocUsernamePasswordCredential() + got.PublicId = cred.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, "password", string(got.GetPassword())) + assert.NotEmpty(t, got.GetPasswordHmac()) + assert.Equal(t, cred.GetPasswordHmac(), got.GetPasswordHmac()) + }) +} + +func TestRewrap_credStaticSshPrivKeyRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct ssh\.public_id, ssh\.private_key_encrypted, ssh\.private_key_passphrase_encrypted, ssh\.key_id from credential_static_ssh_private_key_credential ssh inner join credential_static_store store on store\.public_id = ssh\.store_id where store\.project_id = \$1 and ssh\.key_id = \$2;`, + ).WillReturnError(errors.New("Query error")) + err := credStaticSshPrivKeyRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + // since there are two possible versions (with or without passphrase) we need to make 2 copies of everything, but rewrap only once + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + cs2 := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + cred, err := NewSshPrivateKeyCredential(ctx, cs.GetPublicId(), "username", credential.PrivateKey(TestSshPrivateKeyPem)) + assert.NoError(t, err) + + // we need to assign this one explicitly since the new function (correctly) has some checks on the passphrase actually being correct + cred2 := &SshPrivateKeyCredential{ + SshPrivateKeyCredential: &store.SshPrivateKeyCredential{ + StoreId: cs2.GetPublicId(), + Username: "username", + PrivateKey: credential.PrivateKey(TestSshPrivateKeyPem), + PrivateKeyPassphrase: []byte("passphrase"), + }, + } + + cred.PublicId, err = credential.NewSshPrivateKeyCredentialId(ctx) + assert.NoError(t, err) + cred2.PublicId, err = credential.NewSshPrivateKeyCredentialId(ctx) + assert.NoError(t, err) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + + assert.NoError(t, cred.encrypt(ctx, kmsWrapper)) + assert.NoError(t, cred2.encrypt(ctx, kmsWrapper)) + + // create them in the db + assert.NoError(t, rw.Create(context.Background(), cred)) + assert.NoError(t, rw.Create(context.Background(), cred2)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credStaticSshPrivKeyRewrapFn(ctx, cred.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull both credential2 back from the db, decrypt them with the new key, and ensure things match + got := allocSshPrivateKeyCredential() + got.PublicId = cred.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + got2 := allocSshPrivateKeyCredential() + got2.PublicId = cred2.PublicId + assert.NoError(t, rw.LookupById(ctx, got2)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, TestSshPrivateKeyPem, string(got.PrivateKey)) + assert.NotEqual(t, cred.GetPrivateKeyEncrypted(), got.GetPrivateKeyEncrypted()) + assert.NotEmpty(t, got.GetPrivateKeyHmac()) + assert.Equal(t, cred.GetPrivateKeyHmac(), got.GetPrivateKeyHmac()) + // we didn't set this, so they should be empty before AND after rewrapping + assert.Empty(t, got.GetPrivateKeyPassphrase()) + assert.Empty(t, got.GetPrivateKeyPassphraseHmac()) + assert.Empty(t, got.GetPrivateKeyPassphraseEncrypted()) + + // perform all the same checks again on #2, but also check passphrase + assert.NoError(t, got2.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got2.GetKeyId()) + assert.NotEqual(t, cred2.GetKeyId(), got2.GetKeyId()) + assert.Equal(t, newKeyVersionId, got2.GetKeyId()) + assert.Equal(t, TestSshPrivateKeyPem, string(got2.PrivateKey)) + assert.NotEqual(t, cred.GetPrivateKeyEncrypted(), got.GetPrivateKeyEncrypted()) + assert.NotEmpty(t, got2.GetPrivateKeyHmac()) + assert.Equal(t, cred2.GetPrivateKeyHmac(), got2.GetPrivateKeyHmac()) + // this time, we did set this, so they should be available + assert.NotEmpty(t, got2.GetPrivateKeyPassphraseEncrypted()) + assert.NotEmpty(t, got2.GetPrivateKeyPassphraseHmac()) + assert.Equal(t, []byte("passphrase"), got2.GetPrivateKeyPassphrase()) + assert.Equal(t, cred2.GetPrivateKeyPassphraseHmac(), got2.GetPrivateKeyPassphraseHmac()) + }) +} + +func TestRewrap_credStaticJsonRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct json\.public_id, json\.object_encrypted, json\.key_id from credential_static_json_credential json inner join credential_static_store store on store\.public_id = json\.store_id where store\.project_id = \$1 and json\.key_id = \$2;`, + ).WillReturnError(errors.New("Query error")) + err := credStaticJsonRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + obj, objBytes, err := TestJsonObject() + assert.NoError(t, err) + cred, err := NewJsonCredential(ctx, cs.GetPublicId(), obj) + assert.NoError(t, err) + + cred.PublicId, err = credential.NewJsonCredentialId(ctx) + assert.NoError(t, err) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + + assert.NoError(t, cred.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), cred)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credStaticJsonRewrapFn(ctx, cred.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocJsonCredential() + got.PublicId = cred.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cred.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, objBytes, got.GetObject()) + assert.NotEmpty(t, got.GetObjectHmac()) + assert.Equal(t, cred.GetObjectHmac(), got.GetObjectHmac()) + }) +} diff --git a/internal/credential/vault/rewrapping.go b/internal/credential/vault/rewrapping.go new file mode 100644 index 000000000..472af2ebd --- /dev/null +++ b/internal/credential/vault/rewrapping.go @@ -0,0 +1,92 @@ +package vault + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn("credential_vault_client_certificate", credVaultClientCertificateRewrapFn) + kms.RegisterTableRewrapFn("credential_vault_token", credVaultTokenRewrapFn) +} + +func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) string { + if dataKeyVersionId == "" { + return "missing data key version id" + } + if scopeId == "" { + return "missing scope id" + } + if util.IsNil(reader) { + return "missing database reader" + } + if util.IsNil(writer) { + return "missing database writer" + } + if kmsRepo == nil { + return "missing kms repository" + } + return "" +} + +func credVaultClientCertificateRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "vault.credVaultClientCertificateRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var certs []*ClientCertificate + // only index is store id, and store isn't queryable via scope. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &certs, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cert := range certs { + if err := cert.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt vault client certificate")) + } + if err := cert.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt vault client certificate")) + } + if _, err := writer.Update(ctx, cert, []string{"CtCertificateKey", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update vault client certificate row with rewrapped fields")) + } + } + return nil +} + +func credVaultTokenRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "vault.credVaultTokenRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var tokens []*Token + // Indexes exist on token hmac, store id, expiration time. none of which are queryable via scope or key. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &tokens, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, token := range tokens { + if err := token.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt vault token")) + } + if err := token.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt vault token")) + } + if _, err := writer.Update(ctx, token, []string{"CtToken", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update vault token row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/credential/vault/rewrapping_test.go b/internal/credential/vault/rewrapping_test.go new file mode 100644 index 000000000..49a61f27d --- /dev/null +++ b/internal/credential/vault/rewrapping_test.go @@ -0,0 +1,128 @@ +package vault + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_credVaultClientCertificateRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "credential_vault_client_certificate" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := credVaultClientCertificateRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId, "https://vault.consul.service", "token", "accessor") + cert, err := NewClientCertificate([]byte(certPem), []byte(keyPem)) + assert.NoError(t, err) + + cert.StoreId = cs.PublicId + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + assert.NoError(t, cert.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), cert)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credVaultClientCertificateRewrapFn(ctx, cert.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocClientCertificate() + got.StoreId = cs.PublicId + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, cert.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, keyPem, string(got.GetCertificateKey())) + assert.NotEmpty(t, got.GetCertificateKeyHmac()) + assert.Equal(t, cert.GetCertificateKeyHmac(), got.GetCertificateKeyHmac()) + }) +} + +func TestRewrap_credVaultTokenRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "credential_vault_token" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := credVaultTokenRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId, "https://vault.consul.service", "token", "accessor") + token := cs.outputToken + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, credVaultTokenRewrapFn(ctx, token.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the token back from the db, decrypt it with the new key, and ensure things match + got := allocToken() + assert.NoError(t, rw.LookupWhere(ctx, &got, "store_id = ?", []any{cs.PublicId})) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, token.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, "token", string(got.GetToken())) + assert.NotEmpty(t, got.GetTokenHmac()) + // vault token hmacs are calculated differently and should remain the same + assert.Equal(t, token.GetTokenHmac(), got.GetTokenHmac()) + }) +} diff --git a/internal/daemon/cluster/handlers/worker_service.go b/internal/daemon/cluster/handlers/worker_service.go index 66197b89a..ee921d1c5 100644 --- a/internal/daemon/cluster/handlers/worker_service.go +++ b/internal/daemon/cluster/handlers/worker_service.go @@ -356,6 +356,7 @@ func (ws *workerServiceServer) LookupSession(ctx context.Context, req *pbs.Looku Authorization: &targets.SessionAuthorizationData{ SessionId: sessionInfo.GetPublicId(), Certificate: sessionInfo.Certificate, + PrivateKey: sessionInfo.CertificatePrivateKey, }, Status: sessionInfo.States[0].Status.ProtoVal(), Version: sessionInfo.Version, @@ -374,18 +375,6 @@ func (ws *workerServiceServer) LookupSession(ctx context.Context, req *pbs.Looku resp.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount) } - wrapper, err := ws.kms.GetWrapper(ctx, sessionInfo.ProjectId, kms.KeyPurposeSessions, kms.WithKeyId(sessionInfo.KeyId)) - if err != nil { - return nil, status.Errorf(codes.Internal, "Error getting sessions wrapper: %v", err) - } - - // Derive the private key, which should match. Deriving on both ends allows - // us to not store it in the DB. - _, resp.Authorization.PrivateKey, err = session.DeriveED25519Key(ctx, wrapper, sessionInfo.UserId, sessionInfo.GetPublicId()) - if err != nil { - return nil, status.Errorf(codes.Internal, "Error deriving session key: %v", err) - } - return resp, nil } diff --git a/internal/daemon/cluster/handlers/worker_service_test.go b/internal/daemon/cluster/handlers/worker_service_test.go index 83cce0452..3b32d31f9 100644 --- a/internal/daemon/cluster/handlers/worker_service_test.go +++ b/internal/daemon/cluster/handlers/worker_service_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/internal/authtoken" credstatic "github.com/hashicorp/boundary/internal/credential/static" "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" @@ -23,6 +22,7 @@ import ( "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/testdata" @@ -180,6 +180,11 @@ func TestLookupSession(t *testing.T) { SessionId: sess.PublicId, }, want: &pbs.LookupSessionResponse{ + Authorization: &targets.SessionAuthorizationData{ + SessionId: sess.PublicId, + Certificate: sess.Certificate, + PrivateKey: sess.CertificatePrivateKey, + }, ConnectionLimit: 1, ConnectionsLeft: 1, Version: 1, @@ -198,6 +203,11 @@ func TestLookupSession(t *testing.T) { WorkerId: worker1.GetPublicId(), }, want: &pbs.LookupSessionResponse{ + Authorization: &targets.SessionAuthorizationData{ + SessionId: sess.PublicId, + Certificate: sess.Certificate, + PrivateKey: sess.CertificatePrivateKey, + }, ConnectionLimit: 1, ConnectionsLeft: 1, Version: 1, @@ -215,6 +225,11 @@ func TestLookupSession(t *testing.T) { SessionId: sessWithCreds.PublicId, }, want: &pbs.LookupSessionResponse{ + Authorization: &targets.SessionAuthorizationData{ + SessionId: sessWithCreds.PublicId, + Certificate: sessWithCreds.Certificate, + PrivateKey: sessWithCreds.CertificatePrivateKey, + }, ConnectionLimit: 1, ConnectionsLeft: 1, Version: 1, @@ -244,9 +259,8 @@ func TestLookupSession(t *testing.T) { cmp.Diff( tc.want, got, - cmpopts.IgnoreUnexported(pbs.LookupSessionResponse{}, - pbs.Credential{}, pbs.UsernamePassword{}, pbs.SshPrivateKey{}), - cmpopts.IgnoreFields(pbs.LookupSessionResponse{}, "Expiration", "Authorization"), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.LookupSessionResponse{}, "expiration"), ), ) }) diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 9f03d9cfd..f92e3175b 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + kmsjob "github.com/hashicorp/boundary/internal/kms/job" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/plugin/host" hostplugin "github.com/hashicorp/boundary/internal/plugin/host" @@ -373,6 +374,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { return target.NewRepository(ctx, dbase, dbase, c.kms, o...) } c.SessionRepoFn = func(opt ...session.Option) (*session.Repository, error) { + // Always add a secure random reader to the new session repository. + // Add it as the first option so that it can be overridden by users. + opt = append([]session.Option{session.WithRandomReader(c.conf.SecureRandomReader)}, opt...) return session.NewRepository(ctx, dbase, dbase, c.kms, opt...) } c.ConnectionRepoFn = func() (*session.ConnectionRepository, error) { @@ -517,6 +521,9 @@ func (c *Controller) registerJobs() error { if err := serversjob.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms); err != nil { return err } + if err := kmsjob.RegisterJobs(c.baseContext, c.scheduler, c.kms); err != nil { + return err + } return nil } diff --git a/internal/daemon/controller/controller_test.go b/internal/daemon/controller/controller_test.go index 46da5a1d4..dcfd33028 100644 --- a/internal/daemon/controller/controller_test.go +++ b/internal/daemon/controller/controller_test.go @@ -275,3 +275,44 @@ func TestController_NewPluginsConfig(t *testing.T) { } } } + +func Test_ControllerStart(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + tc := &TestController{ + t: t, + ctx: ctx, + cancel: cancel, + opts: nil, + shutdownDoneCh: make(chan struct{}), + shutdownOnce: new(sync.Once), + } + conf := TestControllerConfig(t, ctx, tc, nil) + + c, err := New(ctx, conf) + require.NoError(t, err) + err = c.Start() + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, c.Shutdown()) + }) + + sqlDb, err := tc.DbConn().SqlDB(ctx) + require.NoError(t, err) + rows, err := sqlDb.QueryContext(ctx, "select name from job") + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, rows.Close()) + }) + var jobNames []string + for rows.Next() { + var jobName string + err := rows.Scan(&jobName) + require.NoError(t, err) + jobNames = append(jobNames, jobName) + } + require.NoError(t, rows.Err()) + // Check that the monitor job and at least one of the rewrapping jobs has been registered + assert.Contains(t, jobNames, "data-key-version-destruction-monitor-job") + assert.Contains(t, jobNames, "session-rewrapping-job") +} diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 45f649170..d07d20f01 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -153,7 +153,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterAuthTokenServiceServer(s, authtoks) } if _, ok := currentServices[services.ScopeService_ServiceDesc.ServiceName]; !ok { - os, err := scopes.NewService(c.IamRepoFn) + os, err := scopes.NewService(c.baseContext, c.IamRepoFn, c.kms) if err != nil { return fmt.Errorf("failed to create scope handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index e1e8a1d17..dd24ff671 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -27,13 +27,17 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/internal/util" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -54,6 +58,10 @@ var ( CollectionActions = action.ActionSet{ action.Create, action.List, + action.ListScopeKeys, + action.RotateScopeKeys, + action.ListScopeKeyVersionDestructionJobs, + action.DestroyScopeKeyVersion, } scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{ @@ -81,6 +89,7 @@ var ( resource.Group: groups.CollectionActions, resource.HostCatalog: host_catalogs.CollectionActions, resource.Role: roles.CollectionActions, + resource.Scope: CollectionActions[2:], // Only Scope key actions are allowed on the project level resource.Session: sessions.CollectionActions, resource.Target: targets.CollectionActions, }, @@ -98,18 +107,22 @@ func init() { type Service struct { pbs.UnsafeScopeServiceServer - repoFn common.IamRepoFactory + repoFn common.IamRepoFactory + kmsRepo *kms.Kms } var _ pbs.ScopeServiceServer = (*Service)(nil) // NewService returns a project service which handles project related requests to boundary. -func NewService(repo common.IamRepoFactory) (Service, error) { +func NewService(ctx context.Context, repo common.IamRepoFactory, kmsRepo *kms.Kms) (Service, error) { const op = "scopes.(Service).NewService" - if repo == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing iam repository") + if util.IsNil(repo) { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") } - return Service{repoFn: repo}, nil + if kmsRepo == nil { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing kms") + } + return Service{repoFn: repo, kmsRepo: kmsRepo}, nil } // ListScopes implements the interface pbs.ScopeServiceServer. @@ -355,6 +368,142 @@ func (s Service) DeleteScope(ctx context.Context, req *pbs.DeleteScopeRequest) ( return nil, nil } +// ListKeys implements the interface pbs.ScopeServiceServer. +func (s Service) ListKeys(ctx context.Context, req *pbs.ListKeysRequest) (*pbs.ListKeysResponse, error) { + if req.GetId() == "" { + req.Id = scope.Global.String() + } + if err := validateListKeysRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetId(), action.ListScopeKeys) + if authResults.Error != nil { + return nil, authResults.Error + } + + items, err := s.kmsRepo.ListKeys(ctx, req.GetId()) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, handlers.NotFoundErrorf("unknown scope_id %q", req.Id) + } + return nil, err + } + + finalItems := make([]*pb.Key, 0, len(items)) + res := perms.Resource{ + Type: resource.Scope, + } + for _, item := range items { + res.Id = item.Id + res.ScopeId = item.Scope + + outputFields := authResults.FetchOutputFields(res, action.ListScopeKeys).SelfOrDefaults(authResults.UserId) + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + + protoItem, err := keyToProto(ctx, item, outputOpts...) + if err != nil { + return nil, err + } + finalItems = append(finalItems, protoItem) + } + sortKeys(finalItems) + return &pbs.ListKeysResponse{Items: finalItems}, nil +} + +// RotateKeys implements the interface pbs.ScopeServiceServer. +func (s Service) RotateKeys(ctx context.Context, req *pbs.RotateKeysRequest) (*pbs.RotateKeysResponse, error) { + if req.GetScopeId() == "" { + req.ScopeId = scope.Global.String() + } + if err := validateRotateKeysRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetScopeId(), action.RotateScopeKeys) + if authResults.Error != nil { + return nil, authResults.Error + } + + if err := s.kmsRepo.RotateKeys(ctx, req.GetScopeId(), kms.WithRewrap(req.GetRewrap())); err != nil { + return nil, err + } + + return nil, nil +} + +// ListKeyVersionDestructionJobs implements the interface pbs.ScopeServiceServer. +func (s Service) ListKeyVersionDestructionJobs(ctx context.Context, req *pbs.ListKeyVersionDestructionJobsRequest) (*pbs.ListKeyVersionDestructionJobsResponse, error) { + if req.GetScopeId() == "" { + req.ScopeId = scope.Global.String() + } + if err := validateListKeyVersionDestructionJobsRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetScopeId(), action.ListScopeKeyVersionDestructionJobs) + if authResults.Error != nil { + return nil, authResults.Error + } + jobs, err := s.kmsRepo.ListDataKeyVersionDestructionJobs(ctx, req.GetScopeId()) + if err != nil { + return nil, err + } + res := perms.Resource{ + Type: resource.Scope, + } + finalJobs := make([]*pb.KeyVersionDestructionJob, 0, len(jobs)) + for _, job := range jobs { + res.Id = job.ScopeId + res.ScopeId = job.ScopeId + + outputFields := authResults.FetchOutputFields(res, action.ListScopeKeyVersionDestructionJobs).SelfOrDefaults(authResults.UserId) + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + + protoJob, err := destructionJobToProto(ctx, job, outputOpts...) + if err != nil { + return nil, err + } + finalJobs = append(finalJobs, protoJob) + } + return &pbs.ListKeyVersionDestructionJobsResponse{ + Items: finalJobs, + }, nil +} + +// DestroyKeyVersion implements the interface pbs.ScopeServiceServer. +func (s Service) DestroyKeyVersion(ctx context.Context, req *pbs.DestroyKeyVersionRequest) (*pbs.DestroyKeyVersionResponse, error) { + if req.GetScopeId() == "" { + req.ScopeId = scope.Global.String() + } + if err := validateDestroyKeyVersionRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetScopeId(), action.DestroyScopeKeyVersion) + if authResults.Error != nil { + return nil, authResults.Error + } + destroyed, err := s.kmsRepo.DestroyKeyVersion(ctx, req.GetScopeId(), req.GetKeyVersionId()) + if err != nil { + if errors.Match(errors.T(errors.KeyNotFound), err) { + return nil, handlers.NotFoundErrorf("unknown key_version_id %q", req.KeyVersionId) + } + return nil, err + } + state := "completed" + if !destroyed { + state = "pending" + } + return &pbs.DestroyKeyVersionResponse{ + State: state, + }, nil +} + func (s Service) getFromRepo(ctx context.Context, id string) (*iam.Scope, error) { repo, err := s.repoFn() if err != nil { @@ -490,6 +639,21 @@ func SortScopes(scps []*pb.Scope) { }) } +func sortKeys(keys []*pb.Key) { + // We stable sort here even though the database may not return things in + // sorted order, still nice to have them as consistent as possible. + sort.SliceStable(keys, func(i, j int) bool { + return keys[i].GetId() < keys[j].GetId() + }) + // we also want to sort the key versions by version id so they are in + // descending order (newest first) + for _, key := range keys { + sort.Slice(key.Versions, func(i, j int) bool { + return key.Versions[i].Version > key.Versions[j].Version + }) + } +} + func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.Scope, error) { repo, err := s.repoFn() if err != nil { @@ -513,7 +677,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. var parentId string opts := []auth.Option{auth.WithType(resource.Scope), auth.WithAction(a)} switch a { - case action.List, action.Create: + case action.List, action.Create, action.ListScopeKeys, action.ListScopeKeyVersionDestructionJobs, action.DestroyScopeKeyVersion: parentId = id s, err := repo.LookupScope(ctx, parentId) if err != nil { @@ -589,6 +753,71 @@ func ToProto(ctx context.Context, in *iam.Scope, opt ...handlers.Option) (*pb.Sc return &out, nil } +func keyToProto(ctx context.Context, in wrappingKms.Key, opt ...handlers.Option) (*pb.Key, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building key proto") + } + outputFields := *opts.WithOutputFields + + out := pb.Key{} + if outputFields.Has(globals.IdField) { + out.Id = in.Id + } + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope + } + if outputFields.Has(globals.KeyPurposeField) { + out.Purpose = string(in.Purpose) + } + if outputFields.Has(globals.CreatedTimeField) { + out.CreatedTime = timestamppb.New(in.CreateTime) + } + if outputFields.Has(globals.TypeField) { + out.Type = string(in.Type) + } + if outputFields.Has(globals.KeyVersionsField) { + for _, keyVersion := range in.Versions { + out.Versions = append(out.Versions, &pb.KeyVersion{ + Id: keyVersion.Id, + Version: uint32(keyVersion.Version), + CreatedTime: timestamppb.New(keyVersion.CreateTime), + }) + } + } + + return &out, nil +} + +func destructionJobToProto(ctx context.Context, in *kms.DataKeyVersionDestructionJobProgress, opt ...handlers.Option) (*pb.KeyVersionDestructionJob, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building data key version destruction job proto") + } + outputFields := *opts.WithOutputFields + + out := pb.KeyVersionDestructionJob{} + if outputFields.Has(globals.KeyVersionIdField) { + out.KeyVersionId = in.KeyId + } + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope + } + if outputFields.Has(globals.CreatedTimeField) { + out.CreatedTime = in.CreateTime.Timestamp + } + if outputFields.Has(globals.CompletedCountField) { + out.CompletedCount = in.CompletedCount + } + if outputFields.Has(globals.TotalCountField) { + out.TotalCount = in.TotalCount + } + if outputFields.Has(globals.StatusField) { + out.Status = in.Status + } + return &out, nil +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // - The path passed in is correctly formatted @@ -745,3 +974,51 @@ func validateListRequest(req *pbs.ListScopesRequest) error { } return nil } + +func validateListKeysRequest(req *pbs.ListKeysRequest) error { + badFields := map[string]string{} + if req.GetId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetId()), scope.Project.Prefix()) { + badFields["id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing keys." + } + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) + } + return nil +} + +func validateRotateKeysRequest(req *pbs.RotateKeysRequest) error { + badFields := map[string]string{} + if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) { + badFields["id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing keys." + } + // other field is just a bool so can't be validated + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) + } + return nil +} + +func validateListKeyVersionDestructionJobsRequest(req *pbs.ListKeyVersionDestructionJobsRequest) error { + badFields := map[string]string{} + if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) { + badFields["scope_id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing key version destruction jobs." + } + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) + } + return nil +} + +func validateDestroyKeyVersionRequest(req *pbs.DestroyKeyVersionRequest) error { + badFields := map[string]string{} + if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) { + badFields["scope_id"] = "Must be 'global', a valid org scope id or a valid project scope id when destroying a key version." + } + if !handlers.ValidId(handlers.Id(req.GetKeyVersionId()), "kdkv", "krkv") { + badFields["key_version_id"] = "Must be a valid KEK or DEK version ID." + } + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) + } + return nil +} diff --git a/internal/daemon/controller/handlers/scopes/scope_service_test.go b/internal/daemon/controller/handlers/scopes/scope_service_test.go index ab720e14f..110df25ca 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service_test.go +++ b/internal/daemon/controller/handlers/scopes/scope_service_test.go @@ -10,15 +10,24 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" @@ -26,14 +35,11 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete"} -func createDefaultScopesAndRepo(t *testing.T) (*iam.Scope, *iam.Scope, func() (*iam.Repository, error)) { +func createDefaultScopesRepoAndKms(t *testing.T) (*iam.Scope, *iam.Scope, func() (*iam.Repository, error), *kms.Kms) { t.Helper() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) @@ -41,6 +47,7 @@ func createDefaultScopesAndRepo(t *testing.T) (*iam.Scope, *iam.Scope, func() (* repoFn := func() (*iam.Repository, error) { return iamRepo, nil } + kms := kms.TestKms(t, conn, wrap) oRes, pRes := iam.TestScopes(t, iamRepo) @@ -57,7 +64,7 @@ func createDefaultScopesAndRepo(t *testing.T) (*iam.Scope, *iam.Scope, func() (* require.NoError(t, err) pRes, _, err = repo.UpdateScope(context.Background(), pRes, 1, []string{"Name", "Description"}) require.NoError(t, err) - return oRes, pRes, repoFn + return oRes, pRes, repoFn, kms } var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ @@ -88,6 +95,10 @@ var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), + structpb.NewStringValue("list-keys"), + structpb.NewStringValue("rotate-keys"), + structpb.NewStringValue("list-key-version-destruction-jobs"), + structpb.NewStringValue("destroy-key-version"), }, }, "users": { @@ -135,6 +146,10 @@ var orgAuthorizedCollectionActions = map[string]*structpb.ListValue{ Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), + structpb.NewStringValue("list-keys"), + structpb.NewStringValue("rotate-keys"), + structpb.NewStringValue("list-key-version-destruction-jobs"), + structpb.NewStringValue("destroy-key-version"), }, }, "users": { @@ -175,6 +190,14 @@ var projectAuthorizedCollectionActions = map[string]*structpb.ListValue{ structpb.NewStringValue("list"), }, }, + "scopes": { + Values: []*structpb.Value{ + structpb.NewStringValue("list-keys"), + structpb.NewStringValue("rotate-keys"), + structpb.NewStringValue("list-key-version-destruction-jobs"), + structpb.NewStringValue("destroy-key-version"), + }, + }, "targets": { Values: []*structpb.Value{ structpb.NewStringValue("create"), @@ -184,7 +207,7 @@ var projectAuthorizedCollectionActions = map[string]*structpb.ListValue{ } func TestGet(t *testing.T) { - org, proj, repoFn := createDefaultScopesAndRepo(t) + org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) toMerge := &pbs.GetScopeRequest{ Id: proj.GetPublicId(), } @@ -246,7 +269,7 @@ func TestGet(t *testing.T) { { name: "Get a non existing project", scopeId: org.GetPublicId(), - req: &pbs.GetScopeRequest{Id: "p_DoesntExis"}, + req: &pbs.GetScopeRequest{Id: "p_DoesntExist"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -272,7 +295,7 @@ func TestGet(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetScopeRequest) proto.Merge(req, tc.req) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(err, "Couldn't create new project service.") got, gErr := s.GetScope(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) @@ -294,6 +317,7 @@ func TestList(t *testing.T) { } repo, err := repoFn() require.NoError(t, err) + kms := kms.TestKms(t, conn, wrap) oNoProjects, p1 := iam.TestScopes(t, repo) _, err = repo.DeleteScope(context.Background(), p1.GetPublicId()) @@ -353,7 +377,7 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(err, "Couldn't create new role service.") // Test with non-anonymous listing first @@ -467,7 +491,7 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(err, "Couldn't create new role service.") // Test with non-anonymous listing first @@ -494,9 +518,9 @@ func TestList(t *testing.T) { } func TestDelete(t *testing.T) { - org, proj, repoFn := createDefaultScopesAndRepo(t) + org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(t, err, "Error when getting new project service.") cases := []struct { @@ -517,7 +541,7 @@ func TestDelete(t *testing.T) { name: "Delete bad project id Project", scopeId: org.GetPublicId(), req: &pbs.DeleteScopeRequest{ - Id: "p_doesntexis", + Id: "p_DoesntExist", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -540,7 +564,7 @@ func TestDelete(t *testing.T) { name: "Delete bad org id Org", scopeId: scope.Global.String(), req: &pbs.DeleteScopeRequest{ - Id: "p_doesntexis", + Id: "p_DoesntExist", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -568,9 +592,9 @@ func TestDelete(t *testing.T) { func TestDelete_twice(t *testing.T) { assert, require := assert.New(t), require.New(t) - org, proj, repoFn := createDefaultScopesAndRepo(t) + org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(err, "Error when getting new scopes service") ctx := auth.DisabledAuthTestContext(repoFn, org.GetPublicId()) req := &pbs.DeleteScopeRequest{ @@ -595,7 +619,7 @@ func TestDelete_twice(t *testing.T) { func TestCreate(t *testing.T) { ctx := context.Background() - defaultOrg, defaultProj, repoFn := createDefaultScopesAndRepo(t) + defaultOrg, defaultProj, repoFn, kms := createDefaultScopesRepoAndKms(t) defaultProjCreated := defaultProj.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.CreateScopeRequest{} @@ -778,7 +802,7 @@ func TestCreate(t *testing.T) { req := proto.Clone(toMerge).(*pbs.CreateScopeRequest) proto.Merge(req, tc.req) - s, err := scopes.NewService(repoFn) + s, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(err, "Error when getting new project service.") if name != "" { @@ -852,8 +876,8 @@ func TestCreate(t *testing.T) { } func TestUpdate(t *testing.T) { - org, proj, repoFn := createDefaultScopesAndRepo(t) - tested, err := scopes.NewService(repoFn) + org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) + tested, err := scopes.NewService(context.Background(), repoFn, kms) require.NoError(t, err, "Error when getting new project service.") iamRepo, err := repoFn() @@ -1172,7 +1196,7 @@ func TestUpdate(t *testing.T) { name: "Update a Non Existing Project", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ - Id: "p_DoesntExis", + Id: "p_DoesntExist", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, @@ -1285,3 +1309,1126 @@ func TestUpdate(t *testing.T) { }) } } + +func TestListKeys(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + aToken := tc.Token() + uToken := tc.UnprivilegedToken() + + iamRepoFn := func() (*iam.Repository, error) { + return tc.IamRepo(), nil + } + serversRepoFn := func() (*server.Repository, error) { + return tc.ServersRepo(), nil + } + authTokenRepoFn := func() (*authtoken.Repository, error) { + return tc.AuthTokenRepo(), nil + } + + privCtx := auth.NewVerifierContext( + context.Background(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: aToken.Id, + EncryptedToken: strings.Split(aToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }) + + unprivCtx := auth.NewVerifierContext( + context.Background(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: uToken.Id, + EncryptedToken: strings.Split(uToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }) + + org, proj := iam.TestScopes(t, tc.IamRepo()) + + org.Name = "defaultOrg" + org.Description = "defaultOrg" + org, _, err := tc.IamRepo().UpdateScope(context.Background(), org, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for listing keys in org + listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) + _, err = tc.IamRepo().AddRoleGrants(context.Background(), listKeysRole.PublicId, 1, []string{"id=*;type=*;actions=list-keys"}) + require.NoError(t, err) + _, err = tc.IamRepo().AddPrincipalRoles(context.Background(), listKeysRole.PublicId, 2, []string{aToken.UserId}) + require.NoError(t, err) + + proj.Name = "defaultProj" + proj.Description = "defaultProj" + proj, _, err = tc.IamRepo().UpdateScope(context.Background(), proj, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for listing keys in project + listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) + _, err = tc.IamRepo().AddRoleGrants(context.Background(), listKeysRole.PublicId, 1, []string{"id=*;type=*;actions=list-keys"}) + require.NoError(t, err) + _, err = tc.IamRepo().AddPrincipalRoles(context.Background(), listKeysRole.PublicId, 2, []string{aToken.UserId}) + require.NoError(t, err) + + cases := []struct { + name string + req *pbs.ListKeysRequest + res *pbs.ListKeysResponse + authCtx context.Context + err error + }{ + { + name: "List keys in the global scope", + req: &pbs.ListKeysRequest{Id: "global"}, + res: &pbs.ListKeysResponse{ + Items: []*pb.Key{ + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "tokens", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "oplog", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "database", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "audit", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "oidc", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "sessions", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Purpose: "rootKey", + Type: "kek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List keys in an existing org", + req: &pbs.ListKeysRequest{Id: org.GetPublicId()}, + res: &pbs.ListKeysResponse{ + Items: []*pb.Key{ + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "tokens", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "oplog", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "database", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "audit", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "oidc", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "sessions", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: org.PublicId, + ParentScopeId: "global", + Type: "org", + Name: org.Name, + Description: org.Description, + }, + Purpose: "rootKey", + Type: "kek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List keys in a non existing org", + req: &pbs.ListKeysRequest{Id: "o_DoesntExis"}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "List keys in an existing project", + req: &pbs.ListKeysRequest{Id: proj.GetPublicId()}, + res: &pbs.ListKeysResponse{ + Items: []*pb.Key{ + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "tokens", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "oplog", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "database", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "audit", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "oidc", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "sessions", + Type: "dek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + { + Scope: &pb.ScopeInfo{ + Id: proj.PublicId, + ParentScopeId: org.PublicId, + Type: "project", + Name: proj.Name, + Description: proj.Description, + }, + Purpose: "rootKey", + Type: "kek", + Versions: []*pb.KeyVersion{ + { + Version: 1, + }, + }, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List keys in a non existing project", + req: &pbs.ListKeysRequest{Id: "p_DoesntExist"}, + err: handlers.ApiErrorWithCode(codes.NotFound), + authCtx: privCtx, + }, + { + name: "Wrong id prefix", + req: &pbs.ListKeysRequest{Id: "j_1234567890"}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + authCtx: privCtx, + }, + { + name: "space in id", + req: &pbs.ListKeysRequest{Id: "p_1 23456789"}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + authCtx: privCtx, + }, + { + name: "unauthorized", + req: &pbs.ListKeysRequest{Id: "global"}, + err: handlers.ApiErrorWithCode(codes.PermissionDenied), + authCtx: unprivCtx, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := scopes.NewService(context.Background(), iamRepoFn, tc.Kms()) + require.NoError(err, "Couldn't create new project service.") + + got, gErr := s.ListKeys(tt.authCtx, tt.req) + if tt.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tt.err), "ListKeys(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) + } else { + require.NoError(gErr) + } + assert.Empty( + cmp.Diff( + tt.res, + got, + protocmp.Transform(), + // Sort by purpose for comparison since it is the only unique and predictable field + protocmp.SortRepeated(func(i, j *pb.Key) bool { return i.GetPurpose() < j.GetPurpose() }), + protocmp.IgnoreFields(&pb.Key{}, "id", "created_time"), + protocmp.IgnoreFields(&pb.KeyVersion{}, "id", "created_time"), + ), + "ListKeys(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, + ) + }) + } +} + +func TestRotateKeys(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + aToken := tc.Token() + uToken := tc.UnprivilegedToken() + + iamRepoFn := func() (*iam.Repository, error) { + return tc.IamRepo(), nil + } + serversRepoFn := func() (*server.Repository, error) { + return tc.ServersRepo(), nil + } + authTokenRepoFn := func() (*authtoken.Repository, error) { + return tc.AuthTokenRepo(), nil + } + + privCtx := auth.NewVerifierContext( + context.Background(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: aToken.Id, + EncryptedToken: strings.Split(aToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }) + + unprivCtx := auth.NewVerifierContext( + context.Background(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: uToken.Id, + EncryptedToken: strings.Split(uToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }) + + org, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(aToken.UserId)) + + // Add new role for listing+rotating keys in org + rotateKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) + iam.TestRoleGrant(t, tc.DbConn(), rotateKeysRole.PublicId, "id=*;type=*;actions=rotate-keys,list-keys") + _ = iam.TestUserRole(t, tc.DbConn(), rotateKeysRole.PublicId, aToken.UserId) + + // Add new role for listing+rotating keys in project + rotateKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) + iam.TestRoleGrant(t, tc.DbConn(), rotateKeysRole.PublicId, "id=*;type=*;actions=rotate-keys,list-keys") + _ = iam.TestUserRole(t, tc.DbConn(), rotateKeysRole.PublicId, aToken.UserId) + + cases := []struct { + name string + req *pbs.RotateKeysRequest + res *pbs.RotateKeysResponse + authCtx context.Context + err error + }{ + { + name: "unauthorized", + req: &pbs.RotateKeysRequest{ScopeId: "global", Rewrap: false}, + err: handlers.ApiErrorWithCode(codes.PermissionDenied), + authCtx: unprivCtx, + }, + { + name: "Rotate keys in a non existing org", + req: &pbs.RotateKeysRequest{ScopeId: "o_DoesntExis", Rewrap: false}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "successfully Rotate keys in org", + req: &pbs.RotateKeysRequest{ScopeId: org.GetPublicId(), Rewrap: false}, + authCtx: privCtx, + }, + { + name: "successfully Rotate and rewrap keys in org", + req: &pbs.RotateKeysRequest{ScopeId: org.GetPublicId(), Rewrap: true}, + authCtx: privCtx, + }, + { + name: "successfully Rotate keys in project", + req: &pbs.RotateKeysRequest{ScopeId: proj.GetPublicId(), Rewrap: false}, + authCtx: privCtx, + }, + { + name: "successfully Rotate and rewrap keys in project", + req: &pbs.RotateKeysRequest{ScopeId: proj.GetPublicId(), Rewrap: true}, + authCtx: privCtx, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := scopes.NewService(context.Background(), iamRepoFn, tc.Kms()) + require.NoError(err, "Couldn't create new project service.") + + prevKeyVersions := map[uint32]int{} + var prevLatest uint32 = 0 + + // checking key versions before rotation + if tt.err == nil { + keys, gErr := s.ListKeys(privCtx, &pbs.ListKeysRequest{Id: tt.req.ScopeId}) + require.NoError(gErr) + + for _, key := range keys.Items { + for _, keyVersion := range key.Versions { + if keyVersion.Version > prevLatest { + prevLatest = keyVersion.Version + } + _, ok := prevKeyVersions[keyVersion.Version] + if !ok { + prevKeyVersions[keyVersion.Version] = 0 + } + prevKeyVersions[keyVersion.Version]++ + } + } + } + + // RotateKeys returns nocontent response + _, kErr := s.RotateKeys(tt.authCtx, tt.req) + + if tt.err != nil { + require.Error(kErr) + assert.True(errors.Is(kErr, tt.err), "RotateKeys(%+v) got error\n%v, wanted\n%v", tt.req, kErr, tt.err) + } else { + require.NoError(kErr) + keys, gErr := s.ListKeys(privCtx, &pbs.ListKeysRequest{Id: tt.req.ScopeId}) + require.NoError(gErr) + + keyVersions := map[uint32]int{} + var latest uint32 = 0 + + for _, key := range keys.Items { + for _, keyVersion := range key.Versions { + if keyVersion.Version > latest { + latest = keyVersion.Version + } + _, ok := keyVersions[keyVersion.Version] + if !ok { + keyVersions[keyVersion.Version] = 0 + } + keyVersions[keyVersion.Version]++ + } + } + + // there should only be one new key version + assert.Equal(len(prevKeyVersions)+1, len(keyVersions)) + // since we just rotated them, there should be the same number of version 1 and version 2 + assert.Equal(prevKeyVersions[prevLatest], keyVersions[latest]) + } + }) + } +} + +func TestListKeyVersionDestructionJobs(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + aToken := tc.Token() + uToken := tc.UnprivilegedToken() + + iamRepoFn := func() (*iam.Repository, error) { + return tc.IamRepo(), nil + } + serversRepoFn := func() (*server.Repository, error) { + return tc.ServersRepo(), nil + } + authTokenRepoFn := func() (*authtoken.Repository, error) { + return tc.AuthTokenRepo(), nil + } + + privCtx := auth.NewVerifierContext( + tc.Context(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: aToken.Id, + EncryptedToken: strings.Split(aToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }, + ) + + unprivCtx := auth.NewVerifierContext( + tc.Context(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: uToken.Id, + EncryptedToken: strings.Split(uToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }, + ) + + sqldb, err := tc.DbConn().SqlDB(tc.Context()) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, tc.IamRepo()) + + org.Name = "defaultOrg" + org.Description = "defaultOrg" + org, _, err = tc.IamRepo().UpdateScope(tc.Context(), org, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for listing key version destructions in org + listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) + _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "id=*;type=*;actions=list-key-version-destruction-jobs") + _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) + // Create a oidc auth method to create an encrypted value in this scope + databaseWrapper, err := tc.Kms().GetWrapper(tc.Context(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, org.PublicId, oidc.InactiveState, "noAccounts", "fido") + + proj.Name = "defaultProj" + proj.Description = "defaultProj" + proj, _, err = tc.IamRepo().UpdateScope(tc.Context(), proj, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for listing key version destructions in project + listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) + _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "id=*;type=*;actions=list-key-version-destruction-jobs") + _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) + // Create a oidc auth method to create an encrypted value in this scope + databaseWrapper, err = tc.Kms().GetWrapper(tc.Context(), proj.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, proj.PublicId, oidc.InactiveState, "noAccounts", "fido") + + for _, scope := range []string{"global", org.PublicId, proj.PublicId} { + err = tc.Kms().RotateKeys(tc.Context(), scope) + require.NoError(t, err) + keys, err := tc.Kms().ListKeys(tc.Context(), scope) + require.NoError(t, err) + + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(kms.KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + destroyed, err := tc.Kms().DestroyKeyVersion(tc.Context(), scope, kvToDestroy.Id) + require.NoError(t, err) + assert.False(t, destroyed) + t.Cleanup(func() { + _, err = sqldb.ExecContext(tc.Context(), "delete from kms_data_key_version_destruction_job where key_id=$1", kvToDestroy.Id) + require.NoError(t, err) + }) + } + + cases := []struct { + name string + req *pbs.ListKeyVersionDestructionJobsRequest + res *pbs.ListKeyVersionDestructionJobsResponse + authCtx context.Context + err error + }{ + { + name: "List key version destruction jobs in the global scope", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "global"}, + res: &pbs.ListKeyVersionDestructionJobsResponse{ + Items: []*pb.KeyVersionDestructionJob{ + { + Scope: &pb.ScopeInfo{ + Id: "global", + Type: "global", + Name: "global", + Description: "Global Scope", + }, + Status: "pending", + CompletedCount: 0, + TotalCount: 7, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List key version destruction jobs in an existing org", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: org.GetPublicId()}, + res: &pbs.ListKeyVersionDestructionJobsResponse{ + Items: []*pb.KeyVersionDestructionJob{ + { + Scope: &pb.ScopeInfo{ + Id: org.GetPublicId(), + ParentScopeId: "global", + Type: "org", + Name: org.GetName(), + Description: org.GetDescription(), + }, + Status: "pending", + CompletedCount: 0, + TotalCount: 1, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List key version destruction jobs in a non existing org", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "o_DoesntExis"}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "List key version destruction jobs in an existing project", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: proj.GetPublicId()}, + res: &pbs.ListKeyVersionDestructionJobsResponse{ + Items: []*pb.KeyVersionDestructionJob{ + { + Scope: &pb.ScopeInfo{ + Id: proj.GetPublicId(), + ParentScopeId: org.GetPublicId(), + Type: "project", + Name: proj.GetName(), + Description: proj.GetDescription(), + }, + Status: "pending", + CompletedCount: 0, + TotalCount: 1, + }, + }, + }, + authCtx: privCtx, + }, + { + name: "List key version destruction jobs in a non existing project", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "p_DoesntExist"}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Wrong id prefix", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "j_1234567890"}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "space in id", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "p_1 23456789"}, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "unauthorized", + req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "global"}, + authCtx: unprivCtx, + err: handlers.ApiErrorWithCode(codes.PermissionDenied), + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := scopes.NewService(context.Background(), iamRepoFn, tc.Kms()) + require.NoError(err, "Couldn't create new project service.") + + got, gErr := s.ListKeyVersionDestructionJobs(tt.authCtx, tt.req) + if tt.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tt.err), "ListKeyVersionDestructionJobs(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) + } else { + require.NoError(gErr) + } + assert.Empty( + cmp.Diff( + tt.res, + got, + protocmp.Transform(), + protocmp.SortRepeated(func(i, j *pb.KeyVersionDestructionJob) bool { return i.GetTotalCount() < j.GetTotalCount() }), + protocmp.IgnoreFields(&pb.KeyVersionDestructionJob{}, "key_version_id", "created_time"), + ), + "ListKeyVersionDestructionJobs(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, + ) + }) + } +} + +func TestDestroyKeyVersion(t *testing.T) { + tc := controller.NewTestController(t, nil) + t.Cleanup(tc.Shutdown) + + aToken := tc.Token() + uToken := tc.UnprivilegedToken() + + iamRepoFn := func() (*iam.Repository, error) { + return tc.IamRepo(), nil + } + serversRepoFn := func() (*server.Repository, error) { + return tc.ServersRepo(), nil + } + authTokenRepoFn := func() (*authtoken.Repository, error) { + return tc.AuthTokenRepo(), nil + } + + privCtx := auth.NewVerifierContext( + tc.Context(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: aToken.Id, + EncryptedToken: strings.Split(aToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }, + ) + + unprivCtx := auth.NewVerifierContext( + tc.Context(), + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + tc.Kms(), + &authpb.RequestInfo{ + PublicId: uToken.Id, + EncryptedToken: strings.Split(uToken.Token, "_")[2], + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }, + ) + + org, proj := iam.TestScopes(t, tc.IamRepo()) + + org.Name = "defaultOrg" + org.Description = "defaultOrg" + org, _, err := tc.IamRepo().UpdateScope(tc.Context(), org, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for destroying key versions in org + listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) + _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "id=*;type=*;actions=destroy-key-version") + _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) + // Create a oidc auth method to create an encrypted value in this scope + databaseWrapper, err := tc.Kms().GetWrapper(tc.Context(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, org.PublicId, oidc.InactiveState, "noAccounts", "fido") + + proj.Name = "defaultProj" + proj.Description = "defaultProj" + proj, _, err = tc.IamRepo().UpdateScope(tc.Context(), proj, 1, []string{"Name", "Description"}) + require.NoError(t, err) + + // Add new role for destroying key versions in project + listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) + _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "id=*;type=*;actions=destroy-key-version") + _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) + // Create a oidc auth method to create an encrypted value in this scope + databaseWrapper, err = tc.Kms().GetWrapper(tc.Context(), proj.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, proj.PublicId, oidc.InactiveState, "noAccounts", "fido") + + scopeToImmediatelyDestroyedKeyVersionId := map[string]string{} + scopeToPendingDestructionKeyVersionId := map[string]string{} + for _, scope := range []string{"global", org.PublicId, proj.PublicId} { + err = tc.Kms().RotateKeys(tc.Context(), scope) + require.NoError(t, err) + + keys, err := tc.Kms().ListKeys(tc.Context(), scope) + require.NoError(t, err) + + for _, key := range keys { + switch key.Purpose { + case wrappingKms.KeyPurpose(kms.KeyPurposeDatabase.String()): + scopeToPendingDestructionKeyVersionId[scope] = key.Versions[0].Id + case wrappingKms.KeyPurpose(kms.KeyPurposeRootKey.String()): + scopeToImmediatelyDestroyedKeyVersionId[scope] = key.Versions[0].Id + } + } + } + + cases := []struct { + name string + req *pbs.DestroyKeyVersionRequest + res *pbs.DestroyKeyVersionResponse + authCtx context.Context + err error + }{ + { + name: "Errors when specifying a non existing key version Id", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "global", + KeyVersionId: "krkv_DoesntExist", + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Creates a key version destruction job in the global scope", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "global", + KeyVersionId: scopeToPendingDestructionKeyVersionId["global"], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "pending", + }, + authCtx: privCtx, + }, + { + name: "Immediately destroys a root key version in the global scope", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "global", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "completed", + }, + authCtx: privCtx, + }, + { + name: "Creates a key version destruction job in an existing org", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: org.GetPublicId(), + KeyVersionId: scopeToPendingDestructionKeyVersionId[org.GetPublicId()], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "pending", + }, + authCtx: privCtx, + }, + { + name: "Immediately destroys a root key version in an existing org", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: org.GetPublicId(), + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[org.GetPublicId()], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "completed", + }, + authCtx: privCtx, + }, + { + name: "Creates a key version destruction job in an existing project", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: proj.GetPublicId(), + KeyVersionId: scopeToPendingDestructionKeyVersionId[proj.GetPublicId()], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "pending", + }, + authCtx: privCtx, + }, + { + name: "Immediately destroys a root key version in an existing project", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: proj.GetPublicId(), + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[proj.GetPublicId()], + }, + res: &pbs.DestroyKeyVersionResponse{ + State: "completed", + }, + authCtx: privCtx, + }, + { + name: "Errors when specifying a key version that doesn't exist in the scope", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: org.GetPublicId(), + KeyVersionId: scopeToPendingDestructionKeyVersionId[proj.GetPublicId()], + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Errors when specifying a non existing org", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "o_DoesntExist", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[org.GetPublicId()], + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Errors when specifying a non existing project", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "p_DoesntExist", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[proj.GetPublicId()], + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Wrong id prefix", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "j_1234567890", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "space in id", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "p_1 23456789", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], + }, + authCtx: privCtx, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "unauthorized", + req: &pbs.DestroyKeyVersionRequest{ + ScopeId: "global", + KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], + }, + authCtx: unprivCtx, + err: handlers.ApiErrorWithCode(codes.PermissionDenied), + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := scopes.NewService(context.Background(), iamRepoFn, tc.Kms()) + require.NoError(err, "Couldn't create new project service.") + + got, gErr := s.DestroyKeyVersion(tt.authCtx, tt.req) + if tt.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tt.err), "DestroyKeyVersion(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) + } else { + require.NoError(gErr) + } + + assert.Empty(cmp.Diff(tt.res, got, protocmp.Transform()), "DestroyKeyVersion(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res) + }) + } +} diff --git a/internal/daemon/controller/handlers/sessions/session_service.go b/internal/daemon/controller/handlers/sessions/session_service.go index 4352a6077..351374a75 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -70,7 +70,7 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p if err := validateGetRequest(req); err != nil { return nil, err } - authResults := s.authResult(ctx, req.GetId(), action.ReadSelf) + authResults := s.authResult(ctx, req.GetId(), action.ReadSelf, false) if authResults.Error != nil { return nil, authResults.Error } @@ -125,7 +125,7 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest) return nil, err } - authResults := s.authResult(ctx, req.GetScopeId(), action.List) + authResults := s.authResult(ctx, req.GetScopeId(), action.List, false) if authResults.Error != nil { // If it's forbidden, and it's a recursive request, and they're // successfully authenticated but just not authorized, keep going as we @@ -212,17 +212,29 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques if err := validateCancelRequest(req); err != nil { return nil, err } - authResults := s.authResult(ctx, req.GetId(), action.CancelSelf) + // Ignore decryption failures to ensure the user can always cancel a session. + authResults := s.authResult(ctx, req.GetId(), action.CancelSelf, true) if authResults.Error != nil { return nil, authResults.Error } // We'll verify it's not already canceled, but after checking auth so as not // to leak that information. - ses, err := s.getFromRepo(ctx, req.GetId()) + repo, err := s.repoFn() if err != nil { return nil, err } + // Ignore decryption failures to ensure the user can always cancel a session. + ses, _, err := repo.LookupSession(ctx, req.GetId(), session.WithIgnoreDecryptionFailures(true)) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, handlers.NotFoundErrorf("Session %q doesn't exist.", req.GetId()) + } + return nil, err + } + if ses == nil { + return nil, handlers.NotFoundErrorf("Session %q doesn't exist.", req.GetId()) + } var outputFields perms.OutputFieldsMap authorizedActions := authResults.FetchActionSetForId(ctx, ses.GetPublicId(), IdActions) @@ -254,9 +266,10 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques } if !skipCancel { - ses, err = s.cancelInRepo(ctx, req.GetId(), req.GetVersion()) + // Ignore decryption failures to ensure the user can always cancel a session. + ses, err = repo.CancelSession(ctx, req.GetId(), req.GetVersion(), session.WithIgnoreDecryptionFailures(true)) if err != nil { - return nil, err + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update session")) } } @@ -294,20 +307,7 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*session.Session, return sess, nil } -func (s Service) cancelInRepo(ctx context.Context, id string, version uint32) (*session.Session, error) { - const op = "sessions.(Service).cancelInRepo" - repo, err := s.repoFn() - if err != nil { - return nil, err - } - out, err := repo.CancelSession(ctx, id, version) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update session")) - } - return out, nil -} - -func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { +func (s Service) authResult(ctx context.Context, id string, a action.Type, ignoreSessionDecryptionFailure bool) auth.VerifyResults { res := auth.VerifyResults{} var parentId string @@ -335,7 +335,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. res.Error = err return res } - t, _, err := repo.LookupSession(ctx, id) + t, _, err := repo.LookupSession(ctx, id, session.WithIgnoreDecryptionFailures(ignoreSessionDecryptionFailure)) if err != nil { res.Error = err return res diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 721fa100a..157dfe7a4 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -846,7 +846,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession if err != nil { return nil, err } - sess, privKey, err := sessionRepo.CreateSession(ctx, wrapper, sess, workerList(selectedWorkers).addresses()) + sess, err = sessionRepo.CreateSession(ctx, wrapper, sess, workerList(selectedWorkers).addresses()) if err != nil { return nil, err } @@ -942,7 +942,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession CreatedTime: sess.CreateTime.GetTimestamp(), Type: t.GetType().String(), Certificate: sess.Certificate, - PrivateKey: privKey, + PrivateKey: sess.CertificatePrivateKey, HostId: chosenEndpoint.HostId, Endpoint: endpointUrl.String(), WorkerInfo: workerList(selectedWorkers).workerInfos(), diff --git a/internal/daemon/controller/rewrapping_test.go b/internal/daemon/controller/rewrapping_test.go new file mode 100644 index 000000000..21ce5e556 --- /dev/null +++ b/internal/daemon/controller/rewrapping_test.go @@ -0,0 +1,41 @@ +package controller + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" +) + +func Test_AllRewrapTablesRegistered(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, extWrapper) + + tables, err := kmsCache.ListDataKeyVersionReferencers(ctx) + assert.NoError(t, err) + + var filteredTables []string + // there are some tables we know won't be rewrapped + for _, table := range tables { + // oplog entries cannot be rewrapped at this time + if table == "oplog_entry" { + continue + } + // kms_data_key_version_destruction_job does not contain encrypted data, only a reference to the key versions + if table == "kms_data_key_version_destruction_job" { + continue + } + + // all other tables should be registered + filteredTables = append(filteredTables, table) + } + + assert.Empty(t, cmp.Diff(filteredTables, kms.ListTablesSupportingRewrap(), cmpopts.SortSlices(func(i, j string) bool { return i < j })), "At least one table referencing a data key does not have a rewrapping function registered") +} diff --git a/internal/db/changesafe_reader_writer.go b/internal/db/changesafe_reader_writer.go index c365450dc..44a2426f4 100644 --- a/internal/db/changesafe_reader_writer.go +++ b/internal/db/changesafe_reader_writer.go @@ -46,6 +46,10 @@ func (r *changeSafeDbwReader) SearchWhere(ctx context.Context, resources any, wh return dbw.New(r.db.underlying.wrapped.Load()).SearchWhere(ctx, resources, where, args, opt...) } +func (r *changeSafeDbwReader) Dialect() (_ dbw.DbType, rawName string, _ error) { + return r.db.underlying.wrapped.Load().DbType() +} + // changeSafeDbwWriter is a type that wraps a *db.Db as a dbw.Writer and ensures // that uses of the underlying database follows changes to the connection. type changeSafeDbwWriter struct { @@ -103,3 +107,7 @@ func (w *changeSafeDbwWriter) ScanRows(rows *sql.Rows, result any) error { func (w *changeSafeDbwWriter) Update(ctx context.Context, i any, fieldMaskPaths []string, setToNullPaths []string, opt ...dbw.Option) (int, error) { return dbw.New(w.db.underlying.wrapped.Load()).Update(ctx, i, fieldMaskPaths, setToNullPaths, opt...) } + +func (w *changeSafeDbwWriter) Dialect() (_ dbw.DbType, rawName string, _ error) { + return w.db.underlying.wrapped.Load().DbType() +} diff --git a/internal/db/read_writer_ext_test.go b/internal/db/read_writer_ext_test.go index 26a212d01..e1a7a9766 100644 --- a/internal/db/read_writer_ext_test.go +++ b/internal/db/read_writer_ext_test.go @@ -16,8 +16,8 @@ import ( func TestDb_Create_OnConflict(t *testing.T) { ctx := context.Background() - wrapper := db.TestWrapper(t) conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestDBWrapper(t, conn, "oplog") rw := db.New(conn) db.TestCreateTables(t, conn) diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 89beff0e8..adcfdb331 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -58,6 +58,7 @@ func TestDb_Update(t *testing.T) { publicId, err := NewPublicId("testuser") require.NoError(t, err) id := testId(t) + wrapper := TestDBWrapper(t, conn, "oplog") badVersion := uint32(22) versionOne := uint32(1) @@ -395,10 +396,10 @@ func TestDb_Update(t *testing.T) { user := testUser(t, conn, "foo-"+id, id, id) user.Name = "friendly-" + id - rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil, + rowsUpdated, err := w.Update(testCtx, user, []string{"Name"}, nil, // write oplogs for this update WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "key-only": nil, "deployment": []string{"amex"}, @@ -445,7 +446,7 @@ func TestDb_Update(t *testing.T) { user, []string{"Name"}, nil, NewOplogMsg(&updateMsg), - WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}}), + WithOplog(wrapper, oplog.Metadata{"alice": []string{"bob"}}), ) require.Error(err) assert.Equal(0, rowsUpdated) @@ -488,7 +489,7 @@ func TestDb_Update(t *testing.T) { "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}) + err = w.WriteOplogEntryWith(context.Background(), wrapper, 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)) @@ -568,7 +569,7 @@ func TestDb_Update(t *testing.T) { user.Name = "friendly-" + id rowsUpdated, err := w.Update(context.Background(), user, []string{"Name"}, nil, WithOplog( - TestWrapper(t), + wrapper, nil, ), ) @@ -632,6 +633,7 @@ func TestDb_Create(t *testing.T) { // intentionally not run with t.Parallel so we don't need to use DoTx for the Create tests conn, _ := TestSetup(t, "postgres") TestCreateTables(t, conn) + wrapper := TestDBWrapper(t, conn, "oplog") t.Run("simple", func(t *testing.T) { assert, require := assert.New(t), require.New(t) w := New(conn) @@ -669,7 +671,7 @@ func TestDb_Create(t *testing.T) { context.Background(), user, WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "key-only": nil, "deployment": []string{"amex"}, @@ -700,7 +702,7 @@ func TestDb_Create(t *testing.T) { context.Background(), user, NewOplogMsg(&createMsg), - WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}}), + WithOplog(wrapper, oplog.Metadata{"alice": []string{"bob"}}), ) require.Error(err) assert.True(errors.Match(errors.T(errors.InvalidParameter), err)) @@ -743,7 +745,7 @@ func TestDb_Create(t *testing.T) { "resource-public-id": []string{user.PublicId}, } - err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &updateMsg}) + err = w.WriteOplogEntryWith(context.Background(), wrapper, 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)) @@ -784,7 +786,7 @@ func TestDb_Create(t *testing.T) { context.Background(), user, WithOplog( - TestWrapper(t), + wrapper, nil, ), ) @@ -1326,6 +1328,7 @@ func TestDb_DoTx(t *testing.T) { func TestDb_Delete(t *testing.T) { conn, _ := TestSetup(t, "postgres") TestCreateTables(t, conn) + wrapper := TestDBWrapper(t, conn, "oplog") newUser := func() *db_test.TestUser { w := New(conn) u, err := db_test.NewTestUser() @@ -1373,7 +1376,7 @@ func TestDb_Delete(t *testing.T) { { name: "simple-no-oplog", underlying: conn, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: newUser(), }, @@ -1383,7 +1386,7 @@ func TestDb_Delete(t *testing.T) { { name: "with-where-no-delete", underlying: conn, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: newUser(), opt: []Option{WithWhere("1 = ?", 2)}, @@ -1395,7 +1398,7 @@ func TestDb_Delete(t *testing.T) { { name: "with-where-and-delete", underlying: conn, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: newUser(), opt: []Option{WithWhere("1 = ?", 1)}, @@ -1406,7 +1409,7 @@ func TestDb_Delete(t *testing.T) { { name: "valid-with-oplog", underlying: conn, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: newUser(), metadata: newMetadata, @@ -1444,7 +1447,7 @@ func TestDb_Delete(t *testing.T) { { name: "nil-underlying", underlying: nil, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: newUser(), }, @@ -1455,7 +1458,7 @@ func TestDb_Delete(t *testing.T) { { name: "not-found", underlying: conn, - wrapper: TestWrapper(t), + wrapper: wrapper, args: args{ i: notFoundUser(), }, @@ -1521,7 +1524,7 @@ func TestDb_Delete(t *testing.T) { require.NoError(err) deleteMsg := oplog.Message{} - rowsDeleted, err := w.Delete(context.Background(), user, NewOplogMsg(&deleteMsg), WithOplog(TestWrapper(t), oplog.Metadata{"alice": []string{"bob"}})) + rowsDeleted, err := w.Delete(context.Background(), user, NewOplogMsg(&deleteMsg), WithOplog(wrapper, oplog.Metadata{"alice": []string{"bob"}})) require.Error(err) assert.Equal(0, rowsDeleted) assert.True(errors.Match(errors.T(errors.InvalidParameter), err)) @@ -1562,7 +1565,7 @@ func TestDb_Delete(t *testing.T) { metadata := oplog.Metadata{ "resource-public-id": []string{user.PublicId}, } - err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &deleteMsg}) + err = w.WriteOplogEntryWith(context.Background(), wrapper, 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)) @@ -1642,6 +1645,7 @@ func TestDb_Query(t *testing.T) { func TestDb_CreateItems(t *testing.T) { db, _ := TestSetup(t, "postgres") TestCreateTables(t, db) + wrapper := TestDBWrapper(t, db, "oplog") testOplogResourceId := testId(t) createFn := func() []any { @@ -1694,7 +1698,7 @@ func TestDb_CreateItems(t *testing.T) { createItems: createFn(), opt: []Option{ WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "resource-public-id": []string{testOplogResourceId}, "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, @@ -1725,7 +1729,7 @@ func TestDb_CreateItems(t *testing.T) { opt: []Option{ NewOplogMsgs(&[]*oplog.Message{}), WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "resource-public-id": []string{testOplogResourceId}, "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, @@ -1752,7 +1756,7 @@ func TestDb_CreateItems(t *testing.T) { createItems: createFn(), opt: []Option{ WithOplog( - TestWrapper(t), + wrapper, nil, ), }, @@ -1853,6 +1857,7 @@ func TestDb_CreateItems(t *testing.T) { func TestDb_DeleteItems(t *testing.T) { db, _ := TestSetup(t, "postgres") TestCreateTables(t, db) + wrapper := TestDBWrapper(t, db, "oplog") testOplogResourceId := testId(t) createFn := func() []any { @@ -1909,7 +1914,7 @@ func TestDb_DeleteItems(t *testing.T) { opt: []Option{ NewOplogMsgs(&[]*oplog.Message{}), WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "resource-public-id": []string{testOplogResourceId}, "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, @@ -1927,7 +1932,7 @@ func TestDb_DeleteItems(t *testing.T) { deleteItems: createFn(), opt: []Option{ WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "resource-public-id": []string{testOplogResourceId}, "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, @@ -1946,7 +1951,7 @@ func TestDb_DeleteItems(t *testing.T) { deleteItems: createFn(), opt: []Option{ WithOplog( - TestWrapper(t), + wrapper, nil, ), }, @@ -2341,6 +2346,7 @@ func TestDb_GetTicket(t *testing.T) { func TestDb_WriteOplogEntryWith(t *testing.T) { conn, _ := TestSetup(t, "postgres") TestCreateTables(t, conn) + wrapper := TestDBWrapper(t, conn, "oplog") w := New(conn) testCtx := context.Background() @@ -2382,7 +2388,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "valid", underlying: conn, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: ticket, metadata: metadata, msgs: []*oplog.Message{&createMsg}, @@ -2393,7 +2399,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "valid-multiple", underlying: conn, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: ticket, metadata: metadata, msgs: []*oplog.Message{&createMsg, &createMsg}, @@ -2404,7 +2410,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "missing-ticket", underlying: conn, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: nil, metadata: metadata, msgs: []*oplog.Message{&createMsg}, @@ -2416,7 +2422,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "missing-db", underlying: nil, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: ticket, metadata: metadata, msgs: []*oplog.Message{&createMsg}, @@ -2440,7 +2446,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "nil-metadata", underlying: conn, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: ticket, metadata: nil, msgs: []*oplog.Message{&createMsg}, @@ -2452,7 +2458,7 @@ func TestDb_WriteOplogEntryWith(t *testing.T) { name: "empty-metadata", underlying: conn, args: args{ - wrapper: TestWrapper(t), + wrapper: wrapper, ticket: ticket, metadata: oplog.Metadata{}, msgs: []*oplog.Message{&createMsg}, diff --git a/internal/db/schema/migrations/oss/postgres/0/11_auth_token.up.sql b/internal/db/schema/migrations/oss/postgres/0/11_auth_token.up.sql index aed440b34..09bdedd03 100644 --- a/internal/db/schema/migrations/oss/postgres/0/11_auth_token.up.sql +++ b/internal/db/schema/migrations/oss/postgres/0/11_auth_token.up.sql @@ -57,6 +57,7 @@ begin; comment on function update_last_access_time() is 'function used in before update triggers to properly set last_access_time columns'; + -- this trigger is deleted in 58/05_mutable_ciphertext_columns.up.sql create or replace function immutable_auth_token_columns() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql index 5c7f35cb6..eeb86461a 100644 --- a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql @@ -190,6 +190,7 @@ begin; create trigger default_create_time_column before insert on credential_vault_token for each row execute procedure default_create_time(); + -- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on credential_vault_token for each row execute procedure immutable_columns('token_hmac', 'token', 'store_id','create_time'); diff --git a/internal/db/schema/migrations/oss/postgres/23/01_session_credential.up.sql b/internal/db/schema/migrations/oss/postgres/23/01_session_credential.up.sql index 5ab025e32..a87bfb27b 100644 --- a/internal/db/schema/migrations/oss/postgres/23/01_session_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/23/01_session_credential.up.sql @@ -22,7 +22,7 @@ begin; 'session_credential is a table where each row contains a credential to be used by ' 'by a worker when a connection is established for the session_id.'; - -- Replaced in 43/01_session_credentials.up.sql + -- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on session_credential for each row execute procedure immutable_columns('session_id', 'credential', 'key_id'); diff --git a/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql b/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql index beb3808f5..447c1d3c7 100644 --- a/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql +++ b/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql @@ -72,6 +72,7 @@ create table worker_auth_ca_certificate( comment on table worker_auth_ca_certificate is 'worker_auth_ca_certificate is a table where each row represents a root certificate for used for worker authentication.'; +-- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on worker_auth_ca_certificate for each row execute procedure immutable_columns('serial_number', 'certificate', 'not_valid_before', 'not_valid_after', 'public_key', 'private_key', 'key_id', 'state', 'issuing_ca'); @@ -101,7 +102,7 @@ create table worker_auth_authorized( comment on table worker_auth_authorized is 'worker_auth_authorized is a table where each row represents key and cert data associated with an authorized worker.'; --- Trigger updated in 55/01_worker_auth_create_time.up.sql +-- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on worker_auth_authorized for each row execute procedure immutable_columns('worker_key_identifier', 'worker_id', 'worker_signing_pub_key', 'worker_encryption_pub_key', 'controller_encryption_priv_key', 'key_id', 'nonce'); diff --git a/internal/db/schema/migrations/oss/postgres/43/01_session_credentials.up.sql b/internal/db/schema/migrations/oss/postgres/43/01_session_credentials.up.sql index 96d314e31..1477b86c5 100644 --- a/internal/db/schema/migrations/oss/postgres/43/01_session_credentials.up.sql +++ b/internal/db/schema/migrations/oss/postgres/43/01_session_credentials.up.sql @@ -13,7 +13,7 @@ begin; add constraint session_credential_session_id_credential_sha256_uq unique(session_id, credential_sha256); - -- Replace the immutable columns trigger from 23/01_session_credential.up.sql + -- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql drop trigger immutable_columns on session_credential; create trigger immutable_columns before update on session_credential for each row execute procedure immutable_columns('session_id', 'credential', 'key_id', 'credential_sha256'); diff --git a/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql b/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql index 1c09ebe92..9de264a92 100644 --- a/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/01_credentials.up.sql @@ -212,6 +212,7 @@ begin; 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; -- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql + -- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view credential_vault_credential_private as select credential.public_id as public_id, credential.library_id as library_id, diff --git a/internal/db/schema/migrations/oss/postgres/44/02_hosts.up.sql b/internal/db/schema/migrations/oss/postgres/44/02_hosts.up.sql index ed72357b8..5d6752c3a 100644 --- a/internal/db/schema/migrations/oss/postgres/44/02_hosts.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/02_hosts.up.sql @@ -70,8 +70,8 @@ begin; $$ language plpgsql; -- update views - -- Replace view from 20/10_plugin_host_secrets_hmac.up.sql drop view host_plugin_catalog_with_secret; + -- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view host_plugin_catalog_with_secret as select hc.public_id, diff --git a/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql b/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql index 5a32ca005..ca08979df 100644 --- a/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/04_sessions.up.sql @@ -12,6 +12,7 @@ begin; ; -- Replaces trigger from 01/50_session.up.sql + -- Replaced in 58/02_add_data_key_foreign_key_references create or replace function cancel_session_with_null_fk() returns trigger as $$ begin @@ -59,6 +60,7 @@ begin; $$ language plpgsql; -- Replaces view from 34/04_views.up.sql + -- Replaced in 58/06_add_session_private_key_column drop view session_list; create view session_list as select diff --git a/internal/db/schema/migrations/oss/postgres/47/01_key_rewrap.up.sql b/internal/db/schema/migrations/oss/postgres/47/01_key_rewrap.up.sql index ea30a7e19..ceb6b0e36 100644 --- a/internal/db/schema/migrations/oss/postgres/47/01_key_rewrap.up.sql +++ b/internal/db/schema/migrations/oss/postgres/47/01_key_rewrap.up.sql @@ -17,6 +17,7 @@ create trigger kms_immutable_columns before update on kms_root_key_version -- rewrapping the data key version. drop trigger kms_immutable_columns on kms_data_key_version; +-- trigger updated again in migration 56 file 04 create trigger kms_immutable_columns before update on kms_data_key_version for each row execute procedure kms_immutable_columns('private_id', 'data_key_id', 'root_key_version_id', 'create_time'); diff --git a/internal/db/schema/migrations/oss/postgres/48/01_worker_auth_activation_token.up.sql b/internal/db/schema/migrations/oss/postgres/48/01_worker_auth_activation_token.up.sql index 4b9d053f8..77c5df420 100644 --- a/internal/db/schema/migrations/oss/postgres/48/01_worker_auth_activation_token.up.sql +++ b/internal/db/schema/migrations/oss/postgres/48/01_worker_auth_activation_token.up.sql @@ -24,6 +24,7 @@ create table worker_auth_server_led_activation_token( comment on table worker_auth_server_led_activation_token is 'worker_auth_server_led_activation_token is a table where each row represents an activation token for a worker. Only one activation token is allowed per worker.'; +-- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on worker_auth_server_led_activation_token for each row execute procedure immutable_columns('worker_id', 'token_id', 'creation_time_encrypted'); diff --git a/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql b/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql index e972054ec..1a2556c9a 100644 --- a/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql +++ b/internal/db/schema/migrations/oss/postgres/49/01_vault_credentials.up.sql @@ -6,6 +6,7 @@ begin; drop view credential_vault_library_private; drop view credential_vault_store_private; + -- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view credential_vault_token_renewal_revocation as with tokens as ( @@ -77,6 +78,7 @@ begin; 'If the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + -- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view credential_vault_store_client as select store.public_id as public_id, store.project_id as project_id, @@ -105,6 +107,7 @@ begin; 'The view returns the current token for the store, if the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + -- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view credential_vault_library_issue_credentials as with password_override (library_id, username_attribute, password_attribute) as ( diff --git a/internal/db/schema/migrations/oss/postgres/55/01_worker_auth_create_time.up.sql b/internal/db/schema/migrations/oss/postgres/55/01_worker_auth_create_time.up.sql index 82a7fd1ed..eb4da1350 100644 --- a/internal/db/schema/migrations/oss/postgres/55/01_worker_auth_create_time.up.sql +++ b/internal/db/schema/migrations/oss/postgres/55/01_worker_auth_create_time.up.sql @@ -93,7 +93,7 @@ alter table worker_auth_authorized ; drop trigger immutable_columns on worker_auth_authorized; --- Trigger updated from 34/03_worker_authentication.up.sql +-- this trigger is updated in 58/05_mutable_ciphertext_columns.up.sql create trigger immutable_columns before update on worker_auth_authorized for each row execute function immutable_columns('worker_key_identifier', 'worker_id', 'worker_signing_pub_key', 'worker_encryption_pub_key', 'controller_encryption_priv_key', 'key_id', 'nonce', 'create_time'); @@ -135,4 +135,4 @@ comment on function insert_worker_auth_authorized is create trigger insert_worker_auth_authorized before insert on worker_auth_authorized for each row execute function insert_worker_auth_authorized(); -commit; \ No newline at end of file +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/01_oplog_key_id_scope_id_truncation.up.sql b/internal/db/schema/migrations/oss/postgres/58/01_oplog_key_id_scope_id_truncation.up.sql new file mode 100644 index 000000000..9154f7dad --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/01_oplog_key_id_scope_id_truncation.up.sql @@ -0,0 +1,34 @@ +begin; + + truncate table oplog_entry, oplog_metadata; + + alter table oplog_entry + add column key_id kms_private_id not null + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade, + add column scope_id wt_scope_id not null + constraint iam_scope_fkey + references iam_scope (public_id) + on delete cascade + on update cascade; + + create function insert_oplog_entry() returns trigger + as $$ + begin + select scope_id into new.scope_id + from kms_root_key + inner join kms_data_key on kms_root_key.private_id = kms_data_key.root_key_id + inner join kms_data_key_version on kms_data_key.private_id = kms_data_key_version.data_key_id + where kms_data_key_version.private_id = new.key_id; + return new; + end; + $$ language plpgsql; + comment on function insert_oplog_entry() is + 'insert_oplog_entry sets the scope_id based on the key_id'; + + create trigger insert_oplog_entry before insert on oplog_entry + for each row execute procedure insert_oplog_entry(); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/02_add_data_key_foreign_key_references.up.sql b/internal/db/schema/migrations/oss/postgres/58/02_add_data_key_foreign_key_references.up.sql new file mode 100644 index 000000000..c2dcd7a3b --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/02_add_data_key_foreign_key_references.up.sql @@ -0,0 +1,325 @@ +begin; + + alter table auth_token + alter column key_id type kms_private_id, + add constraint kms_data_key_version_fkey + foreign key (key_id) + references kms_data_key_version (private_id) + on delete restrict + on update cascade; + + alter table auth_password_argon2_cred + alter column key_id type kms_private_id, + add constraint kms_data_key_version_fkey + foreign key (key_id) + references kms_data_key_version (private_id) + on delete restrict + on update cascade; + + -- Some existing sessions may exist with an empty key_id + update session + set key_id = null + where key_id = ''; + + alter table session + -- cannot set key_id type to kms_private_id because the kms_private_id type + -- has a 'not null' restriction and the key_id can be null in the session + -- table. + add constraint kms_data_key_version_fkey + foreign key (key_id) + references kms_data_key_version (private_id) + on delete restrict + on update cascade; + + -- Replaces trigger from 44/04_session.up.sql + -- Adding a foreign key reference to the kms key means we have + -- to set the key_id to null when the scope_id is set to null, + -- otherwise we can't delete scopes with sessions, since deleting + -- the scope cascade deletes the key referenced by key_id. + create or replace function cancel_session_with_null_fk() returns trigger + as $$ + begin + case + when new.user_id is null then + perform cancel_session(new.public_id); + when new.host_id is null then + perform cancel_session(new.public_id); + when new.target_id is null then + perform cancel_session(new.public_id); + when new.host_set_id is null then + perform cancel_session(new.public_id); + when new.auth_token_id is null then + perform cancel_session(new.public_id); + when new.project_id is null then + -- Setting the key_id to null will allow the scope + -- to cascade delete its keys. + new.key_id = null; + perform cancel_session(new.public_id); + end case; + return new; + end; + $$ language plpgsql; + + + -- we drop some views, so we can recreate them after changing the type of the referencing columns. + drop view + oidc_auth_method_with_value_obj, + host_plugin_catalog_with_secret, + credential_vault_store_client, + credential_vault_library_issue_credentials, + credential_vault_token_renewal_revocation, + credential_vault_credential_private; + + alter table auth_oidc_method + alter column key_id type kms_private_id; + + alter table host_plugin_catalog_secret + alter column key_id type kms_private_id; + + alter table credential_vault_token + alter column key_id type kms_private_id; + + alter table credential_vault_client_certificate + alter column key_id type kms_private_id; + + alter table session_credential + alter column key_id type kms_private_id; + + alter table credential_static_username_password_credential + alter column key_id type kms_private_id; + + alter table credential_static_ssh_private_key_credential + alter column key_id type kms_private_id; + + create view oidc_auth_method_with_value_obj as + select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs, + string_agg(distinct cs.scope, '|') as claims_scopes, + string_agg(distinct concat_ws('=', acm.from_claim, acm.to_claim), '|') as account_claim_maps + from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id + left outer join auth_oidc_scope cs on am.public_id = cs.oidc_method_id + left outer join auth_oidc_account_claim_map acm on am.public_id = acm.oidc_method_id + group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. + comment on view oidc_auth_method_with_value_obj is + 'oidc auth method with its associated value objects (algs, auds, certs, scopes) as columns with | delimited values'; + + create view host_plugin_catalog_with_secret as + select + hc.public_id, + hc.project_id, + hc.plugin_id, + hc.name, + hc.description, + hc.create_time, + hc.update_time, + hc.version, + hc.secrets_hmac, + hc.attributes, + hcs.secret, + hcs.key_id, + hcs.create_time as persisted_create_time, + hcs.update_time as persisted_update_time + from + host_plugin_catalog hc + left outer join host_plugin_catalog_secret hcs on hc.public_id = hcs.catalog_id; + comment on view host_plugin_catalog_with_secret is + 'host plugin catalog with its associated persisted data'; + + create view credential_vault_store_client as + select store.public_id as public_id, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + token.token as ct_token, -- encrypted + token.token_hmac as token_hmac, + coalesce(token.status, 'expired') as token_status, + token.key_id as token_key_id, + cert.certificate as client_cert, + cert.certificate_key as ct_client_key, -- encrypted + cert.key_id as client_key_id + from credential_vault_store store + left join credential_vault_token token + on store.public_id = token.store_id + and token.status = 'current' + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id + where store.delete_time is null; + comment on view credential_vault_store_client is + 'credential_vault_store_client is a view where each row contains a credential store and the credential store''s data needed to connect to Vault. ' + 'The view returns the current token for the store, if the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_library_issue_credentials as + with + password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + coalesce(upasso.username_attribute,sshpk.username_attribute) + as username_attribute, + upasso.password_attribute as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute + from credential_vault_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id; + comment on view credential_vault_library_issue_credentials is + 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' + 'This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_token_renewal_revocation as + with + tokens as ( + select token, -- encrypted + token_hmac, + store_id, + -- renewal time is the midpoint between the last renewal time and the expiration time + last_renewal_time + (expiration_time - last_renewal_time) / 2 as renewal_time, + key_id, + status + from credential_vault_token + where status in ('current', 'maintaining', 'revoke') + ) + select store.public_id as public_id, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.delete_time as delete_time, + token.token as ct_token, -- encrypted + token.token_hmac as token_hmac, + token.renewal_time as token_renewal_time, + token.key_id as token_key_id, + token.status as token_status, + cert.certificate as client_cert, + cert.certificate_key as ct_client_key, -- encrypted + cert.key_id as client_key_id + from credential_vault_store store + join tokens token + on store.public_id = token.store_id + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id; + comment on view credential_vault_token_renewal_revocation is + 'credential_vault_token_renewal_revocation is a view where each row contains a credential store and the credential store''s data needed to connect to Vault. ' + 'The view returns a separate row for each active token in Vault (current, maintaining and revoke tokens); this view should only be used for token renewal and revocation. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + + create view credential_vault_credential_private as + select credential.public_id as public_id, + credential.library_id as library_id, + credential.session_id as session_id, + credential.create_time as create_time, + credential.update_time as update_time, + credential.version as version, + credential.external_id as external_id, + credential.last_renewal_time as last_renewal_time, + credential.expiration_time as expiration_time, + credential.is_renewable as is_renewable, + credential.status as status, + credential.last_renewal_time + (credential.expiration_time - credential.last_renewal_time) / 2 as renewal_time, + token.token_hmac as token_hmac, + token.token as ct_token, -- encrypted + token.create_time as token_create_time, + token.update_time as token_update_time, + token.last_renewal_time as token_last_renewal_time, + token.expiration_time as token_expiration_time, + token.key_id as token_key_id, + token.status as token_status, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + cert.certificate as client_cert, + cert.certificate_key as ct_client_key, -- encrypted + cert.certificate_key_hmac as client_cert_key_hmac, + cert.key_id as client_key_id + from credential_vault_credential credential + join credential_vault_token token + on credential.token_hmac = token.token_hmac + join credential_vault_store store + on token.store_id = store.public_id + left join credential_vault_client_certificate cert + on store.public_id = cert.store_id + where credential.expiration_time != 'infinity'::date; + comment on view credential_vault_credential_private is + 'credential_vault_credential_private is a view where each row contains a credential, ' + 'the vault token used to issue the credential, and the credential store data needed to connect to Vault. ' + 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/03_create_kms_key_destructions.up.sql b/internal/db/schema/migrations/oss/postgres/58/03_create_kms_key_destructions.up.sql new file mode 100644 index 000000000..c3d299caf --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/03_create_kms_key_destructions.up.sql @@ -0,0 +1,110 @@ +begin; + + create table kms_data_key_version_destruction_job ( + key_id kms_private_id primary key -- one job per key version + references kms_data_key_version (private_id) + on delete cascade -- Note: instead of restrict, we cascade the delete to this job when the key version is deleted + on update cascade, + create_time wt_timestamp not null + ); + comment on table kms_data_key_version_destruction_job is + 'Table holding running and pending data key version destruction jobs'; + + create trigger immutable_columns before update on kms_data_key_version_destruction_job + for each row execute procedure immutable_columns('key_id', 'create_time'); + + + create table kms_data_key_version_destruction_job_run ( + key_id kms_private_id not null + references kms_data_key_version_destruction_job (key_id) + on delete cascade + on update cascade + deferrable initially deferred, + table_name text not null, + total_count bigint not null + constraint total_count_cannot_be_negative + check (total_count > 0), -- Must not be 0 + completed_count bigint not null default 0 + constraint completed_count_cannot_be_negative + check (completed_count >= 0), + is_running boolean not null default false, + + primary key (key_id, table_name), + constraint completed_count_less_than_equal_to_total_count + check (completed_count <= total_count), + constraint is_running_only_when_not_completed + check ((completed_count < total_count) or (completed_count = total_count and not is_running)) + ); + comment on table kms_data_key_version_destruction_job_run is + 'Table holding per-table runs of data key version destruction jobs'; + + create trigger immutable_columns before update on kms_data_key_version_destruction_job_run + for each row execute procedure immutable_columns('key_id', 'table_name', 'total_count'); + + create unique index kms_data_key_version_destruction_job_run_is_running_uq + on kms_data_key_version_destruction_job_run (is_running) where is_running = true; -- allow only one running run + + -- A view that holds all the names of tables that reference the + -- data_key_version.private_id column as a foreign key. + -- Excludes tables that should not be used in a job run. + create view kms_data_key_version_destruction_job_run_allowed_table_name as + select distinct + r.table_name + from + information_schema.constraint_column_usage u + inner join information_schema.referential_constraints fk + on u.constraint_catalog = fk.unique_constraint_catalog and + u.constraint_schema = fk.unique_constraint_schema and + u.constraint_name = fk.unique_constraint_name + inner join information_schema.key_column_usage r + on r.constraint_catalog = fk.constraint_catalog and + r.constraint_schema = fk.constraint_schema and + r.constraint_name = fk.constraint_name + where + u.column_name = 'private_id' and + u.table_name = 'kms_data_key_version' and + -- These tables reference the right column, but are not allowed as a destruction + -- job run table name. + r.table_name not in ('oplog_entry', 'kms_data_key_version_destruction_job'); + + create function kms_data_key_table_name_valid() returns trigger + as $$ + begin + if new.table_name not in (select table_name from kms_data_key_version_destruction_job_run_allowed_table_name) then + raise exception 'invalid table name % (must be table that references kms_data_key_version.private_id)', new.table_name; + end if; + return new; + end; + $$ language plpgsql; + comment on function kms_data_key_table_name_valid is + 'Function used to determine whether a table name is valid for a destruction job run'; + + create trigger kms_data_key_table_name_valid before insert on kms_data_key_version_destruction_job_run + for each row execute procedure kms_data_key_table_name_valid(); + + + -- Used to list progress of all destruction jobs + create view kms_data_key_version_destruction_job_progress as + select + j.key_id, + rk.scope_id, + j.create_time, + case + when bool_or(r.is_running) then 'running' + when sum(r.total_count) = sum(r.completed_count) then 'completed' + else 'pending' + end status, + sum(r.completed_count) as completed_count, + sum(r.total_count) as total_count + from kms_data_key_version_destruction_job j + inner join kms_data_key_version_destruction_job_run r + on j.key_id = r.key_id + inner join kms_data_key_version dkv + on j.key_id = dkv.private_id + inner join kms_data_key dk + on dkv.data_key_id = dk.private_id + inner join kms_root_key rk + on dk.root_key_id = rk.private_id + group by (j.key_id, rk.scope_id); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/04_mutable_root_key_version.up.sql b/internal/db/schema/migrations/oss/postgres/58/04_mutable_root_key_version.up.sql new file mode 100644 index 000000000..c9228a755 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/04_mutable_root_key_version.up.sql @@ -0,0 +1,13 @@ +begin; + +-- this migration is from: +-- https://github.com/hashicorp/go-kms-wrapping/blob/main/extras/kms/migrations/postgres/07_mutable_root_key_version.up.sql + +-- we need to make the root_key_version_id mutable in order to support +-- rewrapping the data key version. +drop trigger kms_immutable_columns on kms_data_key_version; + +create trigger kms_immutable_columns before update on kms_data_key_version + for each row execute procedure kms_immutable_columns('private_id', 'data_key_id', 'create_time'); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/05_mutable_ciphertext_columns.up.sql b/internal/db/schema/migrations/oss/postgres/58/05_mutable_ciphertext_columns.up.sql new file mode 100644 index 000000000..f9f856175 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/05_mutable_ciphertext_columns.up.sql @@ -0,0 +1,41 @@ +begin; + + -- We need to make private_key and key_id mutable, so that we can rewrap them. + drop trigger immutable_columns on worker_auth_ca_certificate; + + create trigger immutable_columns before update on worker_auth_ca_certificate + for each row execute procedure immutable_columns('serial_number', 'certificate', 'not_valid_before', 'not_valid_after', 'public_key', 'state', 'issuing_ca'); + + + -- We need to make token mutable so that we can rewrap it. + drop trigger immutable_auth_token_columns on auth_token; + drop function immutable_auth_token_columns; + + + -- We need to make token and token_hmac mutable, so that we can rewrap them. + drop trigger immutable_columns on credential_vault_token; + + create trigger immutable_columns before update on credential_vault_token + for each row execute procedure immutable_columns('store_id','create_time'); + + + -- And again on credential and key_id (and the hash of credential). + drop trigger immutable_columns on session_credential; + + create trigger immutable_columns before update on session_credential + for each row execute procedure immutable_columns('session_id'); + + + -- And one final time on worker auth for controller priv key and key id. + drop trigger immutable_columns on worker_auth_authorized; + + create trigger immutable_columns before update on worker_auth_authorized + for each row execute function immutable_columns('worker_key_identifier', 'worker_id', 'worker_signing_pub_key', 'worker_encryption_pub_key', 'nonce', 'create_time'); + + + drop trigger immutable_columns on worker_auth_server_led_activation_token; + + create trigger immutable_columns before update on worker_auth_server_led_activation_token + for each row execute procedure immutable_columns('worker_id', 'token_id'); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/58/06_add_session_private_key_column.up.sql b/internal/db/schema/migrations/oss/postgres/58/06_add_session_private_key_column.up.sql new file mode 100644 index 000000000..734d7c233 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/58/06_add_session_private_key_column.up.sql @@ -0,0 +1,48 @@ +begin; + + -- nullable because it will not be set for sessions that are active + -- during the upgrade to this version. + alter table session add column certificate_private_key bytea null; + + -- Replaces the view created in 44/05_session_list_no_connections + drop view session_list; + create view session_list as + select + s.public_id, + s.user_id, + s.host_id, + s.target_id, + s.host_set_id, + s.auth_token_id, + s.project_id, + s.certificate, + s.certificate_private_key, + s.expiration_time, + s.connection_limit, + s.tofu_token, + s.key_id, + s.termination_reason, + s.version, + s.create_time, + s.update_time, + s.endpoint, + s.worker_filter, + ss.state, + ss.previous_end_time, + ss.start_time, + ss.end_time, + sc.public_id as connection_id, + sc.client_tcp_address, + sc.client_tcp_port, + sc.endpoint_tcp_address, + sc.endpoint_tcp_port, + sc.bytes_up, + sc.bytes_down, + sc.closed_reason + from session s + join session_state ss on + s.public_id = ss.session_id + left join session_connection sc on + s.public_id = sc.session_id; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/6/01_oidc.up.sql b/internal/db/schema/migrations/oss/postgres/6/01_oidc.up.sql index 7348026e8..863c94806 100644 --- a/internal/db/schema/migrations/oss/postgres/6/01_oidc.up.sql +++ b/internal/db/schema/migrations/oss/postgres/6/01_oidc.up.sql @@ -42,6 +42,7 @@ drop view oidc_auth_method_with_value_obj; -- value is part of the primary key and unique. This view will make things like -- recursive listing of oidc auth methods fairly straightforward to implement -- for the oidc repo. The view also includes an is_primary_auth_method bool +-- Recreated in 58/02_add_data_key_foreign_key_references.up.sql create view oidc_auth_method_with_value_obj as select case when s.primary_auth_method_id is not null then diff --git a/internal/db/schema/migrations/oss/postgres_40_01_test.go b/internal/db/schema/migrations/oss/postgres_40_01_test.go index 07f0c662d..0414551e3 100644 --- a/internal/db/schema/migrations/oss/postgres_40_01_test.go +++ b/internal/db/schema/migrations/oss/postgres_40_01_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/db/common" "github.com/hashicorp/boundary/internal/db/schema" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" @@ -69,23 +70,103 @@ func TestMigrations_Credential_Purpose_Refactor(t *testing.T) { require.NoError(t, err) rw := db.New(conn) - // Create project wrapper := db.TestWrapper(t) - iamRepo := iam.TestRepo(t, conn, wrapper) - org, proj := iam.TestScopes(t, iamRepo) kmsCache := kms.TestKms(t, conn, wrapper) + authRepo, err := authtoken.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) - at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) - uId := at.GetIamUserId() + uId := "u_1234567890" + oId := "o_1234567890" + pId := "p_1234567890" + accId := "acct_1234567890" + + num, err := rw.Exec(ctx, ` +insert into iam_scope + (public_id, type, parent_id) +values + (?, ?, ?) + `, []any{oId, "org", "global"}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + require.NoError(t, kmsCache.CreateKeys(ctx, oId)) + + num, err = rw.Exec(ctx, ` +insert into iam_scope + (public_id, type, parent_id) +values + (?, ?, ?) + `, []any{pId, "project", oId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + require.NoError(t, kmsCache.CreateKeys(ctx, pId)) + + _, err = rw.DoTx(ctx, 0, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { + _, err := w.Exec(ctx, ` + insert into auth_password_argon2_conf + (private_id, password_method_id) + values + (?, ?) + `, []any{"arg2conf_kWA0RG11DL", "ampw_1234567890"}) + if err != nil { + return err + } + _, err = w.Exec(ctx, ` + insert into auth_password_method + (public_id, password_conf_id, scope_id) + values + (?, ?, ?) + `, []any{"ampw_1234567890", "arg2conf_kWA0RG11DL", oId}) + return err + }) + require.NoError(t, err) + + num, err = rw.Exec(ctx, ` +insert into auth_password_account + (public_id, auth_method_id, scope_id, login_name) +values + (?, ?, ?, ?) + `, []any{"acctpw_1234567890", "ampw_1234567890", oId, "name1"}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + num, err = rw.Exec(ctx, ` +insert into iam_user + (public_id, scope_id) +values + (?, ?) + `, []any{uId, oId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + num, err = rw.Exec(ctx, ` +insert into auth_account + (public_id, scope_id, auth_method_id, iam_user_id, iam_user_scope_id) +values + (?, ?, ?, ?, ?) + `, []any{accId, oId, "ampw_1234567890", uId, oId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + user := &iam.User{ + User: &store.User{ + PublicId: uId, + }, + } + require.NoError(t, rw.LookupById(ctx, user)) + + at, err := authRepo.CreateAuthToken(ctx, user, accId) + require.NoError(t, err) // Create host catalog hostCatalogId := "hcst_s1P81LMusN" - num, err := rw.Exec(ctx, ` + num, err = rw.Exec(ctx, ` insert into static_host_catalog (scope_id, public_id, name) values (?, ?, ?) -`, []any{proj.GetPublicId(), hostCatalogId, "my-host-catalog"}) +`, []any{pId, hostCatalogId, "my-host-catalog"}) require.NoError(t, err) assert.Equal(t, 1, num) @@ -128,7 +209,7 @@ insert into target_tcp (public_id, scope_id, name, session_max_seconds, session_connection_limit) values (?, ?, ?, ?, ?); -`, []any{targetId, proj.GetPublicId(), "my-credential-sources", 28800, -1}) +`, []any{targetId, pId, "my-credential-sources", 28800, -1}) require.NoError(t, err) assert.Equal(t, 1, num) @@ -139,7 +220,7 @@ insert into credential_vault_store (public_id, scope_id, vault_address) values (?, ?, ?); -`, []any{vaultStoreId, proj.GetPublicId(), "http://vault"}) +`, []any{vaultStoreId, pId, "http://vault"}) require.NoError(t, err) assert.Equal(t, 1, num) @@ -154,11 +235,11 @@ insert into credential_static_store (public_id, scope_id, name) values (?, ?, ?) -`, []any{staticStoreId, proj.GetPublicId(), "my-static-credential-store"}) +`, []any{staticStoreId, pId, "my-static-credential-store"}) require.NoError(t, err) assert.Equal(t, 1, num) - credsStatic := static.TestUsernamePasswordCredentials(t, conn, wrapper, "u", "p", staticStoreId, proj.GetPublicId(), 2) + credsStatic := static.TestUsernamePasswordCredentials(t, conn, wrapper, "u", "p", staticStoreId, pId, 2) cred1 := credsStatic[0] cred2 := credsStatic[1] @@ -199,14 +280,14 @@ values sessionId := "s_AgLzPhDINE" future := time.Now().Add(time.Hour) expirationTime := fmt.Sprintf("%v-%d-%v %v:%v:%v.000", future.Year(), future.Month(), future.Day(), future.Hour(), future.Minute(), future.Second()) - _, cert, err := session.TestCert(wrapper, uId, sessionId) + _, cert, err := session.TestCert(sessionId) require.NoError(t, err) num, err = rw.Exec(ctx, ` insert into session (public_id, user_id, host_id, target_id, host_set_id, auth_token_id, scope_id, certificate, expiration_time, endpoint) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, []any{sessionId, uId, hostId, targetId, hostSetId, at.GetPublicId(), proj.GetPublicId(), cert, expirationTime, "tcp://127.0.0.1:22"}) + `, []any{sessionId, uId, hostId, targetId, hostSetId, at.GetPublicId(), pId, cert, expirationTime, "tcp://127.0.0.1:22"}) require.NoError(t, err) assert.Equal(t, 1, num) diff --git a/internal/db/schema/migrations/oss/postgres_53_01_test.go b/internal/db/schema/migrations/oss/postgres_53_01_test.go index 3ad96f65a..36f75715f 100644 --- a/internal/db/schema/migrations/oss/postgres_53_01_test.go +++ b/internal/db/schema/migrations/oss/postgres_53_01_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/internal/db/schema" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/testing/dbtest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -59,13 +60,33 @@ func TestMigrations_DeprecatedGrants(t *testing.T) { require.NoError(t, err) conn, err := db.Open(ctx, dbType, u) require.NoError(t, err) + rw := db.New(conn) // Create project wrapper := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj := iam.TestScopes(t, iamRepo) - role := iam.TestRole(t, conn, proj.GetPublicId()) + oId := "o_1234567890" + pId := "p_1234567890" + num, err := rw.Exec(ctx, ` +insert into iam_scope + (public_id, type, parent_id) +values + (?, ?, ?) + `, []any{oId, "org", "global"}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + num, err = rw.Exec(ctx, ` +insert into iam_scope + (public_id, type, parent_id) +values + (?, ?, ?) + `, []any{pId, "project", oId}) + require.NoError(t, err) + assert.Equal(t, 1, num) + + role := iam.TestRole(t, conn, pId) initialGrants := map[string]bool{ "id=*;type=target;actions=read,authorize-session,add-host-sets,add-host-sets": true, diff --git a/internal/db/schema/migrations/oss/postgres_56_01_test.go b/internal/db/schema/migrations/oss/postgres_56_01_test.go index 0d8f3c0cc..46d542d2d 100644 --- a/internal/db/schema/migrations/oss/postgres_56_01_test.go +++ b/internal/db/schema/migrations/oss/postgres_56_01_test.go @@ -75,53 +75,143 @@ func TestMigrations_DeleteOrphanedAccounts(t *testing.T) { iamRepo := iam.TestRepo(t, conn, wrapper) usr, err := iam.NewUser(scope.Global.String()) require.NoError(t, err) - usr, err = iamRepo.CreateUser(ctx, usr) + usr.PublicId = "u_1234567890" + num, err := rw.Exec(ctx, ` +insert into iam_user + (public_id, scope_id) +values + (?, ?) + `, []any{usr.PublicId, scope.Global.String()}) require.NoError(t, err) + assert.Equal(t, 1, num) // Create the accounts we will delete and assert their behavior - pwRepo, err := password.NewRepository(rw, rw, kmsCache) - require.NoError(t, err) pwAm1, err := password.NewAuthMethod(scope.Global.String()) require.NoError(t, err) - pwAm1, err = pwRepo.CreateAuthMethod(ctx, pwAm1) + pwAm1.PublicId = "ampw_1234567890" + _, err = rw.DoTx(ctx, 0, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { + _, err := w.Exec(ctx, ` + insert into auth_password_argon2_conf + (private_id, password_method_id) + values + (?, ?) + `, []any{"arg2conf_1234567890", pwAm1.PublicId}) + if err != nil { + return err + } + _, err = w.Exec(ctx, ` + insert into auth_password_method + (public_id, password_conf_id, scope_id) + values + (?, ?, ?) + `, []any{pwAm1.PublicId, "arg2conf_1234567890", pwAm1.ScopeId}) + return err + }) require.NoError(t, err) pwAcct1, err := password.NewAccount(pwAm1.GetPublicId(), password.WithLoginName("account1"), password.WithPassword("password")) require.NoError(t, err) - pwAcct1, err = pwRepo.CreateAccount(ctx, scope.Global.String(), pwAcct1) + pwAcct1.PublicId = "acctpw_1234567890" + num, err = rw.Exec(ctx, ` +insert into auth_password_account + (public_id, auth_method_id, scope_id, login_name) +values + (?, ?, ?, ?) + `, []any{pwAcct1.PublicId, pwAm1.PublicId, pwAm1.ScopeId, pwAcct1.LoginName}) require.NoError(t, err) + assert.Equal(t, 1, num) pwAm2, err := password.NewAuthMethod(scope.Global.String()) require.NoError(t, err) - pwAm2, err = pwRepo.CreateAuthMethod(ctx, pwAm2) - require.NoError(t, err) + pwAm2.PublicId = "ampw_0123456789" + _, err = rw.DoTx(ctx, 0, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { + _, err := w.Exec(ctx, ` + insert into auth_password_argon2_conf + (private_id, password_method_id) + values + (?, ?) + `, []any{"arg2conf_0123456789", pwAm2.PublicId}) + if err != nil { + return err + } + _, err = w.Exec(ctx, ` + insert into auth_password_method + (public_id, password_conf_id, scope_id) + values + (?, ?, ?) + `, []any{pwAm2.PublicId, "arg2conf_0123456789", pwAm2.ScopeId}) + return err + }) pwAcct2, err := password.NewAccount(pwAm2.GetPublicId(), password.WithLoginName("account2"), password.WithPassword("password")) require.NoError(t, err) - pwAcct2, err = pwRepo.CreateAccount(ctx, scope.Global.String(), pwAcct2) + pwAcct2.PublicId = "acctpw_0123456789" + num, err = rw.Exec(ctx, ` +insert into auth_password_account + (public_id, auth_method_id, scope_id, login_name) +values + (?, ?, ?, ?) + `, []any{pwAcct2.PublicId, pwAm2.PublicId, pwAm2.ScopeId, pwAcct2.LoginName}) require.NoError(t, err) + assert.Equal(t, 1, num) - oidcRepo, err := oidc.NewRepository(ctx, rw, rw, kmsCache) - require.NoError(t, err) databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase) oidcAm1 := oidc.TestAuthMethod(t, conn, databaseWrapper, scope.Global.String(), oidc.InactiveState, "alice_rp1", "my-dogs-name", oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice1.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api1.com")[0])) + oidcAm1.PublicId = "amoidc_1234567890" + num, err = rw.Exec(ctx, ` + insert into auth_oidc_method + (public_id, scope_id, state, key_id) + values + (?, ?, ?, ?) + `, []any{oidcAm1.PublicId, oidcAm1.ScopeId, oidcAm1.OperationalState, oidcAm1.KeyId}) + require.NoError(t, err) + assert.Equal(t, 1, num) oidcAcct1, err := oidc.NewAccount(ctx, oidcAm1.GetPublicId(), "oidcAcct1") require.NoError(t, err) - oidcAcct1, err = oidcRepo.CreateAccount(ctx, scope.Global.String(), oidcAcct1) + oidcAcct1.PublicId = "acctoidc_0123456789" + num, err = rw.Exec(ctx, ` +insert into auth_oidc_account + (public_id, auth_method_id, scope_id, issuer, subject) +values + (?, ?, ?, ?, ?) + `, []any{oidcAcct1.PublicId, oidcAm1.PublicId, oidcAm1.ScopeId, oidcAm1.Issuer, oidcAcct1.Subject}) require.NoError(t, err) + assert.Equal(t, 1, num) + oidcAm2 := oidc.TestAuthMethod(t, conn, databaseWrapper, scope.Global.String(), oidc.InactiveState, "alice_rp2", "my-dogs-name", oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice2.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api2.com")[0])) + oidcAm2.PublicId = "amoidc_0123456789" + num, err = rw.Exec(ctx, ` + insert into auth_oidc_method + (public_id, scope_id, state, key_id) + values + (?, ?, ?, ?) + `, []any{oidcAm2.PublicId, oidcAm2.ScopeId, oidcAm2.OperationalState, oidcAm2.KeyId}) + require.NoError(t, err) + assert.Equal(t, 1, num) oidcAcct2, err := oidc.NewAccount(ctx, oidcAm2.GetPublicId(), "oidcAcct2") require.NoError(t, err) - oidcAcct2, err = oidcRepo.CreateAccount(ctx, scope.Global.String(), oidcAcct2) + oidcAcct2.PublicId = "acctoidc_1234567890" + num, err = rw.Exec(ctx, ` +insert into auth_oidc_account + (public_id, auth_method_id, scope_id, issuer, subject) +values + (?, ?, ?, ?, ?) + `, []any{oidcAcct2.PublicId, oidcAm2.PublicId, oidcAm2.ScopeId, oidcAm2.Issuer, oidcAcct2.Subject}) require.NoError(t, err) + assert.Equal(t, 1, num) - _, err = iamRepo.AddUserAccounts(ctx, usr.GetPublicId(), usr.GetVersion(), - []string{ - pwAcct1.GetPublicId(), pwAcct2.GetPublicId(), - oidcAcct1.GetPublicId(), oidcAcct2.GetPublicId(), - }) + num, err = rw.Exec(ctx, ` +update auth_account set + iam_user_id=? +where + public_id in (?, ?, ?, ?) + `, []any{ + usr.PublicId, + pwAcct1.PublicId, pwAcct2.PublicId, oidcAcct1.PublicId, oidcAcct2.PublicId, + }) require.NoError(t, err) + assert.Equal(t, 4, num) usr, accts, err := iamRepo.LookupUser(ctx, usr.GetPublicId()) require.NoError(t, err) assert.ElementsMatch(t, []string{ @@ -129,10 +219,20 @@ func TestMigrations_DeleteOrphanedAccounts(t *testing.T) { oidcAcct1.GetPublicId(), oidcAcct2.GetPublicId(), }, accts) - _, err = pwRepo.DeleteAccount(ctx, scope.Global.String(), pwAcct1.GetPublicId()) + num, err = rw.Exec(ctx, ` +delete from auth_password_account +where + public_id=? + `, []any{pwAcct1.PublicId}) require.NoError(t, err) - _, err = oidcRepo.DeleteAccount(ctx, scope.Global.String(), oidcAcct1.GetPublicId()) + assert.Equal(t, 1, num) + num, err = rw.Exec(ctx, ` +delete from auth_oidc_account +where + public_id=? + `, []any{oidcAcct1.PublicId}) require.NoError(t, err) + assert.Equal(t, 1, num) // pwAcct1 is still listed as an account for this user! oh no! _, accts, err = iamRepo.LookupUser(ctx, usr.GetPublicId()) @@ -170,13 +270,23 @@ func TestMigrations_DeleteOrphanedAccounts(t *testing.T) { require.NoError(t, err) assert.ElementsMatch(t, []string{pwAcct2.GetPublicId(), oidcAcct2.GetPublicId()}, accts) - _, err = pwRepo.DeleteAccount(ctx, scope.Global.String(), pwAcct2.GetPublicId()) + num, err = rw.Exec(ctx, ` +delete from auth_password_account +where + public_id=? + `, []any{pwAcct2.PublicId}) require.NoError(t, err) + assert.Equal(t, 1, num) _, accts, err = iamRepo.LookupUser(ctx, usr.GetPublicId()) require.NoError(t, err) assert.ElementsMatch(t, []string{oidcAcct2.GetPublicId()}, accts) - _, err = oidcRepo.DeleteAccount(ctx, scope.Global.String(), oidcAcct2.GetPublicId()) + num, err = rw.Exec(ctx, ` +delete from auth_oidc_account +where + public_id=? + `, []any{oidcAcct2.PublicId}) require.NoError(t, err) + assert.Equal(t, 1, num) // Deleting the account now removes the association with the user _, accts, err = iamRepo.LookupUser(ctx, usr.GetPublicId()) diff --git a/internal/db/schema/migrations/oss/postgres_58_01_test.go b/internal/db/schema/migrations/oss/postgres_58_01_test.go new file mode 100644 index 000000000..231aaf355 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres_58_01_test.go @@ -0,0 +1,103 @@ +package oss_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/testing/dbtest" + "github.com/stretchr/testify/require" +) + +func TestMigrations_NewForeignKeys(t *testing.T) { + const ( + priorMigration = 58001 + currentMigration = 58002 + ) + + t.Parallel() + ctx := context.Background() + dialect := dbtest.Postgres + + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) + }) + d, err := common.SqlOpen(dialect, u) + require.NoError(t, err) + + // migration to the prior migration (before the one we want to test) + m, err := schema.NewManager(ctx, schema.Dialect(dialect), d, schema.WithEditions( + schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": priorMigration}), + )) + require.NoError(t, err) + + _, err = m.ApplyMigrations(ctx) + require.NoError(t, err) + state, err := m.CurrentState(ctx) + require.NoError(t, err) + want := &schema.State{ + Initialized: true, + Editions: []schema.EditionState{ + { + Name: "oss", + BinarySchemaVersion: priorMigration, + DatabaseSchemaVersion: priorMigration, + DatabaseSchemaState: schema.Equal, + }, + }, + } + require.Equal(t, want, state) + + // Get a connection + dbType, err := db.StringToDbType(dialect) + require.NoError(t, err) + conn, err := db.Open(ctx, dbType, u) + require.NoError(t, err) + + // Create project + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + err = kmsCache.CreateKeys(ctx, scope.Global.String()) + require.NoError(t, err) + + // Create a password account + pwam := password.TestAuthMethod(t, conn, "global") + password.TestAccount(t, conn, pwam.PublicId, "user", password.WithPassword("password")) + + // Create an auth token + tok := authtoken.TestAuthToken(t, conn, kmsCache, "global") + _ = tok + + // now we're ready for the migration we want to test. + m, err = schema.NewManager(ctx, schema.Dialect(dialect), d, schema.WithEditions( + schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": currentMigration}), + )) + require.NoError(t, err) + + _, err = m.ApplyMigrations(ctx) + require.NoError(t, err) + state, err = m.CurrentState(ctx) + require.NoError(t, err) + want = &schema.State{ + Initialized: true, + Editions: []schema.EditionState{ + { + Name: "oss", + BinarySchemaVersion: currentMigration, + DatabaseSchemaVersion: currentMigration, + DatabaseSchemaState: schema.Equal, + }, + }, + } + require.Equal(t, want, state) + + // As long as there were no errors, the migration succeeded. +} diff --git a/internal/db/sqltest/Makefile b/internal/db/sqltest/Makefile index 9a5c3c698..774f71e70 100644 --- a/internal/db/sqltest/Makefile +++ b/internal/db/sqltest/Makefile @@ -25,7 +25,8 @@ TESTS ?= tests/setup/*.sql \ tests/session/*.sql \ tests/account/*/*.sql \ tests/target/*.sql \ - tests/controller/*.sql + tests/controller/*.sql \ + tests/kms/*.sql POSTGRES_DOCKER_IMAGE_BASE ?= postgres diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index 489adfd2f..5ace45326 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -29,6 +29,27 @@ begin; ('o_____colors', 'u______carly', 'Carly'), ('o_____colors', 'u______ciara', 'Ciara'); + -- Add keys + insert into kms_root_key + (private_id, scope_id) + values + ('krk_______colors', 'o_____colors'); + + insert into kms_root_key_version + (private_id, root_key_id, key) + values + ('krkv_______colors', 'krk_______colors', '_______color1'::bytea); + + insert into kms_data_key + (private_id, root_key_id, purpose) + values + ('kdk_______colors', 'krk_______colors', 'database'); + + insert into kms_data_key_version + (private_id, data_key_id, root_key_version_id, key) + values + ('kdkv_______colors', 'kdk_______colors', 'krkv_______colors', '_______color2'::bytea); + insert into iam_group (scope_id, public_id, name) values @@ -120,10 +141,10 @@ begin; insert into auth_token (key_id, auth_account_id, public_id, token) values - ('key', 'apa____clare', 'tok____clare', 'tok____clare'::bytea), - ('key', 'apa____cindy', 'tok____cindy', 'tok____cindy'::bytea), - ('key', 'apa____ciara', 'tok____ciara', 'tok____ciara'::bytea), - ('key', 'apa____carly', 'tok____carly', 'tok____carly'::bytea); + ('kdkv_______colors', 'apa____clare', 'tok____clare', 'tok____clare'::bytea), + ('kdkv_______colors', 'apa____cindy', 'tok____cindy', 'tok____cindy'::bytea), + ('kdkv_______colors', 'apa____ciara', 'tok____ciara', 'tok____ciara'::bytea), + ('kdkv_______colors', 'apa____carly', 'tok____carly', 'tok____carly'::bytea); insert into static_host (catalog_id, public_id, address) diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 22a0ae253..23b2be74f 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -115,6 +115,16 @@ begin; values ('kdkv___widget', 'kdk____widget', 'krkv___widget', 'kdk____widget'::bytea); + insert into kms_data_key_version_destruction_job + (key_id) + values + ('kdkv___widget'); + + insert into kms_data_key_version_destruction_job_run + (key_id, table_name, total_count) + values + ('kdkv___widget', 'auth_token', 100); + end; $$ language plpgsql; @@ -156,11 +166,11 @@ begin; insert into auth_token (key_id, auth_account_id, public_id, token) values - ('key', 'apa___walter', 'tok___walter', 'tok___walter'::bytea), - ('key', 'apa1__walter', 'tok1__walter', 'tok1__walter'::bytea), - ('key', 'apa___warren', 'tok___warren', 'tok___warren'::bytea), - ('key', 'apa___waylon', 'tok___waylon', 'tok___waylon'::bytea), - ('key', 'apa___wilson', 'tok___wilson', 'tok___wilson'::bytea); + ('kdkv___widget', 'apa___walter', 'tok___walter', 'tok___walter'::bytea), + ('kdkv___widget', 'apa1__walter', 'tok1__walter', 'tok1__walter'::bytea), + ('kdkv___widget', 'apa___warren', 'tok___warren', 'tok___warren'::bytea), + ('kdkv___widget', 'apa___waylon', 'tok___waylon', 'tok___waylon'::bytea), + ('kdkv___widget', 'apa___wilson', 'tok___wilson', 'tok___wilson'::bytea); insert into auth_oidc_method (scope_id, public_id, client_id, name, state, key_id, issuer) @@ -179,8 +189,8 @@ begin; insert into auth_token (key_id, auth_account_id, public_id, token) values - ('key', 'aoa___walter', 'oidc__walter', 'oidc__walter'::bytea), - ('key', 'aoa___warren', 'oidc__warren', 'oidc__warren'::bytea); + ('kdkv___widget', 'aoa___walter', 'oidc__walter', 'oidc__walter'::bytea), + ('kdkv___widget', 'aoa___warren', 'oidc__warren', 'oidc__warren'::bytea); end; $$ language plpgsql; diff --git a/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql b/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql index 27b14ce6b..602dadd7b 100644 --- a/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql +++ b/internal/db/sqltest/initdb.d/04_foodtruck_persona.sql @@ -14,6 +14,28 @@ begin; ('o__foodtruck', 'project', 'p______tacos', 'Tacos'), ('o__foodtruck', 'project', 'p_____nachos', 'Nachos'); + -- KMS + + insert into kms_root_key + (private_id, scope_id) + values + ('krk___foodtruck', 'o__foodtruck'); + + insert into kms_root_key_version + (private_id, root_key_id, key) + values + ('krkv___foodtruck', 'krk___foodtruck', 'krk___foodtruck'::bytea); + + insert into kms_data_key + (private_id, root_key_id, purpose) + values + ('kdk____foodtruck', 'krk___foodtruck', 'database'); + + insert into kms_data_key_version + (private_id, data_key_id, root_key_version_id, key) + values + ('kdkv___foodtruck', 'kdk____foodtruck', 'krkv___foodtruck', 'kdk____foodtruck'::bytea); + -- Host Catalog, Host, Host Set insert into static_host_catalog (project_id, public_id, name) @@ -77,8 +99,8 @@ begin; insert into auth_token (key_id, auth_account_id, public_id, token) values - ('key', 'apa______jim', 'tok______jim', 'tok______jim'::bytea), - ('key', 'apa______bob', 'tok______bob', 'tok______bob'::bytea); + ('kdkv___foodtruck', 'apa______jim', 'tok______jim', 'tok______jim'::bytea), + ('kdkv___foodtruck', 'apa______bob', 'tok______bob', 'tok______bob'::bytea); -- Roles insert into iam_role diff --git a/internal/db/sqltest/tests/kms/kms_data_key_destruction_job.sql b/internal/db/sqltest/tests/kms/kms_data_key_destruction_job.sql new file mode 100644 index 000000000..c3bb10349 --- /dev/null +++ b/internal/db/sqltest/tests/kms/kms_data_key_destruction_job.sql @@ -0,0 +1,126 @@ +begin; + select plan(15); + select wtt_load('widgets', 'iam', 'kms'); + + -- Should fail when inserting an unknown key_id + prepare job_insert_unknown_key_id as + insert into kms_data_key_version_destruction_job + (key_id) + values + ('kdkv___unknown'); + select throws_ok('job_insert_unknown_key_id', '23503', null, 'insert of unknown key_id in kms_data_key_version_destruction_job succeeded'); + + -- Should fail when inserting an already existing key_id + prepare insert_already_existing_key_id as + insert into kms_data_key_version_destruction_job + (key_id) + values + ('kdkv___widget'); + select throws_ok('insert_already_existing_key_id', '23505', null, 'insert of duplicate key_id in kms_data_key_version_destruction_job succeeded'); + + -- Should succeed when inserting new key_id + prepare insert_new_key_id as + insert into kms_data_key_version_destruction_job + (key_id) + values + ('kdkv_______colors'); + select lives_ok('insert_new_key_id', 'insert of valid key_id in kms_data_key_version_destruction_job failed'); + + -- Should fail when inserting an unknown table_name + prepare run_insert_unknown_table_name as + insert into kms_data_key_version_destruction_job_run + (key_id, table_name, total_count) + values + ('kdkv___widget', 'unknown_table', 100); + select throws_ok('run_insert_unknown_table_name', 'P0001', null, 'insert of unknown table_name in kms_data_key_version_destruction_job_run succeeded'); + + -- Should fail when inserting a duplicate (key_id, table_name) tuple + prepare run_insert_duplicate_key_id_table_name as + insert into kms_data_key_version_destruction_job_run + (key_id, table_name, total_count) + values + ('kdkv___widget', 'auth_token', 100); + select throws_ok('run_insert_duplicate_key_id_table_name', '23505', null, 'insert of duplicate (key_id, table_name) tuple in kms_data_key_version_destruction_job_run succeeded'); + + -- Should succeed when inserting a new, valid, (key_id, table_name) tuple + prepare run_insert_key_id_table_name as + insert into kms_data_key_version_destruction_job_run + (key_id, table_name, total_count) + values + ('kdkv___widget', 'auth_oidc_method', 100); + select lives_ok('run_insert_key_id_table_name', 'insert of valid (key_id, table_name) tuple in kms_data_key_version_destruction_job_run failed'); + + -- Should fail when setting completed_count > total_count + prepare run_update_invalid_completed_count as + update kms_data_key_version_destruction_job_run set + completed_count=101 + where + key_id='kdkv___widget' and table_name='auth_oidc_method'; + select throws_ok('run_update_invalid_completed_count', '23514', null, 'setting completed_count > total_count in kms_data_key_version_destruction_job_run succeeded'); + + -- Should succeed to set is_running to true + prepare run_update_is_running as + update kms_data_key_version_destruction_job_run set + is_running=true + where + key_id='kdkv___widget' and table_name='auth_oidc_method'; + select lives_ok('run_update_is_running', 'setting is_running=true with no other run running in kms_data_key_version_destruction_job_run failed'); + + -- Should fail to set is_running to true while another run is running + prepare run_update_invalid_is_running as + update kms_data_key_version_destruction_job_run set + is_running=true + where + key_id='kdkv___widget' and table_name='auth_token'; + select throws_ok('run_update_invalid_is_running', '23505', null, 'setting is_running=true while another run is running in kms_data_key_version_destruction_job_run succeeded'); + + -- Should fail to set completed_count=total_count without also setting is_running=false + prepare run_update_completed_count_without_is_running as + update kms_data_key_version_destruction_job_run set + completed_count=100 + where + key_id='kdkv___widget' and table_name='auth_oidc_method'; + select throws_ok('run_update_completed_count_without_is_running','23514', null, 'setting completed_count=total_count without also setting is_running=false in kms_data_key_version_destruction_job_run succeeded'); + + -- Should succed when setting completed_count=total_count and is_running=false + prepare run_update_completed_count_and_is_running as + update kms_data_key_version_destruction_job_run set + completed_count=100, + is_running=false + where + key_id='kdkv___widget' and table_name='auth_oidc_method'; + select lives_ok('run_update_completed_count_and_is_running', 'setting completed_count=total_count and is_running=false kms_data_key_version_destruction_job_run failed'); + + -- Should succeed to set is_running to true when no other run is running + prepare run_update_is_running_after_other_finished as + update kms_data_key_version_destruction_job_run set + is_running=true + where + key_id='kdkv___widget' and table_name='auth_token'; + select lives_ok('run_update_is_running_after_other_finished', 'setting is_running=true while no other run is running in kms_data_key_version_destruction_job_run failed'); + + -- Progress should report 'running' as one of the runs is running + prepare list_progress as + select key_id, scope_id, status, completed_count, total_count from kms_data_key_version_destruction_job_progress; + select results_eq( + 'list_progress', + $$VALUES ('kdkv___widget'::kms_private_id,'o_____widget'::kms_scope_id,'running',100::numeric,200::numeric)$$ + ); + + -- Should succed when setting completed_count=total_count and is_running=false + prepare run_update_completed_count_and_is_running_again as + update kms_data_key_version_destruction_job_run set + completed_count=100, + is_running=false + where + key_id='kdkv___widget' and table_name='auth_token'; + select lives_ok('run_update_completed_count_and_is_running_again', 'setting completed_count=total_count and is_running=false kms_data_key_version_destruction_job_run failed'); + + -- Progress should report 'completed' as all of the completed_count=total_count + select results_eq( + 'list_progress', + $$VALUES ('kdkv___widget'::kms_private_id,'o_____widget'::kms_scope_id,'completed',200::numeric,200::numeric)$$ + ); + + select * from finish(); +rollback; diff --git a/internal/db/testing.go b/internal/db/testing.go index 2b9dc66b8..642bfe44f 100644 --- a/internal/db/testing.go +++ b/internal/db/testing.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/boundary/internal/oplog/store" "github.com/hashicorp/boundary/testing/dbtest" "github.com/hashicorp/go-dbw" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" wrapping "github.com/hashicorp/go-kms-wrapping/v2" aead "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/stretchr/testify/assert" @@ -109,7 +110,7 @@ func TestSetupWithMock(t *testing.T) (*DB, sqlmock.Sqlmock) { return ret, mock } -// TestWrapper initializes an AEAD wrapping.Wrapper for testing the oplog +// TestWrapper initializes an AEAD wrapping.Wrapper for testing func TestWrapper(t testing.TB) wrapping.Wrapper { t.Helper() rootKey := make([]byte, 32) @@ -131,6 +132,25 @@ func TestWrapper(t testing.TB) wrapping.Wrapper { return root } +// TestDBWrapper initializes a DB wrapper for testing +func TestDBWrapper(t testing.TB, conn *DB, purpose kms.KeyPurpose) wrapping.Wrapper { + t.Helper() + purposes := []kms.KeyPurpose{purpose} + if purpose != "oplog" { + // Always add the oplog purpose, most tests need it + purposes = append(purposes, "oplog") + } + kmsCache, err := kms.New(dbw.New(conn.wrapped.Load()), dbw.New(conn.wrapped.Load()), purposes) + require.NoError(t, err) + err = kmsCache.AddExternalWrapper(context.Background(), kms.KeyPurposeRootKey, TestWrapper(t)) + require.NoError(t, err) + err = kmsCache.CreateKeys(context.Background(), "global", purposes) + require.NoError(t, err) + wrapper, err := kmsCache.GetWrapper(context.Background(), "global", purpose) + require.NoError(t, err) + return wrapper +} + // AssertPublicId is a test helper that asserts that the provided id is in // the format of a public id. func AssertPublicId(t testing.TB, prefix, actual string) { diff --git a/internal/db/testing_test.go b/internal/db/testing_test.go index 963247bd7..75af2d597 100644 --- a/internal/db/testing_test.go +++ b/internal/db/testing_test.go @@ -25,6 +25,7 @@ func TestVerifyOplogEntry(t *testing.T) { d, _ := TestSetup(t, "postgres") assert := assert.New(t) TestCreateTables(t, d) + wrapper := TestDBWrapper(t, d, "oplog") t.Run("valid", func(t *testing.T) { rw := New(d) @@ -37,7 +38,7 @@ func TestVerifyOplogEntry(t *testing.T) { context.Background(), user, WithOplog( - TestWrapper(t), + wrapper, oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, "resource-public-id": []string{user.GetPublicId()}, diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 1800eac87..ee57c64c6 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -2640,6 +2640,110 @@ ] } }, + "/v1/scopes/{id}:list-keys": { + "get": { + "summary": "List all keys in a Scope.", + "operationId": "ScopeService_ListKeys", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.ListKeysResponse" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.ScopeService" + ] + } + }, + "/v1/scopes/{scope_id}:list-key-version-destruction-jobs": { + "get": { + "summary": "Lists all pending key version destruction jobs in a Scope.", + "operationId": "ScopeService_ListKeyVersionDestructionJobs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.ListKeyVersionDestructionJobsResponse" + } + } + }, + "parameters": [ + { + "name": "scope_id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.ScopeService" + ] + } + }, + "/v1/scopes:destroy-key-version": { + "post": { + "summary": "Destroy the specified key version in a Scope. This may start an asynchronous job that re-encrypts all data encrypted by the specified key version. Use GET /v1/scopes/{scope_id}:list-key-version-destruction-jobs to monitor pending destruction jobs.", + "operationId": "ScopeService_DestroyKeyVersion", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.DestroyKeyVersionResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.api.services.v1.DestroyKeyVersionRequest" + } + } + ], + "tags": [ + "controller.api.services.v1.ScopeService" + ] + } + }, + "/v1/scopes:rotate-keys": { + "post": { + "summary": "Rotate all keys in a Scope.", + "operationId": "ScopeService_RotateKeys", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.RotateKeysResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.api.services.v1.RotateKeysRequest" + } + } + ], + "tags": [ + "controller.api.services.v1.ScopeService" + ] + } + }, "/v1/sessions": { "get": { "summary": "Lists all Sessions.", @@ -4945,6 +5049,93 @@ }, "title": "Role contains all fields related to a Role resource" }, + "controller.api.resources.scopes.v1.Key": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the Key." + }, + "scope": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.ScopeInfo", + "description": "Scope information for this resource." + }, + "purpose": { + "type": "string", + "description": "The purpose of the Key." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "The time this Key was created." + }, + "type": { + "type": "string", + "description": "The type of the Key." + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.KeyVersion" + }, + "description": "The versions of the key." + } + }, + "description": "Key contains all fields related to a Key in a Scope." + }, + "controller.api.resources.scopes.v1.KeyVersion": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the key version." + }, + "version": { + "type": "integer", + "format": "int64", + "description": "The iteration of the Key that this version represents." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this version was created." + } + }, + "title": "KeyVersion describes a specific version of a key and holds the actual key material" + }, + "controller.api.resources.scopes.v1.KeyVersionDestructionJob": { + "type": "object", + "properties": { + "key_version_id": { + "type": "string", + "description": "The ID of the Key version this job relates to." + }, + "scope": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.ScopeInfo", + "description": "Scope information for this resource." + }, + "status": { + "type": "string", + "description": "The current status of the key version destruction job. One of \"pending\", \"running\" or \"completed\"." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "The time this key version destruction job was created." + }, + "completed_count": { + "type": "string", + "format": "int64", + "description": "The number of rows that have been successfully re-encrypted with a new key version.\nAll rows must be re-encrypted before the key version can be destroyed." + }, + "total_count": { + "type": "string", + "format": "int64", + "description": "The total number of rows that need re-encrypting." + } + }, + "description": "KeyVersionDestructionJob holds information about a pending key version destruction job." + }, "controller.api.resources.scopes.v1.Scope": { "type": "object", "properties": { @@ -6116,6 +6307,26 @@ "controller.api.services.v1.DeleteWorkerResponse": { "type": "object" }, + "controller.api.services.v1.DestroyKeyVersionRequest": { + "type": "object", + "properties": { + "scope_id": { + "type": "string" + }, + "key_version_id": { + "type": "string" + } + } + }, + "controller.api.services.v1.DestroyKeyVersionResponse": { + "type": "object", + "properties": { + "state": { + "type": "string", + "description": "Destruction state. One of \"pending\" or \"completed\".\nUse GET /v1/scopes/{scope_id}:list-key-version-destruction-jobs\nto monitor pending destruction jobs." + } + } + }, "controller.api.services.v1.GetAccountResponse": { "type": "object", "properties": { @@ -6362,6 +6573,28 @@ } } }, + "controller.api.services.v1.ListKeyVersionDestructionJobsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.KeyVersionDestructionJob" + } + } + } + }, + "controller.api.services.v1.ListKeysResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.Key" + } + } + } + }, "controller.api.services.v1.ListManagedGroupsResponse": { "type": "object", "properties": { @@ -6519,6 +6752,20 @@ } } }, + "controller.api.services.v1.RotateKeysRequest": { + "type": "object", + "properties": { + "scope_id": { + "type": "string" + }, + "rewrap": { + "type": "boolean" + } + } + }, + "controller.api.services.v1.RotateKeysResponse": { + "type": "object" + }, "controller.api.services.v1.SetGroupMembersResponse": { "type": "object", "properties": { diff --git a/internal/gen/controller/api/services/scope_service.pb.go b/internal/gen/controller/api/services/scope_service.pb.go index 0d3c4b248..a978dc6dc 100644 --- a/internal/gen/controller/api/services/scope_service.pb.go +++ b/internal/gen/controller/api/services/scope_service.pb.go @@ -29,7 +29,7 @@ type GetScopeRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *GetScopeRequest) Reset() { @@ -123,9 +123,9 @@ type ListScopesRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty"` - Recursive bool `protobuf:"varint,20,opt,name=recursive,proto3" json:"recursive,omitempty"` - Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty"` + ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` + Recursive bool `protobuf:"varint,20,opt,name=recursive,proto3" json:"recursive,omitempty" class:"public"` // @gotags: `class:"public"` + Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListScopesRequest) Reset() { @@ -233,8 +233,8 @@ type CreateScopeRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SkipAdminRoleCreation bool `protobuf:"varint,1,opt,name=skip_admin_role_creation,json=skipAdminRoleCreation,proto3" json:"skip_admin_role_creation,omitempty"` - SkipDefaultRoleCreation bool `protobuf:"varint,2,opt,name=skip_default_role_creation,json=skipDefaultRoleCreation,proto3" json:"skip_default_role_creation,omitempty"` + SkipAdminRoleCreation bool `protobuf:"varint,1,opt,name=skip_admin_role_creation,json=skipAdminRoleCreation,proto3" json:"skip_admin_role_creation,omitempty" class:"public"` // @gotags: `class:"public"` + SkipDefaultRoleCreation bool `protobuf:"varint,2,opt,name=skip_default_role_creation,json=skipDefaultRoleCreation,proto3" json:"skip_default_role_creation,omitempty" class:"public"` // @gotags: `class:"public"` Item *scopes.Scope `protobuf:"bytes,3,opt,name=item,proto3" json:"item,omitempty"` } @@ -296,7 +296,7 @@ type CreateScopeResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty" class:"public"` // @gotags: `class:"public"` Item *scopes.Scope `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` } @@ -351,7 +351,7 @@ type UpdateScopeRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` Item *scopes.Scope `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,3,opt,name=update_mask,proto3" json:"update_mask,omitempty"` } @@ -461,7 +461,7 @@ type DeleteScopeRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *DeleteScopeRequest) Reset() { @@ -541,6 +541,392 @@ func (*DeleteScopeResponse) Descriptor() ([]byte, []int) { return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{9} } +type ListKeysRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *ListKeysRequest) Reset() { + *x = ListKeysRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListKeysRequest) ProtoMessage() {} + +func (x *ListKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[10] + 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 ListKeysRequest.ProtoReflect.Descriptor instead. +func (*ListKeysRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{10} +} + +func (x *ListKeysRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type ListKeysResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*scopes.Key `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *ListKeysResponse) Reset() { + *x = ListKeysResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListKeysResponse) ProtoMessage() {} + +func (x *ListKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[11] + 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 ListKeysResponse.ProtoReflect.Descriptor instead. +func (*ListKeysResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{11} +} + +func (x *ListKeysResponse) GetItems() []*scopes.Key { + if x != nil { + return x.Items + } + return nil +} + +type RotateKeysRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` + Rewrap bool `protobuf:"varint,2,opt,name=rewrap,proto3" json:"rewrap,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *RotateKeysRequest) Reset() { + *x = RotateKeysRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RotateKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateKeysRequest) ProtoMessage() {} + +func (x *RotateKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[12] + 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 RotateKeysRequest.ProtoReflect.Descriptor instead. +func (*RotateKeysRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{12} +} + +func (x *RotateKeysRequest) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *RotateKeysRequest) GetRewrap() bool { + if x != nil { + return x.Rewrap + } + return false +} + +type RotateKeysResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RotateKeysResponse) Reset() { + *x = RotateKeysResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RotateKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateKeysResponse) ProtoMessage() {} + +func (x *RotateKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[13] + 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 RotateKeysResponse.ProtoReflect.Descriptor instead. +func (*RotateKeysResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{13} +} + +type ListKeyVersionDestructionJobsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *ListKeyVersionDestructionJobsRequest) Reset() { + *x = ListKeyVersionDestructionJobsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListKeyVersionDestructionJobsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListKeyVersionDestructionJobsRequest) ProtoMessage() {} + +func (x *ListKeyVersionDestructionJobsRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[14] + 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 ListKeyVersionDestructionJobsRequest.ProtoReflect.Descriptor instead. +func (*ListKeyVersionDestructionJobsRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{14} +} + +func (x *ListKeyVersionDestructionJobsRequest) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +type ListKeyVersionDestructionJobsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*scopes.KeyVersionDestructionJob `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *ListKeyVersionDestructionJobsResponse) Reset() { + *x = ListKeyVersionDestructionJobsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListKeyVersionDestructionJobsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListKeyVersionDestructionJobsResponse) ProtoMessage() {} + +func (x *ListKeyVersionDestructionJobsResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[15] + 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 ListKeyVersionDestructionJobsResponse.ProtoReflect.Descriptor instead. +func (*ListKeyVersionDestructionJobsResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{15} +} + +func (x *ListKeyVersionDestructionJobsResponse) GetItems() []*scopes.KeyVersionDestructionJob { + if x != nil { + return x.Items + } + return nil +} + +type DestroyKeyVersionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` + KeyVersionId string `protobuf:"bytes,2,opt,name=key_version_id,json=keyVersionId,proto3" json:"key_version_id,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *DestroyKeyVersionRequest) Reset() { + *x = DestroyKeyVersionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DestroyKeyVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DestroyKeyVersionRequest) ProtoMessage() {} + +func (x *DestroyKeyVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[16] + 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 DestroyKeyVersionRequest.ProtoReflect.Descriptor instead. +func (*DestroyKeyVersionRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{16} +} + +func (x *DestroyKeyVersionRequest) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *DestroyKeyVersionRequest) GetKeyVersionId() string { + if x != nil { + return x.KeyVersionId + } + return "" +} + +type DestroyKeyVersionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Destruction state. One of "pending" or "completed". + // Use GET /v1/scopes/{scope_id}:list-key-version-destruction-jobs + // to monitor pending destruction jobs. + State string `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *DestroyKeyVersionResponse) Reset() { + *x = DestroyKeyVersionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DestroyKeyVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DestroyKeyVersionResponse) ProtoMessage() {} + +func (x *DestroyKeyVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_scope_service_proto_msgTypes[17] + 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 DestroyKeyVersionResponse.ProtoReflect.Descriptor instead. +func (*DestroyKeyVersionResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_scope_service_proto_rawDescGZIP(), []int{17} +} + +func (x *DestroyKeyVersionResponse) GetState() string { + if x != nil { + return x.State + } + return "" +} + var File_controller_api_services_v1_scope_service_proto protoreflect.FileDescriptor var file_controller_api_services_v1_scope_service_proto_rawDesc = []byte{ @@ -616,70 +1002,171 @@ var file_controller_api_services_v1_scope_service_proto_rawDesc = []byte{ 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe6, 0x06, 0x0a, 0x0c, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x08, - 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x0a, 0x0f, 0x4c, 0x69, + 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x51, 0x0a, + 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x22, 0x46, 0x0a, 0x11, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, + 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x6f, 0x74, 0x61, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, + 0x0a, 0x24, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, + 0x64, 0x22, 0x7b, 0x0a, 0x25, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, + 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x05, 0x69, 0x74, + 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4b, + 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x5b, + 0x0a, 0x18, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6b, + 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x31, 0x0a, 0x19, 0x44, + 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x32, 0x94, + 0x0f, 0x0a, 0x0c, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x9d, 0x01, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2b, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x92, 0x41, 0x16, 0x12, 0x14, 0x47, 0x65, + 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, + 0xbe, 0x01, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x2d, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, + 0x41, 0x3c, 0x12, 0x3a, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, + 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, + 0x12, 0xaa, 0x01, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, + 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x3a, 0x92, 0x41, 0x19, 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, + 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x73, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xa8, 0x01, + 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, + 0x92, 0x41, 0x12, 0x12, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, + 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x32, 0x0f, 0x2f, 0x76, 0x31, + 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x9c, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x12, 0x12, 0x10, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x2a, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xa6, 0x01, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, + 0x4b, 0x65, 0x79, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x3f, 0x92, 0x41, 0x1b, 0x12, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6b, + 0x65, 0x79, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x12, 0x19, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x73, + 0x12, 0xae, 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x12, + 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, 0x61, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, + 0x92, 0x41, 0x1d, 0x12, 0x1b, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x20, + 0x6b, 0x65, 0x79, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x73, 0x3a, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x3a, 0x01, + 0x2a, 0x12, 0xa4, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, + 0x6f, 0x62, 0x73, 0x12, 0x40, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, + 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x36, 0x92, 0x41, 0x16, 0x12, 0x14, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, - 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x17, 0x12, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xbe, 0x01, 0x0a, 0x0a, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x3c, 0x12, 0x3a, - 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, - 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, - 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0xaa, 0x01, 0x0a, - 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x92, - 0x41, 0x19, 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, - 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x18, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xa8, 0x01, 0x0a, 0x0b, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, 0x12, 0x12, - 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x32, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x12, 0x9c, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7e, 0x92, 0x41, 0x3c, 0x12, 0x3a, 0x4c, + 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x65, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x20, 0x69, 0x6e, + 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x12, + 0x37, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2d, 0x6a, 0x6f, 0x62, 0x73, 0x12, 0xaa, 0x03, 0x0a, 0x11, 0x44, 0x65, 0x73, + 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, + 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x12, 0x12, 0x10, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x11, 0x2a, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, - 0x69, 0x64, 0x7d, 0x42, 0x74, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, - 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x92, 0x41, 0x24, 0x12, 0x1e, 0x0a, 0x1c, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, - 0x79, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x20, 0x48, 0x54, 0x54, - 0x50, 0x20, 0x41, 0x50, 0x49, 0x2a, 0x02, 0x02, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa7, 0x02, 0x92, 0x41, + 0xfa, 0x01, 0x12, 0xf7, 0x01, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x6d, 0x61, 0x79, 0x20, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x6f, 0x75, + 0x73, 0x20, 0x6a, 0x6f, 0x62, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x72, 0x65, 0x2d, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x20, 0x55, 0x73, 0x65, 0x20, 0x47, 0x45, 0x54, 0x20, + 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x2d, 0x6a, 0x6f, 0x62, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x6d, 0x6f, 0x6e, 0x69, 0x74, + 0x6f, 0x72, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3a, 0x64, + 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x3a, 0x01, 0x2a, 0x42, 0x74, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x92, 0x41, 0x24, 0x12, 0x1e, 0x0a, 0x1c, 0x42, 0x6f, 0x75, 0x6e, 0x64, + 0x61, 0x72, 0x79, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x20, 0x48, + 0x54, 0x54, 0x50, 0x20, 0x41, 0x50, 0x49, 0x2a, 0x02, 0x02, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -694,44 +1181,64 @@ func file_controller_api_services_v1_scope_service_proto_rawDescGZIP() []byte { return file_controller_api_services_v1_scope_service_proto_rawDescData } -var file_controller_api_services_v1_scope_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_controller_api_services_v1_scope_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_controller_api_services_v1_scope_service_proto_goTypes = []interface{}{ - (*GetScopeRequest)(nil), // 0: controller.api.services.v1.GetScopeRequest - (*GetScopeResponse)(nil), // 1: controller.api.services.v1.GetScopeResponse - (*ListScopesRequest)(nil), // 2: controller.api.services.v1.ListScopesRequest - (*ListScopesResponse)(nil), // 3: controller.api.services.v1.ListScopesResponse - (*CreateScopeRequest)(nil), // 4: controller.api.services.v1.CreateScopeRequest - (*CreateScopeResponse)(nil), // 5: controller.api.services.v1.CreateScopeResponse - (*UpdateScopeRequest)(nil), // 6: controller.api.services.v1.UpdateScopeRequest - (*UpdateScopeResponse)(nil), // 7: controller.api.services.v1.UpdateScopeResponse - (*DeleteScopeRequest)(nil), // 8: controller.api.services.v1.DeleteScopeRequest - (*DeleteScopeResponse)(nil), // 9: controller.api.services.v1.DeleteScopeResponse - (*scopes.Scope)(nil), // 10: controller.api.resources.scopes.v1.Scope - (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask + (*GetScopeRequest)(nil), // 0: controller.api.services.v1.GetScopeRequest + (*GetScopeResponse)(nil), // 1: controller.api.services.v1.GetScopeResponse + (*ListScopesRequest)(nil), // 2: controller.api.services.v1.ListScopesRequest + (*ListScopesResponse)(nil), // 3: controller.api.services.v1.ListScopesResponse + (*CreateScopeRequest)(nil), // 4: controller.api.services.v1.CreateScopeRequest + (*CreateScopeResponse)(nil), // 5: controller.api.services.v1.CreateScopeResponse + (*UpdateScopeRequest)(nil), // 6: controller.api.services.v1.UpdateScopeRequest + (*UpdateScopeResponse)(nil), // 7: controller.api.services.v1.UpdateScopeResponse + (*DeleteScopeRequest)(nil), // 8: controller.api.services.v1.DeleteScopeRequest + (*DeleteScopeResponse)(nil), // 9: controller.api.services.v1.DeleteScopeResponse + (*ListKeysRequest)(nil), // 10: controller.api.services.v1.ListKeysRequest + (*ListKeysResponse)(nil), // 11: controller.api.services.v1.ListKeysResponse + (*RotateKeysRequest)(nil), // 12: controller.api.services.v1.RotateKeysRequest + (*RotateKeysResponse)(nil), // 13: controller.api.services.v1.RotateKeysResponse + (*ListKeyVersionDestructionJobsRequest)(nil), // 14: controller.api.services.v1.ListKeyVersionDestructionJobsRequest + (*ListKeyVersionDestructionJobsResponse)(nil), // 15: controller.api.services.v1.ListKeyVersionDestructionJobsResponse + (*DestroyKeyVersionRequest)(nil), // 16: controller.api.services.v1.DestroyKeyVersionRequest + (*DestroyKeyVersionResponse)(nil), // 17: controller.api.services.v1.DestroyKeyVersionResponse + (*scopes.Scope)(nil), // 18: controller.api.resources.scopes.v1.Scope + (*fieldmaskpb.FieldMask)(nil), // 19: google.protobuf.FieldMask + (*scopes.Key)(nil), // 20: controller.api.resources.scopes.v1.Key + (*scopes.KeyVersionDestructionJob)(nil), // 21: controller.api.resources.scopes.v1.KeyVersionDestructionJob } var file_controller_api_services_v1_scope_service_proto_depIdxs = []int32{ - 10, // 0: controller.api.services.v1.GetScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope - 10, // 1: controller.api.services.v1.ListScopesResponse.items:type_name -> controller.api.resources.scopes.v1.Scope - 10, // 2: controller.api.services.v1.CreateScopeRequest.item:type_name -> controller.api.resources.scopes.v1.Scope - 10, // 3: controller.api.services.v1.CreateScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope - 10, // 4: controller.api.services.v1.UpdateScopeRequest.item:type_name -> controller.api.resources.scopes.v1.Scope - 11, // 5: controller.api.services.v1.UpdateScopeRequest.update_mask:type_name -> google.protobuf.FieldMask - 10, // 6: controller.api.services.v1.UpdateScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope - 0, // 7: controller.api.services.v1.ScopeService.GetScope:input_type -> controller.api.services.v1.GetScopeRequest - 2, // 8: controller.api.services.v1.ScopeService.ListScopes:input_type -> controller.api.services.v1.ListScopesRequest - 4, // 9: controller.api.services.v1.ScopeService.CreateScope:input_type -> controller.api.services.v1.CreateScopeRequest - 6, // 10: controller.api.services.v1.ScopeService.UpdateScope:input_type -> controller.api.services.v1.UpdateScopeRequest - 8, // 11: controller.api.services.v1.ScopeService.DeleteScope:input_type -> controller.api.services.v1.DeleteScopeRequest - 1, // 12: controller.api.services.v1.ScopeService.GetScope:output_type -> controller.api.services.v1.GetScopeResponse - 3, // 13: controller.api.services.v1.ScopeService.ListScopes:output_type -> controller.api.services.v1.ListScopesResponse - 5, // 14: controller.api.services.v1.ScopeService.CreateScope:output_type -> controller.api.services.v1.CreateScopeResponse - 7, // 15: controller.api.services.v1.ScopeService.UpdateScope:output_type -> controller.api.services.v1.UpdateScopeResponse - 9, // 16: controller.api.services.v1.ScopeService.DeleteScope:output_type -> controller.api.services.v1.DeleteScopeResponse - 12, // [12:17] is the sub-list for method output_type - 7, // [7:12] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 18, // 0: controller.api.services.v1.GetScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope + 18, // 1: controller.api.services.v1.ListScopesResponse.items:type_name -> controller.api.resources.scopes.v1.Scope + 18, // 2: controller.api.services.v1.CreateScopeRequest.item:type_name -> controller.api.resources.scopes.v1.Scope + 18, // 3: controller.api.services.v1.CreateScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope + 18, // 4: controller.api.services.v1.UpdateScopeRequest.item:type_name -> controller.api.resources.scopes.v1.Scope + 19, // 5: controller.api.services.v1.UpdateScopeRequest.update_mask:type_name -> google.protobuf.FieldMask + 18, // 6: controller.api.services.v1.UpdateScopeResponse.item:type_name -> controller.api.resources.scopes.v1.Scope + 20, // 7: controller.api.services.v1.ListKeysResponse.items:type_name -> controller.api.resources.scopes.v1.Key + 21, // 8: controller.api.services.v1.ListKeyVersionDestructionJobsResponse.items:type_name -> controller.api.resources.scopes.v1.KeyVersionDestructionJob + 0, // 9: controller.api.services.v1.ScopeService.GetScope:input_type -> controller.api.services.v1.GetScopeRequest + 2, // 10: controller.api.services.v1.ScopeService.ListScopes:input_type -> controller.api.services.v1.ListScopesRequest + 4, // 11: controller.api.services.v1.ScopeService.CreateScope:input_type -> controller.api.services.v1.CreateScopeRequest + 6, // 12: controller.api.services.v1.ScopeService.UpdateScope:input_type -> controller.api.services.v1.UpdateScopeRequest + 8, // 13: controller.api.services.v1.ScopeService.DeleteScope:input_type -> controller.api.services.v1.DeleteScopeRequest + 10, // 14: controller.api.services.v1.ScopeService.ListKeys:input_type -> controller.api.services.v1.ListKeysRequest + 12, // 15: controller.api.services.v1.ScopeService.RotateKeys:input_type -> controller.api.services.v1.RotateKeysRequest + 14, // 16: controller.api.services.v1.ScopeService.ListKeyVersionDestructionJobs:input_type -> controller.api.services.v1.ListKeyVersionDestructionJobsRequest + 16, // 17: controller.api.services.v1.ScopeService.DestroyKeyVersion:input_type -> controller.api.services.v1.DestroyKeyVersionRequest + 1, // 18: controller.api.services.v1.ScopeService.GetScope:output_type -> controller.api.services.v1.GetScopeResponse + 3, // 19: controller.api.services.v1.ScopeService.ListScopes:output_type -> controller.api.services.v1.ListScopesResponse + 5, // 20: controller.api.services.v1.ScopeService.CreateScope:output_type -> controller.api.services.v1.CreateScopeResponse + 7, // 21: controller.api.services.v1.ScopeService.UpdateScope:output_type -> controller.api.services.v1.UpdateScopeResponse + 9, // 22: controller.api.services.v1.ScopeService.DeleteScope:output_type -> controller.api.services.v1.DeleteScopeResponse + 11, // 23: controller.api.services.v1.ScopeService.ListKeys:output_type -> controller.api.services.v1.ListKeysResponse + 13, // 24: controller.api.services.v1.ScopeService.RotateKeys:output_type -> controller.api.services.v1.RotateKeysResponse + 15, // 25: controller.api.services.v1.ScopeService.ListKeyVersionDestructionJobs:output_type -> controller.api.services.v1.ListKeyVersionDestructionJobsResponse + 17, // 26: controller.api.services.v1.ScopeService.DestroyKeyVersion:output_type -> controller.api.services.v1.DestroyKeyVersionResponse + 18, // [18:27] is the sub-list for method output_type + 9, // [9:18] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_controller_api_services_v1_scope_service_proto_init() } @@ -860,6 +1367,102 @@ func file_controller_api_services_v1_scope_service_proto_init() { return nil } } + file_controller_api_services_v1_scope_service_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListKeysRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListKeysResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RotateKeysRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RotateKeysResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListKeyVersionDestructionJobsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListKeyVersionDestructionJobsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DestroyKeyVersionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_scope_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DestroyKeyVersionResponse); 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{ @@ -867,7 +1470,7 @@ func file_controller_api_services_v1_scope_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_services_v1_scope_service_proto_rawDesc, NumEnums: 0, - NumMessages: 10, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/gen/controller/api/services/scope_service.pb.gw.go b/internal/gen/controller/api/services/scope_service.pb.gw.go index 9e65bd656..05e49cca0 100644 --- a/internal/gen/controller/api/services/scope_service.pb.gw.go +++ b/internal/gen/controller/api/services/scope_service.pb.gw.go @@ -323,6 +323,178 @@ func local_request_ScopeService_DeleteScope_0(ctx context.Context, marshaler run } +func request_ScopeService_ListKeys_0(ctx context.Context, marshaler runtime.Marshaler, client ScopeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListKeysRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.ListKeys(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ScopeService_ListKeys_0(ctx context.Context, marshaler runtime.Marshaler, server ScopeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListKeysRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.ListKeys(ctx, &protoReq) + return msg, metadata, err + +} + +func request_ScopeService_RotateKeys_0(ctx context.Context, marshaler runtime.Marshaler, client ScopeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RotateKeysRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.RotateKeys(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ScopeService_RotateKeys_0(ctx context.Context, marshaler runtime.Marshaler, server ScopeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RotateKeysRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.RotateKeys(ctx, &protoReq) + return msg, metadata, err + +} + +func request_ScopeService_ListKeyVersionDestructionJobs_0(ctx context.Context, marshaler runtime.Marshaler, client ScopeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListKeyVersionDestructionJobsRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["scope_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "scope_id") + } + + protoReq.ScopeId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "scope_id", err) + } + + msg, err := client.ListKeyVersionDestructionJobs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ScopeService_ListKeyVersionDestructionJobs_0(ctx context.Context, marshaler runtime.Marshaler, server ScopeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListKeyVersionDestructionJobsRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["scope_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "scope_id") + } + + protoReq.ScopeId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "scope_id", err) + } + + msg, err := server.ListKeyVersionDestructionJobs(ctx, &protoReq) + return msg, metadata, err + +} + +func request_ScopeService_DestroyKeyVersion_0(ctx context.Context, marshaler runtime.Marshaler, client ScopeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DestroyKeyVersionRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DestroyKeyVersion(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ScopeService_DestroyKeyVersion_0(ctx context.Context, marshaler runtime.Marshaler, server ScopeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DestroyKeyVersionRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DestroyKeyVersion(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterScopeServiceHandlerServer registers the http handlers for service ScopeService to "mux". // UnaryRPC :call ScopeServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -454,6 +626,106 @@ func RegisterScopeServiceHandlerServer(ctx context.Context, mux *runtime.ServeMu }) + mux.Handle("GET", pattern_ScopeService_ListKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/ListKeys", runtime.WithHTTPPathPattern("/v1/scopes/{id}:list-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ScopeService_ListKeys_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_ListKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_ScopeService_RotateKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/RotateKeys", runtime.WithHTTPPathPattern("/v1/scopes:rotate-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ScopeService_RotateKeys_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_RotateKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_ScopeService_ListKeyVersionDestructionJobs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/ListKeyVersionDestructionJobs", runtime.WithHTTPPathPattern("/v1/scopes/{scope_id}:list-key-version-destruction-jobs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ScopeService_ListKeyVersionDestructionJobs_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_ListKeyVersionDestructionJobs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_ScopeService_DestroyKeyVersion_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/DestroyKeyVersion", runtime.WithHTTPPathPattern("/v1/scopes:destroy-key-version")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ScopeService_DestroyKeyVersion_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_DestroyKeyVersion_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -605,6 +877,94 @@ func RegisterScopeServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu }) + mux.Handle("GET", pattern_ScopeService_ListKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/ListKeys", runtime.WithHTTPPathPattern("/v1/scopes/{id}:list-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ScopeService_ListKeys_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_ListKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_ScopeService_RotateKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/RotateKeys", runtime.WithHTTPPathPattern("/v1/scopes:rotate-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ScopeService_RotateKeys_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_RotateKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_ScopeService_ListKeyVersionDestructionJobs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/ListKeyVersionDestructionJobs", runtime.WithHTTPPathPattern("/v1/scopes/{scope_id}:list-key-version-destruction-jobs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ScopeService_ListKeyVersionDestructionJobs_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_ListKeyVersionDestructionJobs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_ScopeService_DestroyKeyVersion_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.ScopeService/DestroyKeyVersion", runtime.WithHTTPPathPattern("/v1/scopes:destroy-key-version")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ScopeService_DestroyKeyVersion_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ScopeService_DestroyKeyVersion_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -645,6 +1005,14 @@ var ( pattern_ScopeService_UpdateScope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "scopes", "id"}, "")) pattern_ScopeService_DeleteScope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "scopes", "id"}, "")) + + pattern_ScopeService_ListKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "scopes", "id"}, "list-keys")) + + pattern_ScopeService_RotateKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "scopes"}, "rotate-keys")) + + pattern_ScopeService_ListKeyVersionDestructionJobs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "scopes", "scope_id"}, "list-key-version-destruction-jobs")) + + pattern_ScopeService_DestroyKeyVersion_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "scopes"}, "destroy-key-version")) ) var ( @@ -657,4 +1025,12 @@ var ( forward_ScopeService_UpdateScope_0 = runtime.ForwardResponseMessage forward_ScopeService_DeleteScope_0 = runtime.ForwardResponseMessage + + forward_ScopeService_ListKeys_0 = runtime.ForwardResponseMessage + + forward_ScopeService_RotateKeys_0 = runtime.ForwardResponseMessage + + forward_ScopeService_ListKeyVersionDestructionJobs_0 = runtime.ForwardResponseMessage + + forward_ScopeService_DestroyKeyVersion_0 = runtime.ForwardResponseMessage ) diff --git a/internal/gen/controller/api/services/scope_service_grpc.pb.go b/internal/gen/controller/api/services/scope_service_grpc.pb.go index 5e765c343..e240c2a2c 100644 --- a/internal/gen/controller/api/services/scope_service_grpc.pb.go +++ b/internal/gen/controller/api/services/scope_service_grpc.pb.go @@ -44,6 +44,19 @@ type ScopeServiceClient interface { // DeleteScope remotes a Scope and all child resources from Boundary. If the // provided Scope IDs are malformed or not provided an error is returned. DeleteScope(ctx context.Context, in *DeleteScopeRequest, opts ...grpc.CallOption) (*DeleteScopeResponse, error) + // ListKeys lists all the keys found in the scope specified. If the scope + // is not found an error is returned. + ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error) + // RotateKeys rotates and optionally rewraps all the keys found in the + // scope specified. If the scope is not found an error is returned. If + // the scope is empty, the global scope is used. + RotateKeys(ctx context.Context, in *RotateKeysRequest, opts ...grpc.CallOption) (*RotateKeysResponse, error) + // ListKeyVersionDestructionJobs lists any pending key version destruction jobs in the scope. + ListKeyVersionDestructionJobs(ctx context.Context, in *ListKeyVersionDestructionJobsRequest, opts ...grpc.CallOption) (*ListKeyVersionDestructionJobsResponse, error) + // DestroyKeyVersion destroys the specified key version. If this requires re-encrypting + // existing data, it will start an asynchronous process to complete this operation + // before destroying the key. Use ListKeyVersionDestructionJobs to monitor pending destruction jobs. + DestroyKeyVersion(ctx context.Context, in *DestroyKeyVersionRequest, opts ...grpc.CallOption) (*DestroyKeyVersionResponse, error) } type scopeServiceClient struct { @@ -99,6 +112,42 @@ func (c *scopeServiceClient) DeleteScope(ctx context.Context, in *DeleteScopeReq return out, nil } +func (c *scopeServiceClient) ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error) { + out := new(ListKeysResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.ScopeService/ListKeys", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) RotateKeys(ctx context.Context, in *RotateKeysRequest, opts ...grpc.CallOption) (*RotateKeysResponse, error) { + out := new(RotateKeysResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.ScopeService/RotateKeys", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) ListKeyVersionDestructionJobs(ctx context.Context, in *ListKeyVersionDestructionJobsRequest, opts ...grpc.CallOption) (*ListKeyVersionDestructionJobsResponse, error) { + out := new(ListKeyVersionDestructionJobsResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.ScopeService/ListKeyVersionDestructionJobs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) DestroyKeyVersion(ctx context.Context, in *DestroyKeyVersionRequest, opts ...grpc.CallOption) (*DestroyKeyVersionResponse, error) { + out := new(DestroyKeyVersionResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.ScopeService/DestroyKeyVersion", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ScopeServiceServer is the server API for ScopeService service. // All implementations must embed UnimplementedScopeServiceServer // for forward compatibility @@ -129,6 +178,19 @@ type ScopeServiceServer interface { // DeleteScope remotes a Scope and all child resources from Boundary. If the // provided Scope IDs are malformed or not provided an error is returned. DeleteScope(context.Context, *DeleteScopeRequest) (*DeleteScopeResponse, error) + // ListKeys lists all the keys found in the scope specified. If the scope + // is not found an error is returned. + ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error) + // RotateKeys rotates and optionally rewraps all the keys found in the + // scope specified. If the scope is not found an error is returned. If + // the scope is empty, the global scope is used. + RotateKeys(context.Context, *RotateKeysRequest) (*RotateKeysResponse, error) + // ListKeyVersionDestructionJobs lists any pending key version destruction jobs in the scope. + ListKeyVersionDestructionJobs(context.Context, *ListKeyVersionDestructionJobsRequest) (*ListKeyVersionDestructionJobsResponse, error) + // DestroyKeyVersion destroys the specified key version. If this requires re-encrypting + // existing data, it will start an asynchronous process to complete this operation + // before destroying the key. Use ListKeyVersionDestructionJobs to monitor pending destruction jobs. + DestroyKeyVersion(context.Context, *DestroyKeyVersionRequest) (*DestroyKeyVersionResponse, error) mustEmbedUnimplementedScopeServiceServer() } @@ -151,6 +213,18 @@ func (UnimplementedScopeServiceServer) UpdateScope(context.Context, *UpdateScope func (UnimplementedScopeServiceServer) DeleteScope(context.Context, *DeleteScopeRequest) (*DeleteScopeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteScope not implemented") } +func (UnimplementedScopeServiceServer) ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListKeys not implemented") +} +func (UnimplementedScopeServiceServer) RotateKeys(context.Context, *RotateKeysRequest) (*RotateKeysResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RotateKeys not implemented") +} +func (UnimplementedScopeServiceServer) ListKeyVersionDestructionJobs(context.Context, *ListKeyVersionDestructionJobsRequest) (*ListKeyVersionDestructionJobsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListKeyVersionDestructionJobs not implemented") +} +func (UnimplementedScopeServiceServer) DestroyKeyVersion(context.Context, *DestroyKeyVersionRequest) (*DestroyKeyVersionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DestroyKeyVersion not implemented") +} func (UnimplementedScopeServiceServer) mustEmbedUnimplementedScopeServiceServer() {} // UnsafeScopeServiceServer may be embedded to opt out of forward compatibility for this service. @@ -254,6 +328,78 @@ func _ScopeService_DeleteScope_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _ScopeService_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListKeysRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).ListKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.ScopeService/ListKeys", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).ListKeys(ctx, req.(*ListKeysRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_RotateKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RotateKeysRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).RotateKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.ScopeService/RotateKeys", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).RotateKeys(ctx, req.(*RotateKeysRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_ListKeyVersionDestructionJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListKeyVersionDestructionJobsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).ListKeyVersionDestructionJobs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.ScopeService/ListKeyVersionDestructionJobs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).ListKeyVersionDestructionJobs(ctx, req.(*ListKeyVersionDestructionJobsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_DestroyKeyVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DestroyKeyVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).DestroyKeyVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.ScopeService/DestroyKeyVersion", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).DestroyKeyVersion(ctx, req.(*DestroyKeyVersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ScopeService_ServiceDesc is the grpc.ServiceDesc for ScopeService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -281,6 +427,22 @@ var ScopeService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteScope", Handler: _ScopeService_DeleteScope_Handler, }, + { + MethodName: "ListKeys", + Handler: _ScopeService_ListKeys_Handler, + }, + { + MethodName: "RotateKeys", + Handler: _ScopeService_RotateKeys_Handler, + }, + { + MethodName: "ListKeyVersionDestructionJobs", + Handler: _ScopeService_ListKeyVersionDestructionJobs_Handler, + }, + { + MethodName: "DestroyKeyVersion", + Handler: _ScopeService_DestroyKeyVersion_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controller/api/services/v1/scope_service.proto", diff --git a/internal/host/plugin/rewrapping.go b/internal/host/plugin/rewrapping.go new file mode 100644 index 000000000..4b838de8d --- /dev/null +++ b/internal/host/plugin/rewrapping.go @@ -0,0 +1,55 @@ +package plugin + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn("host_plugin_catalog_secret", hostCatalogSecretRewrapFn) +} + +func hostCatalogSecretRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "plugin.hostCatalogSecretRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var secrets []*HostCatalogSecret + // The only index on this table is on catalog id and there are no references to catalog id. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &secrets, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, secret := range secrets { + if err := secret.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt host catalog secret")) + } + if err := secret.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt host catalog secret")) + } + if _, err := writer.Update(ctx, secret, []string{"CtSecret", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update host catalog secret row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/host/plugin/rewrapping_test.go b/internal/host/plugin/rewrapping_test.go new file mode 100644 index 000000000..67ac58a67 --- /dev/null +++ b/internal/host/plugin/rewrapping_test.go @@ -0,0 +1,81 @@ +package plugin + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_hostCatalogSecretRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "host_plugin_catalog_secret" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := hostCatalogSecretRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test") + cat := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId()) + secret, err := newHostCatalogSecret(ctx, cat.GetPublicId(), mustStruct(map[string]any{ + "foo": "bar", + })) + assert.NoError(t, err) + assert.NotNil(t, secret) + assert.Empty(t, secret.CtSecret) + assert.NotEmpty(t, secret.Secret) + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + secretSecret := secret.GetSecret() // this is cleared by the encrypt function, so save it for assert + assert.NoError(t, secret.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(context.Background(), secret)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + assert.NoError(t, hostCatalogSecretRewrapFn(ctx, secret.GetKeyId(), prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := allocHostCatalogSecret() + got.CatalogId = cat.GetPublicId() + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, secret.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.Equal(t, secretSecret, got.GetSecret()) + assert.NotEqual(t, secret.GetCtSecret(), got.GetCtSecret()) + }) +} diff --git a/internal/kms/data_key_version_destruction_job.go b/internal/kms/data_key_version_destruction_job.go new file mode 100644 index 000000000..f0ff62e82 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job.go @@ -0,0 +1,31 @@ +package kms + +import ( + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +// DataKeyVersionDestructionJob is used to read and write +// data key version destruction jobs in the DB. +type DataKeyVersionDestructionJob struct { + *store.DataKeyVersionDestructionJob +} + +func (k *DataKeyVersionDestructionJob) TableName() string { + return "kms_data_key_version_destruction_job" +} + +// allocDataKeyVersionDestructionJob makes an empty one in memory. +func allocDataKeyVersionDestructionJob() DataKeyVersionDestructionJob { + return DataKeyVersionDestructionJob{ + DataKeyVersionDestructionJob: &store.DataKeyVersionDestructionJob{}, + } +} + +// Clone a DataKeyVersionDestructionJob +func (c *DataKeyVersionDestructionJob) Clone() *DataKeyVersionDestructionJob { + cp := proto.Clone(c.DataKeyVersionDestructionJob) + return &DataKeyVersionDestructionJob{ + DataKeyVersionDestructionJob: cp.(*store.DataKeyVersionDestructionJob), + } +} diff --git a/internal/kms/data_key_version_destruction_job_progress.go b/internal/kms/data_key_version_destruction_job_progress.go new file mode 100644 index 000000000..719360918 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_progress.go @@ -0,0 +1,31 @@ +package kms + +import ( + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +// DataKeyVersionDestructionJobProgress is used to read +// data key version destruction job progress from the DB. +type DataKeyVersionDestructionJobProgress struct { + *store.DataKeyVersionDestructionJobProgress +} + +func (k *DataKeyVersionDestructionJobProgress) TableName() string { + return "kms_data_key_version_destruction_job_progress" +} + +// allocDataKeyVersionDestructionJobProgress makes an empty one in memory. +func allocDataKeyVersionDestructionJobProgress() DataKeyVersionDestructionJobProgress { + return DataKeyVersionDestructionJobProgress{ + DataKeyVersionDestructionJobProgress: &store.DataKeyVersionDestructionJobProgress{}, + } +} + +// Clone an DataKeyVersionDestructionJob +func (c *DataKeyVersionDestructionJobProgress) Clone() *DataKeyVersionDestructionJobProgress { + cp := proto.Clone(c.DataKeyVersionDestructionJobProgress) + return &DataKeyVersionDestructionJobProgress{ + DataKeyVersionDestructionJobProgress: cp.(*store.DataKeyVersionDestructionJobProgress), + } +} diff --git a/internal/kms/data_key_version_destruction_job_progress_test.go b/internal/kms/data_key_version_destruction_job_progress_test.go new file mode 100644 index 000000000..6101d3087 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_progress_test.go @@ -0,0 +1,18 @@ +package kms + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_DataKeyVersionDestructionJobProgress(t *testing.T) { + progress1 := allocDataKeyVersionDestructionJobProgress() + progress1.KeyId = "abcd" + progress2 := progress1.Clone() + assert.Empty(t, cmp.Diff(progress1, progress2, protocmp.Transform())) + progress1.KeyId = "efgh" + assert.NotEmpty(t, cmp.Diff(progress1, progress2, protocmp.Transform())) +} diff --git a/internal/kms/data_key_version_destruction_job_run.go b/internal/kms/data_key_version_destruction_job_run.go new file mode 100644 index 000000000..c98212f28 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_run.go @@ -0,0 +1,31 @@ +package kms + +import ( + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +// DataKeyVersionDestructionJobRun is used to read and write +// data key version destruction job runs in the DB. +type DataKeyVersionDestructionJobRun struct { + *store.DataKeyVersionDestructionJobRun +} + +func (k *DataKeyVersionDestructionJobRun) TableName() string { + return "kms_data_key_version_destruction_job_run" +} + +// allocDataKeyVersionDestructionJobRun makes an empty one in memory. +func allocDataKeyVersionDestructionJobRun() DataKeyVersionDestructionJobRun { + return DataKeyVersionDestructionJobRun{ + DataKeyVersionDestructionJobRun: &store.DataKeyVersionDestructionJobRun{}, + } +} + +// Clone a DataKeyVersionDestructionJobRun +func (c *DataKeyVersionDestructionJobRun) Clone() *DataKeyVersionDestructionJobRun { + cp := proto.Clone(c.DataKeyVersionDestructionJobRun) + return &DataKeyVersionDestructionJobRun{ + DataKeyVersionDestructionJobRun: cp.(*store.DataKeyVersionDestructionJobRun), + } +} diff --git a/internal/kms/data_key_version_destruction_job_run_allowed_table_name.go b/internal/kms/data_key_version_destruction_job_run_allowed_table_name.go new file mode 100644 index 000000000..6cc6a1c7f --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_run_allowed_table_name.go @@ -0,0 +1,31 @@ +package kms + +import ( + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +// DataKeyVersionDestructionJobRunAllowedTableName is used to read +// the names of tables that reference the data key version. +type DataKeyVersionDestructionJobRunAllowedTableName struct { + *store.DataKeyVersionDestructionJobRunAllowedTableName +} + +func (k *DataKeyVersionDestructionJobRunAllowedTableName) TableName() string { + return "kms_data_key_version_destruction_job_run_allowed_table_name" +} + +// allocDataKeyVersionDestructionJobRunAllowedTableName makes an empty one in memory. +func allocDataKeyVersionDestructionJobRunAllowedTableName() DataKeyVersionDestructionJobRunAllowedTableName { + return DataKeyVersionDestructionJobRunAllowedTableName{ + DataKeyVersionDestructionJobRunAllowedTableName: &store.DataKeyVersionDestructionJobRunAllowedTableName{}, + } +} + +// Clone an DataKeyVersionDestructionJob +func (c *DataKeyVersionDestructionJobRunAllowedTableName) Clone() *DataKeyVersionDestructionJobRunAllowedTableName { + cp := proto.Clone(c.DataKeyVersionDestructionJobRunAllowedTableName) + return &DataKeyVersionDestructionJobRunAllowedTableName{ + DataKeyVersionDestructionJobRunAllowedTableName: cp.(*store.DataKeyVersionDestructionJobRunAllowedTableName), + } +} diff --git a/internal/kms/data_key_version_destruction_job_run_allowed_table_name_test.go b/internal/kms/data_key_version_destruction_job_run_allowed_table_name_test.go new file mode 100644 index 000000000..efec1e831 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_run_allowed_table_name_test.go @@ -0,0 +1,18 @@ +package kms + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_DataKeyVersionDestructionJobRunAllowedTableName(t *testing.T) { + allowedTableName1 := allocDataKeyVersionDestructionJobRunAllowedTableName() + allowedTableName1.DataKeyVersionDestructionJobRunAllowedTableName.TableName = "abcd" + allowedTableName2 := allowedTableName1.Clone() + assert.Empty(t, cmp.Diff(allowedTableName1, allowedTableName2, protocmp.Transform())) + allowedTableName1.DataKeyVersionDestructionJobRunAllowedTableName.TableName = "efgh" + assert.NotEmpty(t, cmp.Diff(allowedTableName1, allowedTableName2, protocmp.Transform())) +} diff --git a/internal/kms/data_key_version_destruction_job_run_test.go b/internal/kms/data_key_version_destruction_job_run_test.go new file mode 100644 index 000000000..5d5e19c6c --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_run_test.go @@ -0,0 +1,18 @@ +package kms + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_DataKeyVersionDestructionJobRun(t *testing.T) { + run1 := allocDataKeyVersionDestructionJobRun() + run1.KeyId = "abcd" + run2 := run1.Clone() + assert.Empty(t, cmp.Diff(run1, run2, protocmp.Transform())) + run1.KeyId = "efgh" + assert.NotEmpty(t, cmp.Diff(run1, run2, protocmp.Transform())) +} diff --git a/internal/kms/data_key_version_destruction_job_test.go b/internal/kms/data_key_version_destruction_job_test.go new file mode 100644 index 000000000..2aaec3a91 --- /dev/null +++ b/internal/kms/data_key_version_destruction_job_test.go @@ -0,0 +1,18 @@ +package kms + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_DataKeyVersionDestructionJob(t *testing.T) { + job1 := allocDataKeyVersionDestructionJob() + job1.KeyId = "abcd" + job2 := job1.Clone() + assert.Empty(t, cmp.Diff(job1, job2, protocmp.Transform())) + job1.KeyId = "efgh" + assert.NotEmpty(t, cmp.Diff(job1, job2, protocmp.Transform())) +} diff --git a/internal/kms/job/data_key_version_destruction_monitor_job.go b/internal/kms/job/data_key_version_destruction_monitor_job.go new file mode 100644 index 000000000..ee558f8a5 --- /dev/null +++ b/internal/kms/job/data_key_version_destruction_monitor_job.go @@ -0,0 +1,60 @@ +package job + +import ( + "context" + "time" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" +) + +type dataKeyVersionDestructionMonitorJob struct { + kmsRepo *kms.Kms +} + +func newDataKeyVersionDestructionMonitorJob(ctx context.Context, kmsRepo *kms.Kms) (*dataKeyVersionDestructionMonitorJob, error) { + const op = "kms.newDataKeyVersionDestructionMonitorJob" + if kmsRepo == nil { + return nil, errors.New(ctx, errors.Internal, "nil kms repo", op, errors.WithoutEvent()) + } + + return &dataKeyVersionDestructionMonitorJob{ + kmsRepo: kmsRepo, + }, nil +} + +// Status reports the job’s current status. We never change these values as +// this job never finishes. +func (d *dataKeyVersionDestructionMonitorJob) Status() scheduler.JobStatus { + return scheduler.JobStatus{} +} + +// Run performs the required work depending on the implementation. +// The context is used to notify the job that it should exit early. +func (r *dataKeyVersionDestructionMonitorJob) Run(ctx context.Context) error { + const op = "kmsjob.(dataKeyVersionDestructionMonitorJob).Run" + + if err := r.kmsRepo.MonitorDataKeyVersionDestruction(ctx); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +// NextRunIn returns the duration until the next job run should be scheduled. +// We report as ready 1 second after a successful run. This doesn't mean that +// this job will run every second, only about as often as the configured scheduler interval. +func (r *dataKeyVersionDestructionMonitorJob) NextRunIn(_ context.Context) (time.Duration, error) { + return time.Second, nil +} + +// Name is the unique name of the job. +func (r *dataKeyVersionDestructionMonitorJob) Name() string { + return "data-key-version-destruction-monitor-job" +} + +// Description is the human readable description of the job. +func (d *dataKeyVersionDestructionMonitorJob) Description() string { + return "Destroy a key version once all data has been rewrapped" +} diff --git a/internal/kms/job/data_key_version_destruction_monitor_job_test.go b/internal/kms/job/data_key_version_destruction_monitor_job_test.go new file mode 100644 index 000000000..e0598a884 --- /dev/null +++ b/internal/kms/job/data_key_version_destruction_monitor_job_test.go @@ -0,0 +1,22 @@ +package job + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +func Test_newDataKeyVersionDestructionMonitorJob(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, extWrapper) + + _, err := newDataKeyVersionDestructionMonitorJob(context.Background(), nil) + require.Error(t, err) + job, err := newDataKeyVersionDestructionMonitorJob(context.Background(), kmsCache) + require.NoError(t, err) + require.NotNil(t, job) +} diff --git a/internal/kms/job/job.go b/internal/kms/job/job.go new file mode 100644 index 000000000..eaf5d14b7 --- /dev/null +++ b/internal/kms/job/job.go @@ -0,0 +1,39 @@ +package job + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" +) + +// RegisterJobs registers kms related jobs with the provided scheduler. +func RegisterJobs(ctx context.Context, s *scheduler.Scheduler, kmsRepo *kms.Kms) error { + const op = "kmsjob.RegisterJobs" + if s == nil { + return errors.New(ctx, errors.Internal, "nil scheduler", op, errors.WithoutEvent()) + } + if kmsRepo == nil { + return errors.New(ctx, errors.Internal, "nil kms repo", op, errors.WithoutEvent()) + } + + dataKeyVersionDestructionMonitorJob, err := newDataKeyVersionDestructionMonitorJob(ctx, kmsRepo) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err := s.RegisterJob(ctx, dataKeyVersionDestructionMonitorJob); err != nil { + return errors.Wrap(ctx, err, op) + } + for _, tableName := range kms.ListTablesSupportingRewrap() { + tableRewrappingJob, err := newTableRewrappingJob(ctx, kmsRepo, tableName) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err := s.RegisterJob(ctx, tableRewrappingJob); err != nil { + return errors.Wrap(ctx, err, op) + } + } + + return nil +} diff --git a/internal/kms/job/job_test.go b/internal/kms/job/job_test.go new file mode 100644 index 000000000..1dc970810 --- /dev/null +++ b/internal/kms/job/job_test.go @@ -0,0 +1,27 @@ +package job + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/stretchr/testify/require" +) + +func Test_RegisterJobs(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, extWrapper) + s := scheduler.TestScheduler(t, conn, extWrapper) + + err := RegisterJobs(context.Background(), nil, nil) + require.Error(t, err) + err = RegisterJobs(context.Background(), s, nil) + require.Error(t, err) + err = RegisterJobs(context.Background(), nil, kmsCache) + require.Error(t, err) + err = RegisterJobs(context.Background(), s, kmsCache) + require.NoError(t, err) +} diff --git a/internal/kms/job/table_rewrapping_job.go b/internal/kms/job/table_rewrapping_job.go new file mode 100644 index 000000000..f964e18a1 --- /dev/null +++ b/internal/kms/job/table_rewrapping_job.go @@ -0,0 +1,66 @@ +package job + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" +) + +type tableRewrappingJob struct { + kmsRepo *kms.Kms + tableName string +} + +func newTableRewrappingJob(ctx context.Context, kmsRepo *kms.Kms, tableName string) (*tableRewrappingJob, error) { + const op = "kms.newTableRewrappingJob" + if kmsRepo == nil { + return nil, errors.New(ctx, errors.Internal, "nil kms repo", op, errors.WithoutEvent()) + } + if tableName == "" { + return nil, errors.New(ctx, errors.Internal, "no table name provided", op, errors.WithoutEvent()) + } + + return &tableRewrappingJob{ + kmsRepo: kmsRepo, + tableName: tableName, + }, nil +} + +// Status reports the job’s current status. We never change these values as +// this job never finishes. +func (d *tableRewrappingJob) Status() scheduler.JobStatus { + return scheduler.JobStatus{} +} + +// Run performs the required work depending on the implementation. +// The context is used to notify the job that it should exit early. +func (r *tableRewrappingJob) Run(ctx context.Context) error { + const op = "kmsjob.(tableRewrappingJob).Run" + + if err := r.kmsRepo.MonitorTableRewrappingRuns(ctx, r.tableName); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +// NextRunIn returns the duration until the next job run should be scheduled. +// We report as ready 1 second after a successful run. This doesn't mean that +// this job will run every second, only about as often as the configured scheduler interval. +func (r *tableRewrappingJob) NextRunIn(_ context.Context) (time.Duration, error) { + return time.Second, nil +} + +// Name is the unique name of the job. +func (r *tableRewrappingJob) Name() string { + return fmt.Sprintf("%s-rewrapping-job", r.tableName) +} + +// Description is the human readable description of the job. +func (d *tableRewrappingJob) Description() string { + return "Re-encrypt all data encrypted in a table using a specific key" +} diff --git a/internal/kms/job/table_rewrapping_job_test.go b/internal/kms/job/table_rewrapping_job_test.go new file mode 100644 index 000000000..e27d95b5e --- /dev/null +++ b/internal/kms/job/table_rewrapping_job_test.go @@ -0,0 +1,26 @@ +package job + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +func Test_newTableRewrappingJob(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, extWrapper) + + _, err := newTableRewrappingJob(context.Background(), nil, "") + require.Error(t, err) + _, err = newTableRewrappingJob(context.Background(), kmsCache, "") + require.Error(t, err) + _, err = newTableRewrappingJob(context.Background(), nil, "someTable") + require.Error(t, err) + job, err := newTableRewrappingJob(context.Background(), kmsCache, "someTable") + require.NoError(t, err) + require.NotNil(t, job) +} diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 949cd466b..19a94a037 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -2,6 +2,7 @@ package kms import ( "context" + "database/sql" "fmt" "io" "reflect" @@ -11,8 +12,10 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-dbw" wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "golang.org/x/exp/slices" ) // Kms is a way to access wrappers for a given scope and purpose. Since keys can @@ -21,6 +24,7 @@ import ( type Kms struct { underlying *wrappingKms.Kms reader db.Reader + writer db.Writer derivedPurposeCache sync.Map } @@ -43,6 +47,7 @@ func New(ctx context.Context, reader *db.Db, writer *db.Db, _ ...Option) (*Kms, return &Kms{ underlying: k, reader: reader, + writer: writer, }, nil } @@ -178,7 +183,7 @@ func (k *Kms) ReconcileKeys(ctx context.Context, randomReader io.Reader, opt ... return nil } -// CreateKeys creates the root key and DEKs returns a map of the new keys. +// CreateKeys creates the root key and DEKs. // Supports the WithRandomReader(...) and WithReaderWriter(...) options. // When WithReaderWriter(...) is used the caller is responsible for managing the // transaction which allows this capability to be shared with the iam repo when @@ -216,6 +221,356 @@ func (k *Kms) CreateKeys(ctx context.Context, scopeId string, opt ...Option) err return nil } +// ListKeys lists all keys in the scope. +// Options are ignored. +func (k *Kms) ListKeys(ctx context.Context, scopeId string, _ ...Option) ([]wrappingKms.Key, error) { + const op = "kms.(Kms).ListKeys" + keys, err := k.underlying.ListKeys(ctx, scopeId) + if err != nil { + if errors.Is(err, dbw.ErrRecordNotFound) { + return nil, errors.E(ctx, errors.WithCode(errors.RecordNotFound), errors.WithOp(op)) + } + return nil, errors.Wrap(ctx, err, op) + } + return keys, nil +} + +// RotateKeys rotates all keys in a given scope. +// Options supported: withRandomReader, withRewrap, withReader, withWriter +// When withReader or withWriter is used, both must be passed and the caller will +// be responsible for managing the underlying db transactions. +func (k *Kms) RotateKeys(ctx context.Context, scopeId string, opt ...Option) error { + const op = "kms.(Kms).RotateKeys" + + opts := getOpts(opt...) + kmsOpts := []wrappingKms.Option{ + wrappingKms.WithRandomReader(opts.withRandomReader), + wrappingKms.WithRewrap(opts.withRewrap), + } + + switch { + case !isNil(opts.withReader) && isNil(opts.withWriter): + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + case isNil(opts.withReader) && !isNil(opts.withWriter): + return errors.New(ctx, errors.InvalidParameter, op, "missing reader") + case !isNil(opts.withReader) && !isNil(opts.withWriter): + r, ok := opts.withReader.(*db.Db) + if !ok { + return errors.New(ctx, errors.InvalidParameter, op, "unable to convert reader to db.Db") + } + w, ok := opts.withWriter.(*db.Db) + if !ok { + return errors.New(ctx, errors.InvalidParameter, op, "unable to convert writer to db.Db") + } + kmsOpts = append(kmsOpts, wrappingKms.WithReaderWriter(db.NewChangeSafeDbwReader(r), db.NewChangeSafeDbwWriter(w))) + } + + err := k.underlying.RotateKeys(ctx, scopeId, kmsOpts...) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +// ListDataKeyVersionReferencers will list the names of all tables +// referencing the private_id column of the data key version table. +// Supported options: +// - WithReader (requires WithWriter) +// - WithWriter (requires WithReader) +func (k *Kms) ListDataKeyVersionReferencers(ctx context.Context, opt ...Option) ([]string, error) { + const op = "kms.(Kms).ListDataKeyVersionReferencers" + + opts := getOpts(opt...) + kmsOpts := []wrappingKms.Option{} + + switch { + case !isNil(opts.withReader) && isNil(opts.withWriter): + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer") + case isNil(opts.withReader) && !isNil(opts.withWriter): + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader") + case !isNil(opts.withReader) && !isNil(opts.withWriter): + r, ok := opts.withReader.(*db.Db) + if !ok { + return nil, errors.New(ctx, errors.InvalidParameter, op, "unable to convert reader to db.Db") + } + w, ok := opts.withWriter.(*db.Db) + if !ok { + return nil, errors.New(ctx, errors.InvalidParameter, op, "unable to convert writer to db.Db") + } + kmsOpts = append(kmsOpts, wrappingKms.WithReaderWriter(db.NewChangeSafeDbwReader(r), db.NewChangeSafeDbwWriter(w))) + } + + refs, err := k.underlying.ListDataKeyVersionReferencers(ctx, kmsOpts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return refs, nil +} + +// ListDataKeyVersionDestructionJobs lists any in-progress data key destruction jobs in the scope. +// Options are ignored. +func (k *Kms) ListDataKeyVersionDestructionJobs(ctx context.Context, scopeId string, _ ...Option) ([]*DataKeyVersionDestructionJobProgress, error) { + const op = "kms.(Kms).ListDataKeyVersionDestructionJobs" + if scopeId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + var jobs []*DataKeyVersionDestructionJobProgress + if err := k.reader.SearchWhere(ctx, &jobs, "scope_id=?", []any{scopeId}); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return jobs, nil +} + +// MonitorTableRewrappingRuns checks for pending rewrapping job runs for the +// specified table name, and attempts to execute each job run and start rewrapping +// data in the specified table. This may be a long running operation. +// Options are ignored. +func (k *Kms) MonitorTableRewrappingRuns(ctx context.Context, tableName string, _ ...Option) (retErr error) { + const op = "kms.(Kms).MonitorTableRewrappingRuns" + if tableName == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing table name") + } + + rewrapFn, ok := tableNameToRewrapFn[tableName] + if !ok { + return errors.E(ctx, errors.WithMsg("no rewrapper for table %q", tableName), errors.WithOp(op)) + } + + run := allocDataKeyVersionDestructionJobRun() + // Check if there is already another run running for another table name. If so, + // we exit early since we are limited to 1 run at a time. Note that we exclude + // our own table name from the search, since if there is a running run for our + // table name, thanks to the scheduler guaranteeing that only one instance of a job + // is running at a time, it means we were interrupted in our processing last time + // and should simply resume that running run. + err := k.reader.LookupWhere(ctx, &run, "is_running=true and table_name!=?", []any{tableName}) + switch { + case err == nil: + // Another run was already running + return nil + case errors.Match(errors.T(errors.RecordNotFound), err): + // Great, no running run, lets continue + default: + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to find any running runs")) + } + // Find the oldest or currently running run for our tablename + run = allocDataKeyVersionDestructionJobRun() + rows, err := k.reader.Query(ctx, oldestPendingOrRunningRun, []any{tableName}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query pending runs for %q", tableName)) + } + defer rows.Close() + for rows.Next() { + if err := k.reader.ScanRows(ctx, rows, &run); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to scan pending run for %q", tableName)) + } + } + if run.KeyId == "" { + // No queued runs, lets try again later + return nil + } + if !run.IsRunning { + // No running job, lets try to become the running job + run.IsRunning = true + if _, err := k.writer.Update(ctx, &run, []string{"IsRunning"}, nil); err != nil { + // Unique error means some other run started running since we last checked. + if errors.Match(errors.T(errors.NotUnique), err) { + return nil + } + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to become running run for %q", tableName)) + } + } + + defer func() { + if retErr != nil { + // Create new context in case we failed because the context was canceled + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + } + // Update the progress of this run + args := []any{ + sql.Named("key_id", run.KeyId), + sql.Named("table_name", tableName), + } + if _, err := k.writer.Exec(ctx, fmt.Sprintf(updateCompletedCountQueryTemplate, tableName), args); err != nil { + if retErr != nil { + // Just emit an error, don't mutate the returned error + _ = errors.E(ctx, errors.WithWrap(err)) + } else { + retErr = err + } + } + }() + + rows, err = k.reader.Query(ctx, dataKeyVersionIdScopeIdQuery, []any{run.KeyId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get scope id for data key version")) + } + defer rows.Close() + var scopeId string + for rows.Next() { + if err := k.reader.ScanRows(ctx, rows, &scopeId); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to scan scope id for data key version")) + } + } + + // Call the function to rewrap the data in the table. The progress will be automatically + // updated by the deferred function. + if err := rewrapFn(ctx, run.KeyId, scopeId, k.reader, k.writer, k); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +// MonitorDataKeyVersionDestruction monitors any pending destruction jobs. If +// a job has finished rewrapping all rows, it will destroy the key version +// by calling RevokeKeyVersion on the underlying kms. +func (k *Kms) MonitorDataKeyVersionDestruction(ctx context.Context) error { + const op = "kms.(Kms).MonitorDataKeyVersionDestruction" + + rows, err := k.reader.Query(ctx, finishedDestructionJobsQuery, nil) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to find completed destruction jobs")) + } + defer rows.Close() + var completedDataKeyVersionIds []string + for rows.Next() { + if err := k.reader.ScanRows(ctx, rows, &completedDataKeyVersionIds); err != nil { + return errors.Wrap(ctx, err, op) + } + } + for _, dataKeyVersionId := range completedDataKeyVersionIds { + // Finally, revoke the key, deleting it from the database. + // This will error if anything still references it that isn't + // meant to be cascade deleted. + if err := k.underlying.RevokeKeyVersion(ctx, dataKeyVersionId); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil +} + +// DestroyKeyVersion starts the process of destroying a key version in the scope. +// If the key version is a KEK, this simply rewraps any existing DEK version's encrypted with +// the KEK and destroys the key version. If the key version is a DEK, this creates a +// key version destruction job, which will rewrap existing data and destroy the key +// asynchronously. As a special case, if the DEK currently encrypts no data, the DEK is +// immediately destroyed. The boolean return value indicates whether the key was immediately +// destroyed. +// Options are ignored. +func (k *Kms) DestroyKeyVersion(ctx context.Context, scopeId string, keyVersionId string, _ ...Option) (bool, error) { + const op = "kms.(Kms).DestroyKeyVersion" + if scopeId == "" { + return false, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if keyVersionId == "" { + return false, errors.New(ctx, errors.InvalidParameter, op, "missing key version id") + } + + scopeKeys, err := k.underlying.ListKeys(ctx, scopeId) + if err != nil { + if errors.Is(err, dbw.ErrRecordNotFound) { + return false, errors.New(ctx, errors.RecordNotFound, op, "scope ID not found") + } + return false, errors.Wrap(ctx, err, op) + } + var foundKey *wrappingKms.Key +keyLoop: + for _, key := range scopeKeys { + for _, version := range key.Versions { + if version.Id == keyVersionId { + foundKey = &key + break keyLoop + } + } + } + if foundKey == nil { + return false, errors.New(ctx, errors.KeyNotFound, op, "key version was not found in the scope") + } + // Sort versions just in case they aren't already sorted + slices.SortFunc(foundKey.Versions, func(i, j wrappingKms.KeyVersion) bool { + return i.Version < j.Version + }) + if foundKey.Versions[len(foundKey.Versions)-1].Id == keyVersionId { + // Attempted to destroy currently active key + return false, errors.New(ctx, errors.InvalidParameter, op, "cannot destroy a currently active key version") + } + switch foundKey.Purpose { + case wrappingKms.KeyPurpose(KeyPurposeOplog.String()): + return false, errors.New(ctx, errors.InvalidParameter, op, "oplog key versions cannot be destroyed") + case wrappingKms.KeyPurposeRootKey: + // Simple case, just rewrap and destroy synchronously + if err := k.underlying.RewrapKeys(ctx, scopeId); err != nil { + return false, errors.Wrap(ctx, err, op) + } + if err := k.underlying.RevokeKeyVersion(ctx, keyVersionId); err != nil { + return false, errors.Wrap(ctx, err, op) + } + return true, nil + } + + tablesNeedingRewrapping := 0 + if _, err := k.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { + var tables []*DataKeyVersionDestructionJobRunAllowedTableName + if err := r.SearchWhere(ctx, &tables, "1=1", nil, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to look up allowed table names")) + } + // Start by adding any job runs necessary by looking up the number of + // rows encrypted with the key that's being destroyed. + for _, table := range tables { + rows, err := r.Query(ctx, fmt.Sprintf(findAffectedRowsForKeyQueryTemplate, table.GetTableName()), []any{keyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get affected rows for %q", table.GetTableName())) + } + defer rows.Close() + var numRows uint + for rows.Next() { + if err := r.ScanRows(ctx, rows, &numRows); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to scan number of rows for %q", table.GetTableName())) + } + } + if numRows == 0 { + // No rows to rewrap 🎉 + continue + } + run := allocDataKeyVersionDestructionJobRun() + run.KeyId = keyVersionId + run.DataKeyVersionDestructionJobRun.TableName = table.GetTableName() + run.TotalCount = int64(numRows) + if err := w.Create(ctx, &run); err != nil { + // Unique error means the key version ID already existed + if errors.Match(errors.T(errors.NotUnique), err) { + return errors.New(ctx, errors.InvalidParameter, op, "key version is already destroying", errors.WithWrap(err)) + } + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to insert new key version destruction job run")) + } + tablesNeedingRewrapping++ + } + if tablesNeedingRewrapping == 0 { + // Data key encrypted no data, just return + return nil + } + job := allocDataKeyVersionDestructionJob() + job.KeyId = keyVersionId + // Create the destruction job since we know we have at least one job run. + if err := w.Create(ctx, &job); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to insert new key version destruction job")) + } + return nil + }); err != nil { + return false, err + } + if tablesNeedingRewrapping == 0 { + // DEK encrypted no data, just destroy synchronously + if err := k.underlying.RevokeKeyVersion(ctx, keyVersionId); err != nil { + return false, errors.Wrap(ctx, err, op) + } + return true, nil + } + return false, err +} + // VerifyGlobalRoot will verify that the global root wrapper is reasonable. func (k *Kms) VerifyGlobalRoot(ctx context.Context) error { const op = "kms.(Kms).VerifyGlobalRoot" diff --git a/internal/kms/kms_test.go b/internal/kms/kms_test.go index 7d61ca4d2..a48d0bdd4 100644 --- a/internal/kms/kms_test.go +++ b/internal/kms/kms_test.go @@ -3,6 +3,7 @@ package kms import ( "context" "testing" + "time" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" @@ -46,6 +47,7 @@ func Test_New(t *testing.T) { w: rw, want: &Kms{ reader: rw, + writer: rw, underlying: func() *wrappingKms.Kms { purposes := make([]wrappingKms.KeyPurpose, 0, len(ValidDekPurposes())) for _, p := range ValidDekPurposes() { @@ -163,6 +165,776 @@ func Test_NewUsingReaderWriter(t *testing.T) { } } +func Test_ListKeys(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + t.Run("success", func(t *testing.T) { + t.Parallel() + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + require.Len(t, keys, len(ValidDekPurposes())+1) + }) + t.Run("unknown-scope", func(t *testing.T) { + t.Parallel() + _, err := kmsCache.ListKeys(testCtx, "myscope") + assert.True(t, errors.IsNotFoundError(err)) + }) + t.Run("empty-scope", func(t *testing.T) { + t.Parallel() + _, err := kmsCache.ListKeys(testCtx, "") + assert.Error(t, err) + }) +} + +func Test_RotateKeys(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + rw := db.New(conn) + + t.Run("success", func(t *testing.T) { + t.Parallel() + // arrange + // we're not trying to test the ListKeys function, although we need to use it to validate the rotation + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + require.Len(t, keys, len(ValidDekPurposes())+1) + + // act + err = kmsCache.RotateKeys(testCtx, "global") + require.NoError(t, err) + + // assert + keys, err = kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + + keyCount := 0 + for _, key := range keys { + keyCount += len(key.Versions) + } + + require.Equal(t, keyCount, 14) + }) + + t.Run("reader provided, missing writer", func(t *testing.T) { + t.Parallel() + // arrange + WithReader := func(reader db.Reader) Option { + return func(o *options) { + o.withReader = reader + } + } + + // act + err := kmsCache.RotateKeys(testCtx, "global", WithReader(rw)) + + // assert + assert.ErrorContains(t, err, "missing writer") + }) + + t.Run("writer provided, missing reader", func(t *testing.T) { + t.Parallel() + // arrange + WithWriter := func(writer db.Writer) Option { + return func(o *options) { + o.withWriter = writer + } + } + + // act + err := kmsCache.RotateKeys(testCtx, "global", WithWriter(rw)) + + // assert + assert.ErrorContains(t, err, "missing reader") + }) + + t.Run("invalid reader", func(t *testing.T) { + t.Parallel() + // act + err := kmsCache.RotateKeys(testCtx, "global", WithReaderWriter(&invalidReader{}, rw)) + + // assert + assert.ErrorContains(t, err, "unable to convert reader") + }) + + t.Run("invalid writer", func(t *testing.T) { + t.Parallel() + // act + err := kmsCache.RotateKeys(testCtx, "global", WithReaderWriter(rw, &invalidWriter{})) + + // assert + assert.ErrorContains(t, err, "unable to convert writer") + }) + + t.Run("both reader and writer succeed", func(t *testing.T) { + t.Parallel() + err = kmsCache.RotateKeys(testCtx, "global", WithReaderWriter(rw, rw)) + require.NoError(t, err) + }) + + // other options are passed directly and shouldn't need to be tested +} + +func Test_ListDataKeyVersionReferencers(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + t.Run("returns-something", func(t *testing.T) { + referencers, err := kmsCache.ListDataKeyVersionReferencers(testCtx) + require.NoError(t, err) + // Don't want to check the length or explicit tables + // as this will make the test fail whenever we add or + // remove a reference. Lets just sanity check that it's + // got at least one element in it. + assert.NotEmpty(t, referencers) + }) + t.Run("errors when just a writer is specified", func(t *testing.T) { + withWriter := func(writer db.Writer) Option { + return func(o *options) { + o.withWriter = writer + } + } + _, err := kmsCache.ListDataKeyVersionReferencers(testCtx, withWriter(rw)) + require.Error(t, err) + }) + t.Run("errors when just a reader is specified", func(t *testing.T) { + withReader := func(reader db.Reader) Option { + return func(o *options) { + o.withReader = reader + } + } + _, err := kmsCache.ListDataKeyVersionReferencers(testCtx, withReader(rw)) + require.Error(t, err) + }) + t.Run("errors when an invalid writer is specified", func(t *testing.T) { + _, err := kmsCache.ListDataKeyVersionReferencers(testCtx, WithReaderWriter(rw, &invalidWriter{})) + require.Error(t, err) + }) + t.Run("errors when an invalid reader is specified", func(t *testing.T) { + _, err := kmsCache.ListDataKeyVersionReferencers(testCtx, WithReaderWriter(&invalidReader{}, rw)) + require.Error(t, err) + }) + t.Run("succeeds-with-reader-writer", func(t *testing.T) { + referencers, err := kmsCache.ListDataKeyVersionReferencers(testCtx, WithReaderWriter(rw, rw)) + require.NoError(t, err) + // Don't want to check the length or explicit tables + // as this will make the test fail whenever we add or + // remove a reference. Lets just sanity check that it's + // got at least one element in it. + assert.NotEmpty(t, referencers) + }) +} + +func Test_ListDataKeyVersionDestructionJobs(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + err = kmsCache.RotateKeys(testCtx, "global") + require.NoError(t, err) + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + + t.Run("errors-on-empty-scope-ids", func(t *testing.T) { + _, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "") + assert.Error(t, err) + }) + t.Run("lists-no-jobs-when-the-scope-id-is-invalid", func(t *testing.T) { + jobs, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "notascope") + require.NoError(t, err) + assert.Empty(t, jobs) + }) + t.Run("lists-no-jobs-when-there-are-none", func(t *testing.T) { + jobs, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "global") + require.NoError(t, err) + assert.Empty(t, jobs) + }) + t.Run("lists-jobs-when-there-are-some", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + sqldb, err := conn.SqlDB(testCtx) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 200)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + jobs, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "global") + require.NoError(t, err) + require.Len(t, jobs, 1) + job := jobs[0] + assert.Equal(t, 0, int(job.CompletedCount)) + assert.Equal(t, 300, int(job.TotalCount)) + assert.Equal(t, "running", job.Status) + assert.Equal(t, kvToDestroy.Id, job.KeyId) + assert.Equal(t, "global", job.ScopeId) + }) + t.Run("lists-no-jobs-when-given-unknown-scope", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + sqldb, err := conn.SqlDB(testCtx) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 200)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + jobs, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "myscope") + require.NoError(t, err) + assert.Empty(t, jobs) + }) +} + +func TestMonitorTableRewrappingRuns(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + err = kmsCache.RotateKeys(testCtx, "global") + require.NoError(t, err) + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + sqldb, err := conn.SqlDB(testCtx) + require.NoError(t, err) + + t.Run("does-nothing-when-no-run-available", func(t *testing.T) { + callbackCalled := false + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + callbackCalled = true + return nil + } + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + require.NoError(t, err) + assert.False(t, callbackCalled, "auth_token callback should not have been called") + }) + t.Run("returns-an-error-when-given-empty-table-name", func(t *testing.T) { + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "") + require.Error(t, err) + }) + t.Run("returns-an-error-when-no-rewrapping-function-registered", func(t *testing.T) { + tableNameToRewrapFn = make(map[string]RewrapFn) + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + require.Error(t, err) + }) + t.Run("does-nothing-when-another-run-is-running", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 100)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_oidc_method'", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + callbackCalled := false + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + callbackCalled = true + return nil + } + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + require.NoError(t, err) + assert.False(t, callbackCalled, "auth_token callback should not have been called") + }) + t.Run("chooses-one-when-one-run-available", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + callbackCalled := make(chan struct{}) + returnFromCallback := make(chan struct{}) + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + close(callbackCalled) + assert.Equal(t, "global", scopeId) + // Block here until we want it to return + <-returnFromCallback + return nil + } + monitorErrCh := make(chan error) + // Run in goroutine so we can check status while the job is running + go func() { + monitorErrCh <- kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + }() + // Wait for callback to have been called + select { + case <-callbackCalled: + case err := <-monitorErrCh: + require.NoError(t, err) + case <-time.After(5 * time.Second): + t.Fatalf("Callback had not been called after 5 seconds") + return + } + // Now we know that the job is waiting for the callback to return, lets + // do some db inspection + row := sqldb.QueryRowContext(testCtx, "select is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + isRunning := false + err = row.Scan(&isRunning) + require.NoError(t, err) + assert.True(t, isRunning, "is_running should be set to true") + // Trigger callback to return + close(returnFromCallback) + // Wait for function to return + select { + case err := <-monitorErrCh: + require.NoError(t, err) + case <-time.After(5 * time.Second): + t.Fatalf("Function has not returned 5 seconds after callback finished") + return + } + // Lets look at the db state again after the function has returned + row = sqldb.QueryRowContext(testCtx, "select completed_count, is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + completedCount := 0 + isRunning = false + err = row.Scan(&completedCount, &isRunning) + require.NoError(t, err) + assert.EqualValues(t, completedCount, 100, "completed_count should have been updated") + assert.False(t, isRunning, "is_running should be set to false") + }) + t.Run("chooses-oldest-when-two-runs-available", func(t *testing.T) { + var kvsToDestroy []wrappingKms.KeyVersion + for _, key := range keys { + switch key.Purpose { + case wrappingKms.KeyPurpose(KeyPurposeDatabase.String()), wrappingKms.KeyPurpose(KeyPurposeTokens.String()): + kvsToDestroy = append(kvsToDestroy, key.Versions[0]) + } + } + require.Len(t, kvsToDestroy, 2) + for _, kvToDestroy := range kvsToDestroy { + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + } + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + rewrappedKeyVersion := "" + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + rewrappedKeyVersion = dataKeyVersionId + return nil + } + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + require.NoError(t, err) + assert.Equal(t, rewrappedKeyVersion, kvsToDestroy[0].Id, "auth_token callback should have been called with the oldest job") + }) + t.Run("resumes-running-one-when-two-runs-available-even-if-not-oldest", func(t *testing.T) { + var kvsToDestroy []wrappingKms.KeyVersion + for _, key := range keys { + switch key.Purpose { + case wrappingKms.KeyPurpose(KeyPurposeDatabase.String()), wrappingKms.KeyPurpose(KeyPurposeTokens.String()): + kvsToDestroy = append(kvsToDestroy, key.Versions[0]) + } + } + require.Len(t, kvsToDestroy, 2) + for i, kvToDestroy := range kvsToDestroy { + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + if i == 1 { + _, err = sqldb.ExecContext(testCtx, "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + require.NoError(t, err) + } + } + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + rewrappedKeyVersion := "" + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + rewrappedKeyVersion = dataKeyVersionId + return nil + } + err = kmsCache.MonitorTableRewrappingRuns(testCtx, "auth_token") + require.NoError(t, err) + assert.Equal(t, rewrappedKeyVersion, kvsToDestroy[1].Id, "auth_token callback should have been called with the already running job") + }) + t.Run("updates-the-running-state-even-when-context-canceled", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + callbackCalled := make(chan struct{}) + tableNameToRewrapFn["auth_token"] = func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + close(callbackCalled) + // Block here until we want it to return + <-ctx.Done() + return ctx.Err() + } + monitorErrCh := make(chan error) + newCtx, cancel := context.WithCancel(testCtx) + defer cancel() + // Run in goroutine so we can check status while the job is running + go func() { + monitorErrCh <- kmsCache.MonitorTableRewrappingRuns(newCtx, "auth_token") + }() + // Wait for callback to have been called + select { + case <-callbackCalled: + case err := <-monitorErrCh: + require.NoError(t, err) + case <-time.After(5 * time.Second): + t.Fatalf("Callback had not been called after 5 seconds") + return + } + // Now we know that the job is waiting for the callback to return, lets + // do some db inspection + row := sqldb.QueryRowContext(testCtx, "select is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + isRunning := false + err = row.Scan(&isRunning) + require.NoError(t, err) + assert.True(t, isRunning, "is_running should be set to true") + // Trigger callback to return + cancel() + // Wait for function to return + select { + case err := <-monitorErrCh: + require.True(t, errors.Is(err, context.Canceled)) + case <-time.After(5 * time.Second): + t.Fatalf("Function has not returned 5 seconds after callback finished") + return + } + // Lets look at the db state again after the function has returned + row = sqldb.QueryRowContext(testCtx, "select completed_count, is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + completedCount := 0 + isRunning = false + err = row.Scan(&completedCount, &isRunning) + require.NoError(t, err) + assert.EqualValues(t, completedCount, 100, "completed_count should have been updated") + assert.False(t, isRunning, "is_running should be set to false") + }) +} + +func TestMonitorDataKeyVersionDestruction(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + err = kmsCache.RotateKeys(testCtx, "global") + require.NoError(t, err) + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + sqldb, err := conn.SqlDB(testCtx) + require.NoError(t, err) + + t.Run("does-nothing-when-no-jobs-available", func(t *testing.T) { + err = kmsCache.MonitorDataKeyVersionDestruction(testCtx) + require.NoError(t, err) + }) + t.Run("does-nothing-when-the-job-isnt-completed", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + err = kmsCache.MonitorDataKeyVersionDestruction(testCtx) + require.NoError(t, err) + row := sqldb.QueryRowContext(testCtx, "select exists(select 1 from kms_data_key_version_destruction_job where key_id=$1)", kvToDestroy.Id) + exists := false + err = row.Scan(&exists) + require.NoError(t, err) + assert.True(t, exists, "the job should still exist") + }) + t.Run("deletes-the-key-when-the-job-is-completed", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job(key_id) values ($1)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)", kvToDestroy.Id) + require.NoError(t, err) + _, err = sqldb.ExecContext(testCtx, "update kms_data_key_version_destruction_job_run set completed_count=100 where key_id=$1 and table_name='auth_token'", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE") + require.NoError(t, err) + }) + err = kmsCache.MonitorDataKeyVersionDestruction(testCtx) + require.NoError(t, err) + row := sqldb.QueryRowContext(testCtx, "select exists(select 1 from kms_data_key_version_destruction_job where key_id=$1)", kvToDestroy.Id) + exists := false + err = row.Scan(&exists) + require.NoError(t, err) + // The job is deleted by virtue of cascading foreign key references + assert.False(t, exists, "the job should be deleted") + }) +} + +func TestDestroyKeyVersion(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + extWrapper := db.TestWrapper(t) + kmsCache := TestKms(t, conn, extWrapper) + err := kmsCache.CreateKeys(testCtx, "global") + require.NoError(t, err) + err = kmsCache.RotateKeys(testCtx, "global") + require.NoError(t, err) + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + sqldb, err := conn.SqlDB(testCtx) + require.NoError(t, err) + + t.Run("returns-an-error-when-given-an-empty-scope-id", func(t *testing.T) { + _, err := kmsCache.DestroyKeyVersion(testCtx, "", "krkv_DoesntExist") + assert.Error(t, err) + }) + t.Run("returns-an-error-when-given-an-empty-key-version-id", func(t *testing.T) { + _, err := kmsCache.DestroyKeyVersion(testCtx, "global", "") + assert.Error(t, err) + }) + t.Run("returns-an-error-when-attempting-to-destroy-an-unknown-key-version", func(t *testing.T) { + _, err := kmsCache.DestroyKeyVersion(testCtx, "global", "krkv_DoesntExist") + require.Error(t, err) + assert.True(t, errors.Match(errors.T(errors.KeyNotFound), err)) + }) + t.Run("returns-an-error-when-attempting-to-destroy-a-key-version-in-an-unknown-scope", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[len(key.Versions)-1] + } + } + _, err := kmsCache.DestroyKeyVersion(testCtx, "p_DoesntExist", kvToDestroy.Id) + require.Error(t, err) + assert.True(t, errors.Match(errors.T(errors.RecordNotFound), err)) + }) + t.Run("returns-an-error-when-attempting-to-destroy-latest-key-version", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[len(key.Versions)-1] + } + } + _, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.Error(t, err) + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }) + t.Run("returns-an-error-when-attempting-to-destroy-oplog-key-version", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeOplog.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.Error(t, err) + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }) + t.Run("succeeds-synchronously-when-destroying-old-root-key-version", func(t *testing.T) { + var chosenKey wrappingKms.Key + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeRootKey.String()) { + kvToDestroy = key.Versions[0] + chosenKey = key + } + } + destroyed, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.NoError(t, err) + assert.True(t, destroyed) + // Check that the key version is destroyed + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + for _, key := range keys { + if key.Id == chosenKey.Id { + require.Len(t, key.Versions, 1, "the root key should only have one version left") + assert.NotEqual(t, key.Versions[0].Id, kvToDestroy.Id) + } + } + }) + t.Run("succeeds-synchronously-when-destroying-old-data-key-version-that-encrypts-no-data", func(t *testing.T) { + require.NoError(t, err) + var chosenKey wrappingKms.Key + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeAudit.String()) { + kvToDestroy = key.Versions[0] + chosenKey = key + } + } + destroyed, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.NoError(t, err) + assert.True(t, destroyed) + // Check that the key version is destroyed + keys, err := kmsCache.ListKeys(testCtx, "global") + require.NoError(t, err) + for _, key := range keys { + if key.Id == chosenKey.Id { + require.Len(t, key.Versions, 1, "the data key should only have one version left") + assert.NotEqual(t, key.Versions[0].Id, kvToDestroy.Id) + } + } + }) + t.Run("creates-asynchronous-job-and-runs-for-data-key-version-that-encrypt-data", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + // Create a row that references the key we want to destroy. + // Note: This could break if the underlying table definitions change. + // Future contributors; forgive me, but we do really want to test this + // functionality here, and we can't import any of the normal test helpers + // since it would create circular dependencies. + _, err = sqldb.ExecContext(testCtx, "insert into worker_auth_ca_certificate(certificate, not_valid_after, public_key, private_key, state, issuing_ca, key_id) values ('certificate', CURRENT_TIMESTAMP+'1h'::interval, 'public_key', 'private_key', 'current', 'roots', $1)", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "delete from worker_auth_ca_certificate") + require.NoError(t, err) + }) + destroyed, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "delete from kms_data_key_version_destruction_job; delete from kms_data_key_version_destruction_job_run") + require.NoError(t, err) + }) + assert.False(t, destroyed) + // Check that the data key version destruction job was created + jobs, err := kmsCache.ListDataKeyVersionDestructionJobs(testCtx, "global") + require.NoError(t, err) + require.Len(t, jobs, 1) + assert.Equal(t, jobs[0].KeyId, kvToDestroy.Id) + assert.Equal(t, jobs[0].CompletedCount, int64(0)) + assert.Equal(t, jobs[0].TotalCount, int64(1)) + assert.Equal(t, jobs[0].Status, "pending") + assert.Equal(t, jobs[0].ScopeId, "global") + assert.True(t, jobs[0].CreateTime.AsTime().Before(time.Now())) + }) + t.Run("errors-when-destroying-key-that-is-already-destroying", func(t *testing.T) { + var kvToDestroy wrappingKms.KeyVersion + for _, key := range keys { + if key.Purpose == wrappingKms.KeyPurpose(KeyPurposeDatabase.String()) { + kvToDestroy = key.Versions[0] + } + } + _, err = sqldb.ExecContext(testCtx, "insert into worker_auth_ca_certificate(certificate, not_valid_after, public_key, private_key, state, issuing_ca, key_id) values ('certificate', CURRENT_TIMESTAMP+'1h'::interval, 'public_key', 'private_key', 'current', 'roots', $1)", kvToDestroy.Id) + require.NoError(t, err) + t.Cleanup(func() { + _, err = sqldb.ExecContext(testCtx, "delete from worker_auth_ca_certificate") + require.NoError(t, err) + }) + destroyed, err := kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.NoError(t, err) + assert.False(t, destroyed) + _, err = kmsCache.DestroyKeyVersion(testCtx, "global", kvToDestroy.Id) + require.Error(t, err) + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err), "error did not match InvalidParameter as expected: %v", err) + }) +} + +func Test_RegisterTableRewrapFn(t *testing.T) { + rewrapFn := func(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + return nil + } + RegisterTableRewrapFn("a_table", rewrapFn) + t.Cleanup(func() { + tableNameToRewrapFn = map[string]RewrapFn{} + }) + assert.Panics(t, func() { + RegisterTableRewrapFn("a_table", rewrapFn) + }) +} + +func Test_ListTablesSupportingRewrap(t *testing.T) { + assert.Empty(t, ListTablesSupportingRewrap()) + rewrapFn := func(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error { + return nil + } + RegisterTableRewrapFn("a_table", rewrapFn) + t.Cleanup(func() { + tableNameToRewrapFn = map[string]RewrapFn{} + }) + registeredTables := ListTablesSupportingRewrap() + assert.Len(t, registeredTables, 1) + assert.Equal(t, registeredTables[0], "a_table") +} + type invalidReader struct { db.Reader } diff --git a/internal/kms/option_test.go b/internal/kms/option_test.go index 2ed5562a6..5af71d139 100644 --- a/internal/kms/option_test.go +++ b/internal/kms/option_test.go @@ -42,4 +42,11 @@ func Test_GetOpts(t *testing.T) { testOpts.withKeyId = "100" assert.Equal(opts, testOpts) }) + t.Run("WithRewrap", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithRewrap(true)) + testOpts := getDefaultOptions() + testOpts.withRewrap = true + assert.Equal(opts, testOpts) + }) } diff --git a/internal/kms/options.go b/internal/kms/options.go index 6b97f8ed8..9ef37656a 100644 --- a/internal/kms/options.go +++ b/internal/kms/options.go @@ -32,6 +32,7 @@ type options struct { withRandomReader io.Reader withReader db.Reader withWriter db.Writer + withRewrap bool } func getDefaultOptions() options { @@ -120,3 +121,11 @@ func WithReaderWriter(r db.Reader, w db.Writer) Option { o.withWriter = w } } + +// WithRewrap allows for optionally specifying that the keys should be +// rewrapped. +func WithRewrap(enableRewrap bool) Option { + return func(o *options) { + o.withRewrap = enableRewrap + } +} diff --git a/internal/kms/query.go b/internal/kms/query.go new file mode 100644 index 000000000..eb7953c50 --- /dev/null +++ b/internal/kms/query.go @@ -0,0 +1,70 @@ +package kms + +const ( + // oldestPendingOrRunningRun lists the currently running or oldest + // run that has not yet completed for the specified table name. + oldestPendingOrRunningRun = ` +select + jr.key_id, jr.table_name, jr.total_count, jr.completed_count, jr.is_running +from + kms_data_key_version_destruction_job_run jr +inner join + kms_data_key_version_destruction_job j + on jr.key_id=j.key_id +where + jr.table_name=? and jr.completed_count!=jr.total_count +order by + jr.is_running desc, + j.create_time asc +limit + 1 +` + // updateCompletedCountQueryTemplate is used to update + // the completed count for a specific table after running. + // The interpolated variable is the table name. + updateCompletedCountQueryTemplate = ` +update kms_data_key_version_destruction_job_run +set + is_running=false, + completed_count=( + total_count-(select count(*) from %q where key_id=@key_id) + ) +where + key_id=@key_id and table_name=@table_name +` + // dataKeyVersionIdScopeIdQuery looks up the scopeId + // a specific data key version is in. + dataKeyVersionIdScopeIdQuery = ` +select + rk.scope_id +from + kms_root_key rk +inner join + kms_data_key dk + on dk.root_key_id=rk.private_id +inner join + kms_data_key_version dkv + on dkv.data_key_id=dk.private_id +where + dkv.private_id=? +` + // finishedDestructionJobsQuery returns all key version + // destruction jobs that have completed rewrapping all of their rows. + finishedDestructionJobsQuery = ` +select + j.key_id +from + kms_data_key_version_destruction_job j +inner join + kms_data_key_version_destruction_job_run jr + on j.key_id=jr.key_id +group by + (j.key_id) +having + sum(jr.total_count)=sum(jr.completed_count) +` + // findAffectedRowsForKeyQueryTemplate is used to find rows encrypted + // with a specific data key version ID in a table. The interpolated + // variable is the table name. + findAffectedRowsForKeyQueryTemplate = `select count(*) from %q where key_id=?` +) diff --git a/internal/kms/rewrapping.go b/internal/kms/rewrapping.go new file mode 100644 index 000000000..0eb3ac678 --- /dev/null +++ b/internal/kms/rewrapping.go @@ -0,0 +1,26 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "golang.org/x/exp/maps" +) + +type RewrapFn func(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kms *Kms) error + +var tableNameToRewrapFn = map[string]RewrapFn{} + +// RegisterTableRewrapFn registers a function to be used to rewrap data in a specific table with a new key +func RegisterTableRewrapFn(tableName string, rewrapFn RewrapFn) { + if _, ok := tableNameToRewrapFn[tableName]; ok { + panic(fmt.Sprintf("rewrap function for table name %q already exists", tableName)) + } + tableNameToRewrapFn[tableName] = rewrapFn +} + +// ListTablesSupportingRewrap lists all the table names registered with a rewrap function +func ListTablesSupportingRewrap() []string { + return maps.Keys(tableNameToRewrapFn) +} diff --git a/internal/kms/store/data_key_version_destruction_job.pb.go b/internal/kms/store/data_key_version_destruction_job.pb.go new file mode 100644 index 000000000..2a897e6e7 --- /dev/null +++ b/internal/kms/store/data_key_version_destruction_job.pb.go @@ -0,0 +1,176 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: controller/storage/kms/store/v1/data_key_version_destruction_job.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/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) +) + +// DataKeyVersionDestructionJob is used to read and write +// data from the kms_data_key_version_destruction_job table. +type DataKeyVersionDestructionJob struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // key_id is the private id of the data key version being destroyed. + // @inject_tag: `gorm:"primary_key"` + KeyId string `protobuf:"bytes,10,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"primary_key"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *DataKeyVersionDestructionJob) Reset() { + *x = DataKeyVersionDestructionJob{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataKeyVersionDestructionJob) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataKeyVersionDestructionJob) ProtoMessage() {} + +func (x *DataKeyVersionDestructionJob) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_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 DataKeyVersionDestructionJob.ProtoReflect.Descriptor instead. +func (*DataKeyVersionDestructionJob) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescGZIP(), []int{0} +} + +func (x *DataKeyVersionDestructionJob) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *DataKeyVersionDestructionJob) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_kms_store_v1_data_key_version_destruction_job_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDesc = []byte{ + 0x0a, 0x46, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, + 0x6f, 0x62, 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, 0x6b, 0x6d, 0x73, + 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, 0x82, 0x01, 0x0a, 0x1c, 0x44, + 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6b, + 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, + 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x14, 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, 0x42, + 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescData = file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_goTypes = []interface{}{ + (*DataKeyVersionDestructionJob)(nil), // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJob + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_depIdxs = []int32{ + 1, // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJob.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_init() } +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_init() { + if File_controller_storage_kms_store_v1_data_key_version_destruction_job_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataKeyVersionDestructionJob); 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_kms_store_v1_data_key_version_destruction_job_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_data_key_version_destruction_job_proto = out.File + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_rawDesc = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_goTypes = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_proto_depIdxs = nil +} diff --git a/internal/kms/store/data_key_version_destruction_job_progress.pb.go b/internal/kms/store/data_key_version_destruction_job_progress.pb.go new file mode 100644 index 000000000..60516ef29 --- /dev/null +++ b/internal/kms/store/data_key_version_destruction_job_progress.pb.go @@ -0,0 +1,225 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: controller/storage/kms/store/v1/data_key_version_destruction_job_progress.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/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) +) + +type DataKeyVersionDestructionJobProgress struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The id of the data key version that is being revoked + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,10,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // Scope id this data key version belongs to. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"not_null"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"not_null"` + // The status of this data key version destruction job + // One of "pending", "running" or "completed" + // @inject_tag: `gorm:"not_null"` + Status string `protobuf:"bytes,40,opt,name=status,proto3" json:"status,omitempty" gorm:"not_null"` + // The number of rows that have been rewrapped + // @inject_tag: `gorm:"not_null"` + CompletedCount int64 `protobuf:"varint,50,opt,name=completed_count,json=completedCount,proto3" json:"completed_count,omitempty" gorm:"not_null"` + // The total number of rows that need rewrapping + // @inject_tag: `gorm:"not_null"` + TotalCount int64 `protobuf:"varint,60,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty" gorm:"not_null"` +} + +func (x *DataKeyVersionDestructionJobProgress) Reset() { + *x = DataKeyVersionDestructionJobProgress{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataKeyVersionDestructionJobProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataKeyVersionDestructionJobProgress) ProtoMessage() {} + +func (x *DataKeyVersionDestructionJobProgress) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_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 DataKeyVersionDestructionJobProgress.ProtoReflect.Descriptor instead. +func (*DataKeyVersionDestructionJobProgress) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescGZIP(), []int{0} +} + +func (x *DataKeyVersionDestructionJobProgress) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *DataKeyVersionDestructionJobProgress) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *DataKeyVersionDestructionJobProgress) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *DataKeyVersionDestructionJobProgress) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *DataKeyVersionDestructionJobProgress) GetCompletedCount() int64 { + if x != nil { + return x.CompletedCount + } + return 0 +} + +func (x *DataKeyVersionDestructionJobProgress) GetTotalCount() int64 { + if x != nil { + return x.TotalCount + } + return 0 +} + +var File_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDesc = []byte{ + 0x0a, 0x4f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, + 0x6f, 0x62, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 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, 0x6b, 0x6d, 0x73, 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, 0x87, 0x02, 0x0a, 0x24, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4a, 0x6f, 0x62, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x15, 0x0a, 0x06, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, + 0x79, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x4b, + 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 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, 0x16, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x32, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x38, 0x5a, + 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescData = file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_goTypes = []interface{}{ + (*DataKeyVersionDestructionJobProgress)(nil), // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJobProgress + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_depIdxs = []int32{ + 1, // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJobProgress.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_init() +} +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_init() { + if File_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataKeyVersionDestructionJobProgress); 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_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto = out.File + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_rawDesc = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_goTypes = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_progress_proto_depIdxs = nil +} diff --git a/internal/kms/store/data_key_version_destruction_job_run.pb.go b/internal/kms/store/data_key_version_destruction_job_run.pb.go new file mode 100644 index 000000000..142b07c3e --- /dev/null +++ b/internal/kms/store/data_key_version_destruction_job_run.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: controller/storage/kms/store/v1/data_key_version_destruction_job_run.proto + +package store + +import ( + 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) +) + +// DataKeyVersionDestructionJobRun is used to read and write +// data from the kms_data_key_version_destruction_job_run table. +type DataKeyVersionDestructionJobRun struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // key_id is the private id of the data key version being destroyed. + // @inject_tag: `gorm:"primary_key"` + KeyId string `protobuf:"bytes,10,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"primary_key"` + // table_name is the table name that is being rewrapped by this run. + // @inject_tag: `gorm:"primary_key"` + TableName string `protobuf:"bytes,20,opt,name=table_name,json=tableName,proto3" json:"table_name,omitempty" gorm:"primary_key"` + // total_count is the total number of rows that need to be rewrapped + // in this table. + // @inject_tag: `gorm:"not_null"` + TotalCount int64 `protobuf:"varint,30,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty" gorm:"not_null"` + // completed_count is the number of rows that have completed rewrapping. + // @inject_tag: `gorm:"not_null"` + CompletedCount int64 `protobuf:"varint,40,opt,name=completed_count,json=completedCount,proto3" json:"completed_count,omitempty" gorm:"not_null"` + // is_running defines whether this run is currently running. Only + // one run is allowed to be running at a time. + // @inject_tag: `gorm:"not_null"` + IsRunning bool `protobuf:"varint,50,opt,name=is_running,json=isRunning,proto3" json:"is_running,omitempty" gorm:"not_null"` +} + +func (x *DataKeyVersionDestructionJobRun) Reset() { + *x = DataKeyVersionDestructionJobRun{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataKeyVersionDestructionJobRun) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataKeyVersionDestructionJobRun) ProtoMessage() {} + +func (x *DataKeyVersionDestructionJobRun) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_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 DataKeyVersionDestructionJobRun.ProtoReflect.Descriptor instead. +func (*DataKeyVersionDestructionJobRun) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescGZIP(), []int{0} +} + +func (x *DataKeyVersionDestructionJobRun) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *DataKeyVersionDestructionJobRun) GetTableName() string { + if x != nil { + return x.TableName + } + return "" +} + +func (x *DataKeyVersionDestructionJobRun) GetTotalCount() int64 { + if x != nil { + return x.TotalCount + } + return 0 +} + +func (x *DataKeyVersionDestructionJobRun) GetCompletedCount() int64 { + if x != nil { + return x.CompletedCount + } + return 0 +} + +func (x *DataKeyVersionDestructionJobRun) GetIsRunning() bool { + if x != nil { + return x.IsRunning + } + return false +} + +var File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDesc = []byte{ + 0x0a, 0x4a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, + 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 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, 0x6b, 0x6d, 0x73, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x22, 0xc0, 0x01, + 0x0a, 0x1f, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x75, + 0x6e, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x28, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, + 0x32, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescData = file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_goTypes = []interface{}{ + (*DataKeyVersionDestructionJobRun)(nil), // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJobRun +} +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_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_kms_store_v1_data_key_version_destruction_job_run_proto_init() } +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_init() { + if File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataKeyVersionDestructionJobRun); 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_kms_store_v1_data_key_version_destruction_job_run_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto = out.File + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_rawDesc = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_goTypes = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_proto_depIdxs = nil +} diff --git a/internal/kms/store/data_key_version_destruction_job_run_allowed_table_name.pb.go b/internal/kms/store/data_key_version_destruction_job_run_allowed_table_name.pb.go new file mode 100644 index 000000000..1db9dbd43 --- /dev/null +++ b/internal/kms/store/data_key_version_destruction_job_run_allowed_table_name.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: controller/storage/kms/store/v1/data_key_version_destruction_job_run_allowed_table_name.proto + +package store + +import ( + 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) +) + +type DataKeyVersionDestructionJobRunAllowedTableName struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The name of a table that is allowed to be used + // in a data key version destruction job run. + // @inject_tag: `gorm:"not_null"` + TableName string `protobuf:"bytes,10,opt,name=table_name,json=tableName,proto3" json:"table_name,omitempty" gorm:"not_null"` +} + +func (x *DataKeyVersionDestructionJobRunAllowedTableName) Reset() { + *x = DataKeyVersionDestructionJobRunAllowedTableName{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataKeyVersionDestructionJobRunAllowedTableName) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataKeyVersionDestructionJobRunAllowedTableName) ProtoMessage() {} + +func (x *DataKeyVersionDestructionJobRunAllowedTableName) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_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 DataKeyVersionDestructionJobRunAllowedTableName.ProtoReflect.Descriptor instead. +func (*DataKeyVersionDestructionJobRunAllowedTableName) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescGZIP(), []int{0} +} + +func (x *DataKeyVersionDestructionJobRunAllowedTableName) GetTableName() string { + if x != nil { + return x.TableName + } + return "" +} + +var File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDesc = []byte{ + 0x0a, 0x5d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, + 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 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, 0x6b, 0x6d, 0x73, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, + 0x22, 0x50, 0x0a, 0x2f, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, + 0x52, 0x75, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x6d, 0x73, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescData = file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_goTypes = []interface{}{ + (*DataKeyVersionDestructionJobRunAllowedTableName)(nil), // 0: controller.storage.kms.store.v1.DataKeyVersionDestructionJobRunAllowedTableName +} +var file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_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_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_init() +} +func file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_init() { + if File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataKeyVersionDestructionJobRunAllowedTableName); 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_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto = out.File + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_rawDesc = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_goTypes = nil + file_controller_storage_kms_store_v1_data_key_version_destruction_job_run_allowed_table_name_proto_depIdxs = nil +} diff --git a/internal/kms/testing.go b/internal/kms/testing.go index 0d2311467..027170554 100644 --- a/internal/kms/testing.go +++ b/internal/kms/testing.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/oplog" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/require" ) @@ -31,6 +32,7 @@ func TestKmsDeleteKeyPurpose(t testing.TB, conn *db.DB, purpose KeyPurpose) { // TestKmsDeleteAllKeys allows you to delete all the keys for testing. func TestKmsDeleteAllKeys(t testing.TB, conn *db.DB) { + oplog.TestOplogDeleteAllEntries(t, db.New(conn).UnderlyingDB()()) db.TestDeleteWhere(t, conn, func() any { i := rootKey{}; return &i }(), "1=1") } diff --git a/internal/oplog/immutable_fields_test.go b/internal/oplog/immutable_fields_test.go index cec5811bd..1ce5fce14 100644 --- a/internal/oplog/immutable_fields_test.go +++ b/internal/oplog/immutable_fields_test.go @@ -17,8 +17,7 @@ import ( func Test_ImmutableFields(t *testing.T) { testCtx := context.Background() - db := setup(t) - cipherer := testWrapper(t) + db, wrapper := setup(testCtx, t) writer := &Writer{db} id := testId(t) @@ -47,7 +46,7 @@ func Test_ImmutableFields(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(t, err) diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index c1b39aa0b..7bd8de1ee 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -9,6 +9,7 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/oplog/store" + "github.com/hashicorp/boundary/internal/util" "github.com/hashicorp/go-dbw" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" @@ -37,7 +38,7 @@ type Message struct { // Entry represents an oplog entry type Entry struct { *store.Entry - Cipherer wrapping.Wrapper `gorm:"-"` + Wrapper wrapping.Wrapper `gorm:"-"` Ticketer Ticketer `gorm:"-"` } @@ -45,14 +46,22 @@ type Entry struct { type Metadata map[string][]string // NewEntry creates a new Entry -func NewEntry(ctx context.Context, aggregateName string, metadata Metadata, cipherer wrapping.Wrapper, ticketer Ticketer) (*Entry, error) { +func NewEntry(ctx context.Context, aggregateName string, metadata Metadata, wrapper wrapping.Wrapper, ticketer Ticketer) (*Entry, error) { const op = "oplog.NewEntry" + if util.IsNil(wrapper) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil wrapper") + } + keyId, err := wrapper.KeyId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } entry := Entry{ Entry: &store.Entry{ AggregateName: aggregateName, Version: Version, + KeyId: keyId, }, - Cipherer: cipherer, + Wrapper: wrapper, Ticketer: ticketer, } if len(metadata) > 0 { @@ -77,10 +86,10 @@ func NewEntry(ctx context.Context, aggregateName string, metadata Metadata, ciph func (e *Entry) validate(ctx context.Context) error { const op = "oplog.(Entry).validate" - if e.Cipherer == nil { - return errors.New(ctx, errors.InvalidParameter, op, "nil cipherer") + if util.IsNil(e.Wrapper) { + return errors.New(ctx, errors.InvalidParameter, op, "nil wrapper") } - if e.Ticketer == nil { + if util.IsNil(e.Ticketer) { return errors.New(ctx, errors.InvalidParameter, op, "nil ticketer") } if e.Entry == nil { @@ -92,6 +101,9 @@ func (e *Entry) validate(ctx context.Context) error { if e.Entry.AggregateName == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing entry aggregate name") } + if e.Entry.KeyId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing entry key id") + } return nil } @@ -123,6 +135,7 @@ func (e *Entry) UnmarshalData(ctx context.Context, types *TypeCatalog) ([]Messag } dbwOpts, err := convertToDbwOpts(ctx, item.operationOptions) if err != nil { + return nil, errors.Wrap(ctx, err, op) } msgs = append(msgs, Message{ Message: item.msg, @@ -297,7 +310,7 @@ func convertFromDbwOpts(ctx context.Context, opts dbw.Options) (*OperationOption } // WriteEntryWith the []proto.Message marshaled into the entry data as a FIFO QueueBuffer -// if Cipherer != nil then the data is authentication encrypted +// if Wrapper != nil then the data is authentication encrypted func (e *Entry) WriteEntryWith(ctx context.Context, tx *Writer, ticket *store.Ticket, msgs ...*Message) error { const op = "oplog.(Entry).WriteEntryWith" if tx == nil { @@ -323,7 +336,7 @@ func (e *Entry) WriteEntryWith(ctx context.Context, tx *Writer, ticket *store.Ti } e.Data = append(e.Data, queue.Bytes()...) - if e.Cipherer != nil { + if e.Wrapper != nil { if err := e.encryptData(ctx); err != nil { return errors.Wrap(ctx, err, op) } @@ -339,7 +352,7 @@ func (e *Entry) WriteEntryWith(ctx context.Context, tx *Writer, ticket *store.Ti } // Write the entry as is with whatever it has for e.Data marshaled into a FIFO QueueBuffer -// If Cipherer != nil then the data is authentication encrypted +// If Wrapper != nil then the data is authentication encrypted func (e *Entry) Write(ctx context.Context, tx *Writer, ticket *store.Ticket) error { const op = "oplog.(Entry).Write" if err := e.validate(ctx); err != nil { @@ -351,7 +364,7 @@ func (e *Entry) Write(ctx context.Context, tx *Writer, ticket *store.Ticket) err if ticket.Version == 0 { return errors.New(ctx, errors.InvalidParameter, op, "missing ticket version") } - if e.Cipherer != nil { + if e.Wrapper != nil { if err := e.encryptData(ctx); err != nil { return errors.Wrap(ctx, err, op) } @@ -366,21 +379,21 @@ func (e *Entry) Write(ctx context.Context, tx *Writer, ticket *store.Ticket) err return nil } -// encryptData the entry's data using its Cipherer (wrapping.Wrapper) +// encryptData the entry's data using its Wrapper (wrapping.Wrapper) func (e *Entry) encryptData(ctx context.Context) error { const op = "oplog.(Entry).EncryptData" // structwrapping doesn't support embedding, so we'll pass in the store.Entry directly - if err := structwrapping.WrapStruct(ctx, e.Cipherer, e.Entry, nil); err != nil { + if err := structwrapping.WrapStruct(ctx, e.Wrapper, e.Entry, nil); err != nil { return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) } return nil } -// DecryptData will decrypt the entry's data using its Cipherer (wrapping.Wrapper) +// DecryptData will decrypt the entry's data using its Wrapper (wrapping.Wrapper) func (e *Entry) DecryptData(ctx context.Context) error { const op = "oplog.(Entry).DecryptData" // structwrapping doesn't support embedding, so we'll pass in the store.Entry directly - if err := structwrapping.UnwrapStruct(ctx, e.Cipherer, e.Entry, nil); err != nil { + if err := structwrapping.UnwrapStruct(ctx, e.Wrapper, e.Entry, nil); err != nil { return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) } return nil diff --git a/internal/oplog/oplog_test.go b/internal/oplog/oplog_test.go index e2948520f..c8213c0e1 100644 --- a/internal/oplog/oplog_test.go +++ b/internal/oplog/oplog_test.go @@ -5,10 +5,9 @@ import ( "fmt" "testing" + "github.com/hashicorp/boundary/internal/oplog/oplog_test" dbassert "github.com/hashicorp/dbassert/gorm" "github.com/hashicorp/go-dbw" - - "github.com/hashicorp/boundary/internal/oplog/oplog_test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,12 +17,13 @@ import ( // Test_BasicOplog provides some basic unit tests for oplogs func Test_BasicOplog(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, wrapper := setup(testCtx, t) t.Run("EncryptData/DecryptData/UnmarshalData", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - cipherer := testWrapper(t) + keyId, err := wrapper.KeyId(testCtx) + require.NoError(err) // now let's us optimistic locking via a ticketing system for a serialized oplog ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) @@ -46,7 +46,7 @@ func Test_BasicOplog(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -64,9 +64,11 @@ func Test_BasicOplog(t *testing.T) { var foundEntry Entry err = dbw.New(db).LookupWhere(testCtx, &foundEntry, "id = ?", []any{entryId}) require.NoError(err) - foundEntry.Cipherer = cipherer + foundEntry.Wrapper = wrapper err = foundEntry.DecryptData(context.Background()) require.NoError(err) + require.Equal(keyId, foundEntry.KeyId) + require.Equal("global", foundEntry.ScopeId) foundUsers, err := foundEntry.UnmarshalData(testCtx, types) require.NoError(err) @@ -78,8 +80,6 @@ func Test_BasicOplog(t *testing.T) { t.Run("write entry", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - cipherer := testWrapper(t) - // now let's us optimistic locking via a ticketing system for a serialized oplog ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) @@ -105,7 +105,7 @@ func Test_BasicOplog(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -118,16 +118,17 @@ func Test_BasicOplog(t *testing.T) { // Test_NewEntry provides some basic unit tests for NewEntry func Test_NewEntry(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, wrapper := setup(testCtx, t) t.Run("valid", func(t *testing.T) { require := require.New(t) - cipherer := testWrapper(t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) + keyId, err := wrapper.KeyId(testCtx) + require.NoError(err) - _, err = NewEntry( + entry, err := NewEntry( testCtx, "test-users", Metadata{ @@ -135,14 +136,14 @@ func Test_NewEntry(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) + require.Equal(keyId, entry.KeyId) }) t.Run("no metadata success", func(t *testing.T) { require := require.New(t) - cipherer := testWrapper(t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) @@ -150,14 +151,13 @@ func Test_NewEntry(t *testing.T) { testCtx, "test-users", nil, - cipherer, + wrapper, ticketer, ) require.NoError(err) }) t.Run("no aggregateName", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - cipherer := testWrapper(t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) @@ -169,13 +169,13 @@ func Test_NewEntry(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.Error(err) assert.Equal("oplog.NewEntry: oplog.(Entry).validate: missing entry aggregate name: parameter violation: error #100", err.Error()) }) - t.Run("bad cipherer", func(t *testing.T) { + t.Run("bad wrapper", func(t *testing.T) { assert, require := assert.New(t), require.New(t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) @@ -192,11 +192,10 @@ func Test_NewEntry(t *testing.T) { ticketer, ) require.Error(err) - assert.Equal("oplog.NewEntry: oplog.(Entry).validate: nil cipherer: parameter violation: error #100", err.Error()) + assert.Equal("oplog.NewEntry: nil wrapper: parameter violation: error #100", err.Error()) }) t.Run("bad ticket", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - cipherer := testWrapper(t) _, err := NewEntry( testCtx, "test-users", @@ -205,7 +204,7 @@ func Test_NewEntry(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, nil, ) require.Error(err) @@ -214,10 +213,8 @@ func Test_NewEntry(t *testing.T) { } func Test_UnmarshalData(t *testing.T) { - db := setup(t) - - cipherer := testWrapper(t) testCtx := context.Background() + db, wrapper := setup(testCtx, t) // now let's us optimistic locking via a ticketing system for a serialized oplog ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) @@ -245,7 +242,7 @@ func Test_UnmarshalData(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -270,7 +267,7 @@ func Test_UnmarshalData(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -297,7 +294,7 @@ func Test_UnmarshalData(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -323,7 +320,7 @@ func Test_UnmarshalData(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -338,10 +335,9 @@ func Test_UnmarshalData(t *testing.T) { // Test_Replay provides some basic unit tests for replaying entries func Test_Replay(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, wrapper := setup(testCtx, t) - cipherer := testWrapper(t) id := testId(t) // setup new tables for replay @@ -368,7 +364,7 @@ func Test_Replay(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -426,7 +422,7 @@ func Test_Replay(t *testing.T) { var foundEntry Entry require.NoError(tx.LookupWhere(testCtx, &foundEntry, "id = ?", []any{newLogEntry.Id})) - foundEntry.Cipherer = cipherer + foundEntry.Wrapper = wrapper err = foundEntry.DecryptData(context.Background()) require.NoError(err) @@ -484,7 +480,7 @@ func Test_Replay(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -497,7 +493,7 @@ func Test_Replay(t *testing.T) { var foundEntry2 Entry err = tx2.LookupWhere(testCtx, &foundEntry2, "id = ?", []any{newLogEntry2.Id}) require.NoError(err) - foundEntry2.Cipherer = cipherer + foundEntry2.Wrapper = wrapper err = foundEntry2.DecryptData(context.Background()) require.NoError(err) @@ -548,7 +544,7 @@ func Test_Replay(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -561,7 +557,7 @@ func Test_Replay(t *testing.T) { var foundEntry2 Entry err = tx2.LookupWhere(testCtx, &foundEntry2, "id = ?", []any{newLogEntry2.Id}) require.NoError(err) - foundEntry2.Cipherer = cipherer + foundEntry2.Wrapper = wrapper err = foundEntry2.DecryptData(context.Background()) require.NoError(err) @@ -583,9 +579,8 @@ func Test_Replay(t *testing.T) { // Test_WriteEntryWith provides unit tests for oplog.WriteEntryWith func Test_WriteEntryWith(t *testing.T) { - db := setup(t) testCtx := context.Background() - cipherer := testWrapper(t) + db, wrapper := setup(testCtx, t) id := testId(t) u := oplog_test.TestUser{ @@ -615,7 +610,7 @@ func Test_WriteEntryWith(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -627,7 +622,7 @@ func Test_WriteEntryWith(t *testing.T) { var foundEntry Entry require.NoError(dbw.New(db).LookupWhere(testCtx, &foundEntry, "id = ?", []any{newLogEntry.Id})) require.NoError(err) - foundEntry.Cipherer = cipherer + foundEntry.Wrapper = wrapper types, err := NewTypeCatalog(testCtx, Type{new(oplog_test.TestUser), "user"}) require.NoError(err) err = foundEntry.DecryptData(context.Background()) @@ -646,7 +641,7 @@ func Test_WriteEntryWith(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -666,7 +661,7 @@ func Test_WriteEntryWith(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -686,7 +681,7 @@ func Test_WriteEntryWith(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -697,9 +692,9 @@ func Test_WriteEntryWith(t *testing.T) { } func TestEntry_WriteEntryWith(t *testing.T) { + assert, require := assert.New(t), require.New(t) testCtx := context.Background() - db := setup(t) - cipherer := testWrapper(t) + db, wrapper := setup(testCtx, t) db.Debug(true) // setup new tables for replay @@ -707,7 +702,7 @@ func TestEntry_WriteEntryWith(t *testing.T) { tableSuffix := "_" + id ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) - require.NoError(t, err) + require.NoError(err) newEntryFn := func() *Entry { testEntry, err := NewEntry( @@ -718,10 +713,10 @@ func TestEntry_WriteEntryWith(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) - require.NoError(t, err) + require.NoError(err) return testEntry } @@ -851,7 +846,6 @@ func TestEntry_WriteEntryWith(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) ticket, err := ticketer.GetTicket(testCtx, "default") require.NoError(err) err = tt.e.WriteEntryWith( @@ -871,7 +865,7 @@ func TestEntry_WriteEntryWith(t *testing.T) { var foundEntry Entry require.NoError(dbw.New(db).LookupWhere(testCtx, &foundEntry, "id = ?", []any{tt.e.Id})) require.NoError(err) - foundEntry.Cipherer = cipherer + foundEntry.Wrapper = wrapper types, err := NewTypeCatalog(testCtx, Type{new(oplog_test.TestUser), "user"}) require.NoError(err) err = foundEntry.DecryptData(context.Background()) @@ -891,7 +885,7 @@ func TestEntry_WriteEntryWith(t *testing.T) { testMsgOpts := dbw.GetOpts(tt.msg.Opts...) assert.Equal(entryOpts, testMsgOpts) - foundEntry.Cipherer = cipherer + foundEntry.Wrapper = wrapper err = foundEntry.DecryptData(context.Background()) require.NoError(err) err = foundEntry.Replay(context.Background(), tt.w, types, tableSuffix) @@ -913,15 +907,13 @@ func TestEntry_WriteEntryWith(t *testing.T) { // Test_TicketSerialization provides unit tests for making sure oplog.Tickets properly serialize writes to oplog entries func Test_TicketSerialization(t *testing.T) { - db := setup(t) assert, require := assert.New(t), require.New(t) testCtx := context.Background() + db, wrapper := setup(testCtx, t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(err) - cipherer := testWrapper(t) - id := testId(t) firstTx, err := dbw.New(db).Begin(testCtx) require.NoError(err) @@ -945,7 +937,7 @@ func Test_TicketSerialization(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) @@ -974,7 +966,7 @@ func Test_TicketSerialization(t *testing.T) { "deployment": []string{"amex"}, "project": []string{"central-info-systems", "local-info-systems"}, }, - cipherer, + wrapper, ticketer, ) require.NoError(err) diff --git a/internal/oplog/store/oplog.pb.go b/internal/oplog/store/oplog.pb.go index 061ab1761..ed2ffe1c4 100644 --- a/internal/oplog/store/oplog.pb.go +++ b/internal/oplog/store/oplog.pb.go @@ -49,6 +49,13 @@ type Entry struct { // we are NOT storing this plain-text entry data in the db // @inject_tag: gorm:"-" wrapping:"pt,entry_data" Data []byte `protobuf:"bytes,8,opt,name=data,proto3" json:"data,omitempty" gorm:"-" wrapping:"pt,entry_data"` + // the private id of the data key version used to encrypt the data. + // @inject_tag: gorm:"not_null" + KeyId string `protobuf:"bytes,9,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // the scope id associated with the key used to encrypt the data. + // This value is populated from the key_id in a trigger. + // @inject_tag: gorm:"default:null" + ScopeId string `protobuf:"bytes,10,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` } func (x *Entry) Reset() { @@ -139,6 +146,20 @@ func (x *Entry) GetData() []byte { return nil } +func (x *Entry) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *Entry) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + // Metadata provides a message for oplog metadata that's compatible with gorm type Metadata struct { state protoimpl.MessageState @@ -328,7 +349,7 @@ var file_controller_storage_oplog_store_v1_oplog_proto_rawDesc = []byte{ 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, 0xe8, 0x02, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, + 0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x03, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, @@ -350,41 +371,44 @@ var file_controller_storage_oplog_store_v1_oplog_proto_rawDesc = []byte{ 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xea, - 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 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, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x49, 0x64, 0x12, 0x3e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x06, - 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 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, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2f, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x15, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, + 0x22, 0xea, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 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, 0x19, 0x0a, 0x08, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x3e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 0x2e, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xe0, 0x01, + 0x0a, 0x06, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 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, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 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, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6f, 0x70, 0x6c, 0x6f, 0x67, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/oplog/testing.go b/internal/oplog/testing.go index d43c2a036..753e5ae71 100644 --- a/internal/oplog/testing.go +++ b/internal/oplog/testing.go @@ -3,6 +3,7 @@ package oplog import ( "context" "crypto/rand" + "fmt" "testing" "github.com/hashicorp/boundary/internal/db/common" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/oplog/oplog_test" "github.com/hashicorp/boundary/testing/dbtest" "github.com/hashicorp/go-dbw" + kms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" wrapping "github.com/hashicorp/go-kms-wrapping/v2" aead "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/stretchr/testify/assert" @@ -17,8 +19,14 @@ import ( "gorm.io/driver/postgres" ) +// TestOplogDeleteAllEntries allows you to delete all the entries for testing. +func TestOplogDeleteAllEntries(t testing.TB, conn *dbw.DB) { + _, err := dbw.New(conn).Exec(context.Background(), fmt.Sprintf("delete from %q", Entry{}.TableName()), nil) + require.NoError(t, err) +} + // setup the tests (initialize the database one-time and intialized testDatabaseURL) -func setup(t testing.TB) *dbw.DB { +func setup(ctx context.Context, t testing.TB) (*dbw.DB, wrapping.Wrapper) { t.Helper() require := require.New(t) cleanup, url, err := testInitDbInDocker(t) @@ -30,7 +38,25 @@ func setup(t testing.TB) *dbw.DB { assert.NoError(t, db.Close(context.Background())) assert.NoError(t, cleanup()) }) - return db + rootKey := make([]byte, 32) + n, err := rand.Read(rootKey) + require.NoError(err) + require.Equal(n, 32) + root := aead.NewWrapper() + root.SetConfig(context.Background(), wrapping.WithKeyId("key_id")) + err = root.SetAesGcmKeyBytes(rootKey) + require.NoError(err) + r := dbw.New(db) + w := dbw.New(db) + kmsCache, err := kms.New(r, w, []kms.KeyPurpose{kms.KeyPurposeRootKey, "oplog"}) + require.NoError(err) + err = kmsCache.AddExternalWrapper(ctx, kms.KeyPurposeRootKey, root) + require.NoError(err) + err = kmsCache.CreateKeys(ctx, "global", []kms.KeyPurpose{"oplog"}) + require.NoError(err) + wrapper, err := kmsCache.GetWrapper(ctx, "global", "oplog") + require.NoError(err) + return db, wrapper } func testOpen(t testing.TB, dbType string, connectionUrl string) *dbw.DB { @@ -86,19 +112,6 @@ func testInitDbInDocker(t testing.TB) (cleanup func() error, retURL string, err return } -// testWrapper initializes an AEAD wrapping.Wrapper for testing the oplog -func testWrapper(t testing.TB) wrapping.Wrapper { - t.Helper() - rootKey := make([]byte, 32) - n, err := rand.Read(rootKey) - require.NoError(t, err) - require.Equal(t, n, 32) - root := aead.NewWrapper() - err = root.SetAesGcmKeyBytes(rootKey) - require.NoError(t, err) - return root -} - // testInitStore will execute the migrations needed to initialize the store for tests func testInitStore(t testing.TB, cleanup func() error, url string) { t.Helper() diff --git a/internal/oplog/testing_test.go b/internal/oplog/testing_test.go index a30c7adc2..5a1f08521 100644 --- a/internal/oplog/testing_test.go +++ b/internal/oplog/testing_test.go @@ -5,14 +5,13 @@ import ( "testing" "github.com/hashicorp/boundary/internal/db/common" - wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_testUser(t *testing.T) { assert, require := assert.New(t), require.New(t) - db := setup(t) + db, _ := setup(context.Background(), t) id := testId(t) @@ -25,7 +24,7 @@ func Test_testUser(t *testing.T) { func Test_testFindUser(t *testing.T) { assert, require := assert.New(t), require.New(t) - db := setup(t) + db, _ := setup(context.Background(), t) id := testId(t) u := testUser(t, db, id, id, id) require.NotNil(u) @@ -50,14 +49,6 @@ func Test_testInitDbInDocker(t *testing.T) { require.NotNil(cleanup) } -func Test_testWrapper(t *testing.T) { - w := testWrapper(t) - require.NotNil(t, w) - typ, err := w.Type(context.Background()) - require.NoError(t, err) - assert.Equal(t, wrapping.WrapperTypeAead, typ) -} - func Test_testInitStore(t *testing.T) { assert, require := assert.New(t), require.New(t) cleanup, url, err := testInitDbInDocker(t) @@ -81,7 +72,7 @@ select count(*) from information_schema."tables" t where table_name = 'boundary_ func Test_testListConstraints(t *testing.T) { assert := assert.New(t) - db := setup(t) + db, _ := setup(context.Background(), t) constraints := testListConstraints(t, db, "oplog_test_user") assert.NotEmpty(constraints) } diff --git a/internal/oplog/ticketer_test.go b/internal/oplog/ticketer_test.go index 82fae96bb..8b70062bd 100644 --- a/internal/oplog/ticketer_test.go +++ b/internal/oplog/ticketer_test.go @@ -11,8 +11,8 @@ import ( // Test_NewGormTicketer provides unit tests for creating a Gorm ticketer func Test_NewTicketer(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) t.Run("valid", func(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -30,8 +30,8 @@ func Test_NewTicketer(t *testing.T) { // Test_GetTicket provides unit tests for getting oplog.Tickets func Test_GetTicket(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) ticketer, err := NewTicketer(testCtx, db, WithAggregateNames(true)) require.NoError(t, err) @@ -55,8 +55,8 @@ func Test_GetTicket(t *testing.T) { // Test_Redeem provides unit tests for redeeming tickets func Test_Redeem(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) t.Run("valid", func(t *testing.T) { require := require.New(t) diff --git a/internal/oplog/writer_test.go b/internal/oplog/writer_test.go index 3a0c0527d..23b97eb47 100644 --- a/internal/oplog/writer_test.go +++ b/internal/oplog/writer_test.go @@ -14,8 +14,8 @@ import ( // Test_WriterCreate provides unit tests for Writer Create func Test_WriterCreate(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) t.Run("valid", func(t *testing.T) { assert, require := assert.New(t), require.New(t) tx, err := dbw.New(db).Begin(testCtx) @@ -52,8 +52,8 @@ func Test_WriterCreate(t *testing.T) { // Test_WriterDelete provides unit tests for Writer Delete func Test_WriterDelete(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) t.Run("valid", func(t *testing.T) { assert, require := assert.New(t), require.New(t) tx, err := dbw.New(db).Begin(testCtx) @@ -92,8 +92,8 @@ func Test_WriterDelete(t *testing.T) { // TestWriter_hasTable provides unit tests for Writer HasTable func TestWriter_hasTable(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) w := Writer{db} t.Run("success", func(t *testing.T) { @@ -116,8 +116,8 @@ func TestWriter_hasTable(t *testing.T) { // TestWriter_createTableLike provides unit tests for Writer createTable func TestWriter_createTableLike(t *testing.T) { - db := setup(t) testCtx := context.Background() + db, _ := setup(testCtx, t) t.Run("success", func(t *testing.T) { assert := assert.New(t) w := Writer{db} @@ -169,7 +169,7 @@ func TestWriter_createTableLike(t *testing.T) { // Test_WriterDropTableIfExists provides unit tests for Writer dropTableIfExists func Test_WriterDropTableIfExists(t *testing.T) { testCtx := context.Background() - db := setup(t) + db, _ := setup(testCtx, t) t.Run("success", func(t *testing.T) { assert := assert.New(t) w := Writer{db} @@ -192,7 +192,7 @@ func Test_WriterDropTableIfExists(t *testing.T) { } func TestWriter_Update(t *testing.T) { - db := setup(t) + db, _ := setup(context.Background(), t) id := testId(t) type args struct { user *oplog_test.TestUser diff --git a/internal/proto/controller/api/resources/scopes/v1/scope.proto b/internal/proto/controller/api/resources/scopes/v1/scope.proto index 7f9b6c6b0..a6af99f89 100644 --- a/internal/proto/controller/api/resources/scopes/v1/scope.proto +++ b/internal/proto/controller/api/resources/scopes/v1/scope.proto @@ -85,3 +85,58 @@ message Scope { // Output only. The authorized actions for the scope's collections. map authorized_collection_actions = 310 [json_name = "authorized_collection_actions"]; } + +// KeyVersion describes a specific version of a key and holds the actual key material +message KeyVersion { + // The ID of the key version. + string id = 10; // @gotags: `class:"public"` + + // The iteration of the Key that this version represents. + uint32 version = 20; // @gotags: `class:"public"` + + // When this version was created. + google.protobuf.Timestamp created_time = 30 [json_name = "created_time"]; // @gotags: `class:"public"` +} + +// Key contains all fields related to a Key in a Scope. +message Key { + // The ID of the Key. + string id = 10; // @gotags: `class:"public"` + + // Scope information for this resource. + ScopeInfo scope = 20; + + // The purpose of the Key. + string purpose = 30; // @gotags: `class:"public"` + + // The time this Key was created. + google.protobuf.Timestamp created_time = 40 [json_name = "created_time"]; // @gotags: `class:"public"` + + // The type of the Key. + string type = 50; // @gotags: `class:"public"` + + // The versions of the key. + repeated KeyVersion versions = 60; +} + +// KeyVersionDestructionJob holds information about a pending key version destruction job. +message KeyVersionDestructionJob { + // The ID of the Key version this job relates to. + string key_version_id = 10; // @gotags: `class:"public"` + + // Scope information for this resource. + ScopeInfo scope = 20; + + // The current status of the key version destruction job. One of "pending", "running" or "completed". + string status = 30; // @gotags: `class:"public"` + + // The time this key version destruction job was created. + google.protobuf.Timestamp created_time = 40 [json_name = "created_time"]; // @gotags: `class:"public"` + + // The number of rows that have been successfully re-encrypted with a new key version. + // All rows must be re-encrypted before the key version can be destroyed. + int64 completed_count = 50; // @gotags: `class:"public"` + + // The total number of rows that need re-encrypting. + int64 total_count = 60; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/api/services/v1/scope_service.proto b/internal/proto/controller/api/services/v1/scope_service.proto index 8796fa2b3..a72d75d3e 100644 --- a/internal/proto/controller/api/services/v1/scope_service.proto +++ b/internal/proto/controller/api/services/v1/scope_service.proto @@ -87,10 +87,57 @@ service ScopeService { summary: "Deletes a Scope." }; } + + // ListKeys lists all the keys found in the scope specified. If the scope + // is not found an error is returned. + rpc ListKeys(ListKeysRequest) returns (ListKeysResponse) { + option (google.api.http) = { + get: "/v1/scopes/{id}:list-keys" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "List all keys in a Scope." + }; + } + + // RotateKeys rotates and optionally rewraps all the keys found in the + // scope specified. If the scope is not found an error is returned. If + // the scope is empty, the global scope is used. + rpc RotateKeys(RotateKeysRequest) returns (RotateKeysResponse) { + option (google.api.http) = { + post: "/v1/scopes:rotate-keys" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Rotate all keys in a Scope." + }; + } + + // ListKeyVersionDestructionJobs lists any pending key version destruction jobs in the scope. + rpc ListKeyVersionDestructionJobs(ListKeyVersionDestructionJobsRequest) returns (ListKeyVersionDestructionJobsResponse) { + option (google.api.http) = { + get: "/v1/scopes/{scope_id}:list-key-version-destruction-jobs" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Lists all pending key version destruction jobs in a Scope." + }; + } + + // DestroyKeyVersion destroys the specified key version. If this requires re-encrypting + // existing data, it will start an asynchronous process to complete this operation + // before destroying the key. Use ListKeyVersionDestructionJobs to monitor pending destruction jobs. + rpc DestroyKeyVersion(DestroyKeyVersionRequest) returns (DestroyKeyVersionResponse) { + option (google.api.http) = { + post: "/v1/scopes:destroy-key-version" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Destroy the specified key version in a Scope. This may start an asynchronous job that re-encrypts all data encrypted by the specified key version. Use GET /v1/scopes/{scope_id}:list-key-version-destruction-jobs to monitor pending destruction jobs." + }; + } } message GetScopeRequest { - string id = 1; + string id = 1; // @gotags: `class:"public"` } message GetScopeResponse { @@ -98,9 +145,9 @@ message GetScopeResponse { } message ListScopesRequest { - string scope_id = 1; - bool recursive = 20 [json_name = "recursive"]; - string filter = 30 [json_name = "filter"]; + string scope_id = 1; // @gotags: `class:"public"` + bool recursive = 20 [json_name = "recursive"]; // @gotags: `class:"public"` + string filter = 30 [json_name = "filter"]; // @gotags: `class:"public"` } message ListScopesResponse { @@ -108,18 +155,18 @@ message ListScopesResponse { } message CreateScopeRequest { - bool skip_admin_role_creation = 1; - bool skip_default_role_creation = 2; + bool skip_admin_role_creation = 1; // @gotags: `class:"public"` + bool skip_default_role_creation = 2; // @gotags: `class:"public"` resources.scopes.v1.Scope item = 3; } message CreateScopeResponse { - string uri = 1; + string uri = 1; // @gotags: `class:"public"` resources.scopes.v1.Scope item = 2; } message UpdateScopeRequest { - string id = 1; + string id = 1; // @gotags: `class:"public"` resources.scopes.v1.Scope item = 2; google.protobuf.FieldMask update_mask = 3 [json_name = "update_mask"]; } @@ -129,7 +176,42 @@ message UpdateScopeResponse { } message DeleteScopeRequest { - string id = 1; + string id = 1; // @gotags: `class:"public"` } message DeleteScopeResponse {} + +message ListKeysRequest { + string id = 1; // @gotags: `class:"public"` +} + +message ListKeysResponse { + repeated resources.scopes.v1.Key items = 1; +} + +message RotateKeysRequest { + string scope_id = 1; // @gotags: `class:"public"` + bool rewrap = 2; // @gotags: `class:"public"` +} + +message RotateKeysResponse {} + +message ListKeyVersionDestructionJobsRequest { + string scope_id = 1; // @gotags: `class:"public"` +} + +message ListKeyVersionDestructionJobsResponse { + repeated resources.scopes.v1.KeyVersionDestructionJob items = 1; +} + +message DestroyKeyVersionRequest { + string scope_id = 1; // @gotags: `class:"public"` + string key_version_id = 2; // @gotags: `class:"public"` +} + +message DestroyKeyVersionResponse { + // Destruction state. One of "pending" or "completed". + // Use GET /v1/scopes/{scope_id}:list-key-version-destruction-jobs + // to monitor pending destruction jobs. + string state = 1; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job.proto b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job.proto new file mode 100644 index 000000000..0bbbb11c2 --- /dev/null +++ b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package controller.storage.kms.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/kms/store;store"; + +// DataKeyVersionDestructionJob is used to read and write +// data from the kms_data_key_version_destruction_job table. +message DataKeyVersionDestructionJob { + // key_id is the private id of the data key version being destroyed. + // @inject_tag: `gorm:"primary_key"` + string key_id = 10; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; +} diff --git a/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_progress.proto b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_progress.proto new file mode 100644 index 000000000..2cf2a9413 --- /dev/null +++ b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_progress.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package controller.storage.kms.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/kms/store;store"; + +message DataKeyVersionDestructionJobProgress { + // The id of the data key version that is being revoked + // @inject_tag: `gorm:"not_null"` + string key_id = 10; + + // Scope id this data key version belongs to. + // @inject_tag: `gorm:"not_null"` + string scope_id = 20; + + // create_time from the RDBMS + // @inject_tag: `gorm:"not_null"` + timestamp.v1.Timestamp create_time = 30; + + // The status of this data key version destruction job + // One of "pending", "running" or "completed" + // @inject_tag: `gorm:"not_null"` + string status = 40; + + // The number of rows that have been rewrapped + // @inject_tag: `gorm:"not_null"` + int64 completed_count = 50; + + // The total number of rows that need rewrapping + // @inject_tag: `gorm:"not_null"` + int64 total_count = 60; +} diff --git a/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run.proto b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run.proto new file mode 100644 index 000000000..4f17a8c0e --- /dev/null +++ b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package controller.storage.kms.store.v1; + +option go_package = "github.com/hashicorp/boundary/internal/kms/store;store"; + +// DataKeyVersionDestructionJobRun is used to read and write +// data from the kms_data_key_version_destruction_job_run table. +message DataKeyVersionDestructionJobRun { + // key_id is the private id of the data key version being destroyed. + // @inject_tag: `gorm:"primary_key"` + string key_id = 10; + + // table_name is the table name that is being rewrapped by this run. + // @inject_tag: `gorm:"primary_key"` + string table_name = 20; + + // total_count is the total number of rows that need to be rewrapped + // in this table. + // @inject_tag: `gorm:"not_null"` + int64 total_count = 30; + + // completed_count is the number of rows that have completed rewrapping. + // @inject_tag: `gorm:"not_null"` + int64 completed_count = 40; + + // is_running defines whether this run is currently running. Only + // one run is allowed to be running at a time. + // @inject_tag: `gorm:"not_null"` + bool is_running = 50; +} diff --git a/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run_allowed_table_name.proto b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run_allowed_table_name.proto new file mode 100644 index 000000000..f80f98813 --- /dev/null +++ b/internal/proto/controller/storage/kms/store/v1/data_key_version_destruction_job_run_allowed_table_name.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package controller.storage.kms.store.v1; + +option go_package = "github.com/hashicorp/boundary/internal/kms/store;store"; + +message DataKeyVersionDestructionJobRunAllowedTableName { + // The name of a table that is allowed to be used + // in a data key version destruction job run. + // @inject_tag: `gorm:"not_null"` + string table_name = 10; +} diff --git a/internal/proto/controller/storage/oplog/store/v1/oplog.proto b/internal/proto/controller/storage/oplog/store/v1/oplog.proto index b210ef9fd..349ed1cca 100644 --- a/internal/proto/controller/storage/oplog/store/v1/oplog.proto +++ b/internal/proto/controller/storage/oplog/store/v1/oplog.proto @@ -34,6 +34,15 @@ message Entry { // we are NOT storing this plain-text entry data in the db // @inject_tag: gorm:"-" wrapping:"pt,entry_data" bytes data = 8; + + // the private id of the data key version used to encrypt the data. + // @inject_tag: gorm:"not_null" + string key_id = 9; + + // the scope id associated with the key used to encrypt the data. + // This value is populated from the key_id in a trigger. + // @inject_tag: gorm:"default:null" + string scope_id = 10; } // Metadata provides a message for oplog metadata that's compatible with gorm diff --git a/internal/proto/controller/storage/servers/store/v1/root_certificate.proto b/internal/proto/controller/storage/servers/store/v1/root_certificate.proto index 7b1d417e8..fbdabb879 100644 --- a/internal/proto/controller/storage/servers/store/v1/root_certificate.proto +++ b/internal/proto/controller/storage/servers/store/v1/root_certificate.proto @@ -34,13 +34,18 @@ message RootCertificate { storage.timestamp.v1.Timestamp not_valid_after = 40; // The public key associated with this certificate - // @inject_tag: `gorm:"not_null"` + // @inject_tag: `gorm:"primary_key"` bytes public_key = 50; + // The plain-text private key associated with this certificate. We are not storing this + // in the database. + // @inject_tag: gorm:"-" wrapping:"pt,private_key" + bytes private_key = 60; + // The private key associated with this certificate // This is a ciphertext field - // @inject_tag: `gorm:"not_null"` - bytes private_key = 60; + // @inject_tag: gorm:"column:private_key;not_null" wrapping:"ct,private_key" + bytes ct_private_key = 61; // The id of the kms database key used for encrypting this entry. // @inject_tag: `gorm:"not_null"` diff --git a/internal/proto/controller/storage/servers/store/v1/worker_auth.proto b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto index 1d0ce32a1..6cabf647a 100644 --- a/internal/proto/controller/storage/servers/store/v1/worker_auth.proto +++ b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto @@ -25,10 +25,15 @@ message WorkerAuth { // @inject_tag: `gorm:"not_null"` bytes worker_encryption_pub_key = 40; + // The plain-text private key associated with this certificate. We are not storing this + // in the database. + // @inject_tag: gorm:"-" wrapping:"pt,private_key" + bytes controller_encryption_priv_key = 50; + // The private key associated with this certificate // This is a ciphertext field - // @inject_tag: `gorm:"not_null"` - bytes controller_encryption_priv_key = 50; + // @inject_tag: gorm:"column:controller_encryption_priv_key;not_null" wrapping:"ct,private_key" + bytes ct_controller_encryption_priv_key = 51; // The id of the kms database key used for encrypting this entry. // @inject_tag: `gorm:"not_null"` diff --git a/internal/server/query.go b/internal/server/query.go index 5be4ab5a5..b09b549d5 100644 --- a/internal/server/query.go +++ b/internal/server/query.go @@ -38,4 +38,27 @@ const ( where w.worker_key_identifier in (?) ` + + workerAuthRewrapQuery = ` + select distinct + auth.worker_key_identifier, + auth.controller_encryption_priv_key, + auth.key_id + from server_worker worker + inner join worker_auth_authorized auth + on auth.worker_id = worker.public_id + where worker.scope_id = ? + and auth.key_id = ? + ` + workerAuthServerLedActivationTokenRewrapQuery = ` + select distinct + auth_token.worker_id, + auth_token.creation_time_encrypted, + auth_token.key_id + from server_worker worker + inner join worker_auth_server_led_activation_token auth_token + on auth_token.worker_id = worker.public_id + where worker.scope_id = ? + and auth_token.key_id = ? + ` ) diff --git a/internal/server/repository_workerauth.go b/internal/server/repository_workerauth.go index a93a5e352..65524633c 100644 --- a/internal/server/repository_workerauth.go +++ b/internal/server/repository_workerauth.go @@ -128,21 +128,15 @@ func StoreNodeInformationTx(ctx context.Context, writer db.Writer, databaseWrapp nodeAuth.WorkerEncryptionPubKey = node.EncryptionPublicKeyBytes nodeAuth.WorkerSigningPubKey = node.CertificatePublicKeyPkix nodeAuth.Nonce = node.RegistrationNonce + nodeAuth.ControllerEncryptionPrivKey = node.ServerEncryptionPrivateKeyBytes - var err error - nodeAuth.KeyId, err = databaseWrapper.KeyId(ctx) - if err != nil { - return errors.Wrap(ctx, err, op) - } - nodeAuth.ControllerEncryptionPrivKey, err = encrypt(ctx, node.ServerEncryptionPrivateKeyBytes, databaseWrapper) - if err != nil { + if err := nodeAuth.encrypt(ctx, databaseWrapper); err != nil { return errors.Wrap(ctx, err, op) } // Get workerId from state passed in var result workerAuthWorkerId - err = mapstructure.Decode(node.State.AsMap(), &result) - if err != nil { + if err := mapstructure.Decode(node.State.AsMap(), &result); err != nil { return errors.Wrap(ctx, err, op) } nodeAuth.WorkerId = result.WorkerId @@ -230,23 +224,19 @@ func (r *WorkerAuthRepositoryStorage) convertRootCertificate(ctx context.Context rootCert.PublicKey = cert.PublicKeyPkix rootCert.State = cert.Id rootCert.IssuingCa = CaId + rootCert.PrivateKey = cert.PrivateKeyPkcs8 // Encrypt the private key databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) if err != nil { return nil, errors.Wrap(ctx, err, op) } - rootCert.KeyId, err = databaseWrapper.KeyId(ctx) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - rootCert.PrivateKey, err = encrypt(ctx, cert.PrivateKeyPkcs8, databaseWrapper) - if err != nil { + + if err = rootCert.encrypt(ctx, databaseWrapper); err != nil { return nil, errors.Wrap(ctx, err, op) } - err = rootCert.ValidateNewRootCertificate(ctx) - if err != nil { + if err = rootCert.ValidateNewRootCertificate(ctx); err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -430,10 +420,10 @@ func (r *WorkerAuthRepositoryStorage) loadNodeInformation(ctx context.Context, n if err != nil { return errors.Wrap(ctx, err, op) } - node.ServerEncryptionPrivateKeyBytes, err = decrypt(ctx, workerAuthorizedSet.Current.ControllerEncryptionPrivKey, databaseWrapper) - if err != nil { + if err = workerAuthorizedSet.Current.decrypt(ctx, databaseWrapper); err != nil { return errors.Wrap(ctx, err, op) } + node.ServerEncryptionPrivateKeyBytes = workerAuthorizedSet.Current.ControllerEncryptionPrivKey workerIdInfo := workerAuthWorkerId{WorkerId: workerAuthorizedSet.Current.GetWorkerId()} s := structs.New(workerIdInfo) @@ -527,7 +517,7 @@ func (r *WorkerAuthRepositoryStorage) FilterToAuthorizedWorkerKeyIds(ctx context if len(workerKeyIds) == 0 { return nil, nil } - rows, err := r.reader.Query(ctx, authorizedWorkerQuery, []interface{}{workerKeyIds}) + rows, err := r.reader.Query(ctx, authorizedWorkerQuery, []any{workerKeyIds}) if err != nil { } defer rows.Close() @@ -588,22 +578,22 @@ func (r *WorkerAuthRepositoryStorage) loadRootCertificates(ctx context.Context, return nodee.ErrNotFound } - rootCert.CertificateDer = rootCertificate.Certificate - rootCert.NotAfter = rootCertificate.NotValidAfter.Timestamp - rootCert.NotBefore = rootCertificate.NotValidBefore.Timestamp - rootCert.PublicKeyPkix = rootCertificate.PublicKey - rootCert.PrivateKeyType = types.KEYTYPE_ED25519 - // decrypt private key databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(rootCertificate.KeyId)) if err != nil { return errors.Wrap(ctx, err, op) } - rootCert.PrivateKeyPkcs8, err = decrypt(ctx, rootCertificate.PrivateKey, databaseWrapper) - if err != nil { + if err = rootCertificate.decrypt(ctx, databaseWrapper); err != nil { return errors.Wrap(ctx, err, op) } + rootCert.CertificateDer = rootCertificate.Certificate + rootCert.NotAfter = rootCertificate.NotValidAfter.Timestamp + rootCert.NotBefore = rootCertificate.NotValidBefore.Timestamp + rootCert.PublicKeyPkix = rootCertificate.PublicKey + rootCert.PrivateKeyType = types.KEYTYPE_ED25519 + rootCert.PrivateKeyPkcs8 = rootCertificate.PrivateKey + if c == string(NextState) { cert.Next = rootCert } @@ -876,49 +866,6 @@ func (r *WorkerAuthRepositoryStorage) listCertificateAuthority(ctx context.Conte return certIds, nil } -// encrypt value before writing it to the db -func encrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { - const op = "server.(WorkerAuthRepositoryStorage).encrypt" - if value == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") - } - if wrapper == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") - } - - blobInfo, err := wrapper.Encrypt(ctx, value) - if err != nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "error encrypting recovery info", errors.WithWrap(err)) - } - marshaledBlob, err := proto.Marshal(blobInfo) - if err != nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "error marshaling encrypted blob", errors.WithWrap(err)) - } - return marshaledBlob, nil -} - -func decrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { - const op = "server.(WorkerAuthRepositoryStorage).decrypt" - if value == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") - } - if wrapper == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") - } - - blobInfo := new(wrapping.BlobInfo) - if err := proto.Unmarshal(value, blobInfo); err != nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "error decoding encrypted blob", errors.WithWrap(err)) - } - - marshaledInfo, err := wrapper.Decrypt(ctx, blobInfo) - if err != nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "error decrypting recovery info", errors.WithWrap(err)) - } - - return marshaledInfo, nil -} - func (r *WorkerAuthRepositoryStorage) validateWorkerAuths(ctx context.Context, workerAuths []*WorkerAuth) (*WorkerAuthSet, error) { const op = "server.(WorkerAuthRepositoryStorage).validateWorkerAuths" diff --git a/internal/server/repository_workerauth_test.go b/internal/server/repository_workerauth_test.go index 1028c1eab..87660b928 100644 --- a/internal/server/repository_workerauth_test.go +++ b/internal/server/repository_workerauth_test.go @@ -389,8 +389,8 @@ func TestStoreNodeInformationTx(t *testing.T) { databaseWrapper: &mockTestWrapper{err: errors.New(testCtx, errors.Internal, "testing", "key-id-error")}, node: testNodeInfoFn(), wantErr: true, - wantErrIs: errors.Internal, - wantErrContains: "key-id-error", + wantErrIs: errors.Encrypt, + wantErrContains: "reading cipher key id", }, { name: "encrypt-error", @@ -512,7 +512,7 @@ func (m *mockTestWrapper) Encrypt(ctx context.Context, plaintext []byte, options if m.err != nil && m.encryptError { return nil, m.err } - panic("todo") + return &wrapping.BlobInfo{}, nil } func (m *mockTestWrapper) Decrypt(ctx context.Context, ciphertext *wrapping.BlobInfo, options ...wrapping.Option) ([]byte, error) { diff --git a/internal/server/rewrapping.go b/internal/server/rewrapping.go new file mode 100644 index 000000000..a1b268f8a --- /dev/null +++ b/internal/server/rewrapping.go @@ -0,0 +1,157 @@ +package server + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn("worker_auth_ca_certificate", workerAuthCertRewrapFn) + kms.RegisterTableRewrapFn("worker_auth_authorized", workerAuthRewrapFn) + kms.RegisterTableRewrapFn("worker_auth_server_led_activation_token", workerAuthServerLedActivationTokenRewrapFn) +} + +func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) string { + if dataKeyVersionId == "" { + return "missing data key version id" + } + if scopeId == "" { + return "missing scope id" + } + if util.IsNil(reader) { + return "missing database reader" + } + if util.IsNil(writer) { + return "missing database writer" + } + if kmsRepo == nil { + return "missing kms repository" + } + return "" +} + +func workerAuthCertRewrapFn(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "server.workerAuthCertRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var certs []*RootCertificate + // Indexes on public id, state. neither of which are queryable via scope. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &certs, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cert := range certs { + if err := cert.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt worker auth certificate")) + } + if err := cert.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt worker auth certificate")) + } + if _, err := writer.Update(ctx, cert, []string{"CtPrivateKey", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update worker auth certificate row with rewrapped fields")) + } + } + return nil +} + +func workerAuthRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "server.workerAuthRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var auths []*WorkerAuth + // An index exists on (worker_id, state), so we can query workers via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, workerAuthRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + workerAuth := allocWorkerAuth() + if err := rows.Scan( + &workerAuth.WorkerKeyIdentifier, + &workerAuth.CtControllerEncryptionPrivKey, + &workerAuth.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to scan row")) + } + auths = append(auths, workerAuth) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, workerAuth := range auths { + if err := workerAuth.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt worker auth")) + } + if err := workerAuth.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt worker auth")) + } + if _, err := writer.Update(ctx, workerAuth, []string{"CtControllerEncryptionPrivKey", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update worker auth row with rewrapped fields")) + } + } + return nil +} + +func workerAuthServerLedActivationTokenRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "server.workerAuthServerLedActivationTokenRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var tokens []*WorkerAuthServerLedActivationToken + // An index exists on worker_id, so we can query workers via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, workerAuthServerLedActivationTokenRewrapQuery, []any{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + token := allocWorkerAuthServerLedActivationToken() + if err := rows.Scan( + &token.WorkerId, + &token.CreationTimeEncrypted, + &token.KeyId, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to scan row")) + } + tokens = append(tokens, token) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to retrieve updated key version id")) + } + for _, token := range tokens { + if err := token.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt activation token")) + } + if err := token.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt activation token")) + } + if _, err := writer.Update(ctx, token, []string{"CreationTimeEncrypted", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update activation token row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/server/rewrapping_test.go b/internal/server/rewrapping_test.go new file mode 100644 index 000000000..2d5c13ace --- /dev/null +++ b/internal/server/rewrapping_test.go @@ -0,0 +1,194 @@ +package server + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/server/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_workerAuthCertRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "worker_auth_ca_certificate" WHERE key_id=\$1`, + ).WillReturnError(errors.New("Query error")) + err := workerAuthCertRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) // despite not looking like it, this is necessary for some reason + + // create a repo and rotate to generate the first auth cert + workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + assert.NoError(t, err) + roots, err := rotation.RotateRootCertificates(ctx, workerAuthRepo) + assert.NoError(t, err) + + // pull this directly from the db rather than convert so we don't lose key info + currentRoot := allocRootCertificate() + assert.NoError(t, rw.SearchWhere(ctx, ¤tRoot, "state = ?", []any{"current"}, db.WithLimit(-1))) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, scope.Global.String())) + assert.NoError(t, workerAuthCertRewrapFn(ctx, currentRoot.KeyId, scope.Global.String(), rw, rw, kmsCache)) + + // now we pull the certs back from the db, decrypt it with the new key, and ensure things match + got := allocRootCertificate() + assert.NoError(t, rw.SearchWhere(ctx, &got, "state = ?", []any{"current"}, db.WithLimit(-1))) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, currentRoot.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.NotEqual(t, currentRoot.GetCtPrivateKey(), got.GetCtPrivateKey()) + assert.Equal(t, roots.Current.PrivateKeyPkcs8, got.GetPrivateKey()) + }) +} + +func TestRewrap_workerAuthRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct auth\.worker_key_identifier, auth\.controller_encryption_priv_key, auth\.key_id from server_worker worker inner join worker_auth_authorized auth on auth\.worker_id = worker\.public_id where worker\.scope_id = \$1 and auth\.key_id = \$2`, + ).WillReturnError(errors.New("Query error")) + err := workerAuthRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) // despite not looking like it, this is necessary for some reason + + worker := TestPkiWorker(t, conn, wrapper) + kmsWrapper, err := kmsCache.GetWrapper(ctx, worker.GetScopeId(), kms.KeyPurposeDatabase) + assert.NoError(t, err) + workerAuth := TestWorkerAuth(t, conn, worker, kmsWrapper) + + // TestWorkerAuth DOES encrypt the entry, so just store in the db + rows, err := rw.Update(ctx, workerAuth, []string{"CtControllerEncryptionPrivKey"}, nil) + assert.Equal(t, 1, rows) + assert.NoError(t, err) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, scope.Global.String())) + assert.NoError(t, workerAuthRewrapFn(ctx, workerAuth.GetKeyId(), scope.Global.String(), rw, rw, kmsCache)) + + // now we pull the auth back from the db, decrypt it with the new key, and ensure things match + got := allocWorkerAuth() + got.WorkerKeyIdentifier = workerAuth.WorkerKeyIdentifier + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), worker.GetScopeId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, workerAuth.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.NotEqual(t, workerAuth.GetCtControllerEncryptionPrivKey(), got.GetCtControllerEncryptionPrivKey()) + assert.Equal(t, workerAuth.ControllerEncryptionPrivKey, got.GetControllerEncryptionPrivKey()) + }) +} + +func TestRewrap_workerAuthServerLedActivationTokenRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct auth_token\.worker_id, auth_token\.creation_time_encrypted, auth_token\.key_id from server_worker worker inner join worker_auth_server_led_activation_token auth_token on auth_token\.worker_id = worker\.public_id where worker\.scope_id = \$1 and auth_token\.key_id = \$2`, + ).WillReturnError(errors.New("Query error")) + err := workerAuthServerLedActivationTokenRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) // despite not looking like it, this is necessary for some reason + + worker := TestPkiWorker(t, conn, wrapper) + kmsWrapper, err := kmsCache.GetWrapper(ctx, worker.GetScopeId(), kms.KeyPurposeDatabase) + assert.NoError(t, err) + token := &WorkerAuthServerLedActivationToken{ + WorkerAuthServerLedActivationToken: &store.WorkerAuthServerLedActivationToken{ + WorkerId: worker.GetPublicId(), + TokenId: "not a real token id lmao", + CreationTime: []byte("marshaled timestamppb.Timestamp indeed"), + }, + } + + // encrypt the entry and store in db + assert.NoError(t, token.encrypt(ctx, kmsWrapper)) + assert.NoError(t, rw.Create(ctx, token)) + + // now things are stored in the db, we can rotate and rewrap + assert.NoError(t, kmsCache.RotateKeys(ctx, scope.Global.String())) + assert.NoError(t, workerAuthServerLedActivationTokenRewrapFn(ctx, token.GetKeyId(), worker.GetScopeId(), rw, rw, kmsCache)) + + // now we pull the auth back from the db, decrypt it with the new key, and ensure things match + got := allocWorkerAuthServerLedActivationToken() + got.WorkerId = worker.GetPublicId() + assert.NoError(t, rw.LookupById(ctx, got)) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), worker.GetScopeId(), kms.KeyPurposeDatabase, kms.WithKeyId(got.GetKeyId())) + assert.NoError(t, err) + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + assert.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + assert.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.GetKeyId()) + assert.NotEqual(t, token.GetKeyId(), got.GetKeyId()) + assert.Equal(t, newKeyVersionId, got.GetKeyId()) + assert.NotEqual(t, token.GetCreationTimeEncrypted(), got.GetCreationTimeEncrypted()) + assert.Equal(t, token.GetCreationTime(), got.GetCreationTime()) + }) +} diff --git a/internal/server/root_certificate.go b/internal/server/root_certificate.go index 20511b754..416fe7629 100644 --- a/internal/server/root_certificate.go +++ b/internal/server/root_certificate.go @@ -4,10 +4,11 @@ import ( "context" "fmt" - "github.com/hashicorp/boundary/internal/errors" - timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/server/store" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" "google.golang.org/protobuf/proto" ) @@ -50,6 +51,30 @@ type RootCertificate struct { tableName string `gorm:"-"` } +func (r *RootCertificate) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "server.(RootCertificate).encrypt" + if len(r.PrivateKey) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "no private key provided") + } + if err := structwrapping.WrapStruct(ctx, cipher, r.RootCertificate, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id")) + } + r.KeyId = keyId + return nil +} + +func (r *RootCertificate) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "server.(RootCertificate).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, r.RootCertificate, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + // RootCertificateKeys contains the public and private keys for use in constructing a RootCertificate type RootCertificateKeys struct { publicKey []byte @@ -93,7 +118,7 @@ func newRootCertificate(ctx context.Context, serialNumber uint64, certificate [] NotValidAfter: notValidAfter, NotValidBefore: notValidBefore, PublicKey: rootCertificateKeys.publicKey, - PrivateKey: rootCertificateKeys.privateKey, + CtPrivateKey: rootCertificateKeys.privateKey, KeyId: keyId, State: string(state), IssuingCa: CaId, @@ -133,8 +158,8 @@ func (r *RootCertificate) ValidateNewRootCertificate(ctx context.Context) error if r.PublicKey == nil { return errors.New(ctx, errors.InvalidParameter, op, "missing public key") } - if r.PrivateKey == nil { - return errors.New(ctx, errors.InvalidParameter, op, "missing private key") + if r.CtPrivateKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing encrypted private key") } if r.KeyId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing key id") diff --git a/internal/server/store/root_certificate.pb.go b/internal/server/store/root_certificate.pb.go index 9194d98a4..638c25156 100644 --- a/internal/server/store/root_certificate.pb.go +++ b/internal/server/store/root_certificate.pb.go @@ -99,12 +99,16 @@ type RootCertificate struct { // Not valid after is the timestamp at which this certificate's validity period ends NotValidAfter *timestamp.Timestamp `protobuf:"bytes,40,opt,name=not_valid_after,json=notValidAfter,proto3" json:"not_valid_after,omitempty"` // The public key associated with this certificate - // @inject_tag: `gorm:"not_null"` - PublicKey []byte `protobuf:"bytes,50,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"primary_key"` + PublicKey []byte `protobuf:"bytes,50,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" gorm:"primary_key"` + // The plain-text private key associated with this certificate. We are not storing this + // in the database. + // @inject_tag: gorm:"-" wrapping:"pt,private_key" + PrivateKey []byte `protobuf:"bytes,60,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"-" wrapping:"pt,private_key"` // The private key associated with this certificate // This is a ciphertext field - // @inject_tag: `gorm:"not_null"` - PrivateKey []byte `protobuf:"bytes,60,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"not_null"` + // @inject_tag: gorm:"column:private_key;not_null" wrapping:"ct,private_key" + CtPrivateKey []byte `protobuf:"bytes,61,opt,name=ct_private_key,json=ctPrivateKey,proto3" json:"ct_private_key,omitempty" gorm:"column:private_key;not_null" wrapping:"ct,private_key"` // The id of the kms database key used for encrypting this entry. // @inject_tag: `gorm:"not_null"` KeyId string `protobuf:"bytes,70,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` @@ -190,6 +194,13 @@ func (x *RootCertificate) GetPrivateKey() []byte { return nil } +func (x *RootCertificate) GetCtPrivateKey() []byte { + if x != nil { + return x.CtPrivateKey + } + return nil +} + func (x *RootCertificate) GetKeyId() string { if x != nil { return x.KeyId @@ -228,7 +239,7 @@ var file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc = [] 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x03, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x74, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0xb4, 0x03, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, @@ -248,16 +259,18 @@ var file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc = [] 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, - 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, - 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, - 0x5f, 0x63, 0x61, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x73, 0x73, 0x75, 0x69, - 0x6e, 0x67, 0x43, 0x61, 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, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x74, 0x5f, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x3d, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0c, 0x63, 0x74, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x15, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x50, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, + 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x61, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x69, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x43, 0x61, 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, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/server/store/worker_auth.pb.go b/internal/server/store/worker_auth.pb.go index dac25fa91..5c6f489fb 100644 --- a/internal/server/store/worker_auth.pb.go +++ b/internal/server/store/worker_auth.pb.go @@ -41,10 +41,14 @@ type WorkerAuth struct { // The worker's public encryption key // @inject_tag: `gorm:"not_null"` WorkerEncryptionPubKey []byte `protobuf:"bytes,40,opt,name=worker_encryption_pub_key,json=workerEncryptionPubKey,proto3" json:"worker_encryption_pub_key,omitempty" gorm:"not_null"` + // The plain-text private key associated with this certificate. We are not storing this + // in the database. + // @inject_tag: gorm:"-" wrapping:"pt,private_key" + ControllerEncryptionPrivKey []byte `protobuf:"bytes,50,opt,name=controller_encryption_priv_key,json=controllerEncryptionPrivKey,proto3" json:"controller_encryption_priv_key,omitempty" gorm:"-" wrapping:"pt,private_key"` // The private key associated with this certificate // This is a ciphertext field - // @inject_tag: `gorm:"not_null"` - ControllerEncryptionPrivKey []byte `protobuf:"bytes,50,opt,name=controller_encryption_priv_key,json=controllerEncryptionPrivKey,proto3" json:"controller_encryption_priv_key,omitempty" gorm:"not_null"` + // @inject_tag: gorm:"column:controller_encryption_priv_key;not_null" wrapping:"ct,private_key" + CtControllerEncryptionPrivKey []byte `protobuf:"bytes,51,opt,name=ct_controller_encryption_priv_key,json=ctControllerEncryptionPrivKey,proto3" json:"ct_controller_encryption_priv_key,omitempty" gorm:"column:controller_encryption_priv_key;not_null" wrapping:"ct,private_key"` // The id of the kms database key used for encrypting this entry. // @inject_tag: `gorm:"not_null"` KeyId string `protobuf:"bytes,60,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` @@ -130,6 +134,13 @@ func (x *WorkerAuth) GetControllerEncryptionPrivKey() []byte { return nil } +func (x *WorkerAuth) GetCtControllerEncryptionPrivKey() []byte { + if x != nil { + return x.CtControllerEncryptionPrivKey + } + return nil +} + func (x *WorkerAuth) GetKeyId() string { if x != nil { return x.KeyId @@ -339,7 +350,7 @@ var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{ 0x65, 0x72, 0x73, 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, 0xef, 0x03, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb9, 0x04, 0x0a, 0x0a, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, @@ -357,48 +368,53 @@ var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{ 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x69, 0x76, 0x4b, - 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, - 0x63, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x50, - 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, 0x5a, 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, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, - 0xa6, 0x01, 0x0a, 0x10, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x42, 0x75, - 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, 0x72, 0x6f, 0x6f, 0x74, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, - 0x79, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, - 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, - 0x72, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x22, 0x57, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x65, 0x64, - 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, - 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x15, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x28, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 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, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x79, 0x12, 0x48, 0x0a, 0x21, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, + 0x72, 0x69, 0x76, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x33, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1d, 0x63, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x69, 0x76, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, + 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x46, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x50, 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, 0x5a, 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, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x10, 0x57, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x3d, + 0x0a, 0x1b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x18, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, + 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, 0x72, 0x74, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x22, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x41, 0x75, 0x74, + 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, + 0x64, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x15, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x15, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6b, 0x65, 0x79, 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, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/server/testing.go b/internal/server/testing.go index e14c45bd5..1681bd263 100644 --- a/internal/server/testing.go +++ b/internal/server/testing.go @@ -35,7 +35,7 @@ func populateBytes(length int) []byte { return fieldBytes } -func TestKmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) string { +func TestKmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) (string, wrapping.Wrapper) { t.Helper() org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) kmsCache := kms.TestKms(t, conn, wrapper) @@ -44,7 +44,7 @@ func TestKmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping testKey, err := databaseWrapper.KeyId(ctx) require.NoError(t, err) - return testKey + return testKey, databaseWrapper } func TestRootCertificate(ctx context.Context, t *testing.T, conn *db.DB, kmsKey string) *RootCertificate { @@ -66,7 +66,7 @@ func TestRootCertificate(ctx context.Context, t *testing.T, conn *db.DB, kmsKey return cert } -func TestWorkerAuth(t *testing.T, conn *db.DB, worker *Worker, kmsKey string) *WorkerAuth { +func TestWorkerAuth(t *testing.T, conn *db.DB, worker *Worker, kmsWrapper wrapping.Wrapper) *WorkerAuth { t.Helper() ctx := context.Background() rw := db.New(conn) @@ -76,7 +76,6 @@ func TestWorkerAuth(t *testing.T, conn *db.DB, worker *Worker, kmsKey string) *W controllerKey := populateBytes(defaultLength) nonce := populateBytes(defaultLength) opt := []Option{ - WithKeyId(kmsKey), WithWorkerKeys(workerKeys), WithControllerEncryptionPrivateKey(controllerKey), WithNonce(nonce), @@ -84,8 +83,8 @@ func TestWorkerAuth(t *testing.T, conn *db.DB, worker *Worker, kmsKey string) *W workerAuth, err := newWorkerAuth(ctx, "worker-key-identifier", worker.PublicId, opt...) require.NoError(t, err) - err = rw.Create(ctx, workerAuth) - require.NoError(t, err) + require.NoError(t, workerAuth.encrypt(ctx, kmsWrapper)) + require.NoError(t, rw.Create(ctx, workerAuth)) return workerAuth } diff --git a/internal/server/worker_auth.go b/internal/server/worker_auth.go index c485b110a..cb39f43a2 100644 --- a/internal/server/worker_auth.go +++ b/internal/server/worker_auth.go @@ -5,6 +5,8 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/server/store" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" "google.golang.org/protobuf/proto" ) @@ -21,6 +23,30 @@ type WorkerAuth struct { tableName string `gorm:"-"` } +func (w *WorkerAuth) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "server.(WorkerAuth).encrypt" + if len(w.ControllerEncryptionPrivKey) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "no private key provided") + } + if err := structwrapping.WrapStruct(ctx, cipher, w.WorkerAuth, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id")) + } + w.KeyId = keyId + return nil +} + +func (w *WorkerAuth) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "server.(WorkerAuth).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, w.WorkerAuth, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + // WorkerAuthSet is intended to store a set of WorkerAuth records // This set represents the current and previous WorkerAuth records for a worker type WorkerAuthSet struct { @@ -34,6 +60,11 @@ type WorkerKeys struct { workerEncryptionPubKey []byte } +// newWorkerAuth initializes a new in-memory WorkerAuth struct. +// supported options: +// - withWorkerKeys +// - withControllerEncryptionPrivateKey (assigns the value to the plain-text field) +// - withNonce func newWorkerAuth(ctx context.Context, workerKeyIdentifier, workerId string, opt ...Option) (*WorkerAuth, error) { const op = "server.newWorkerAuth" opts := GetOpts(opt...) @@ -59,9 +90,6 @@ func newWorkerAuth(ctx context.Context, workerKeyIdentifier, workerId string, op if &opts.withControllerEncryptionPrivateKey != nil { l.ControllerEncryptionPrivKey = opts.withControllerEncryptionPrivateKey } - if opts.withKeyId != "" { - l.KeyId = opts.withKeyId - } if opts.withNonce != nil { l.Nonce = opts.withNonce } @@ -96,8 +124,8 @@ func (w *WorkerAuth) ValidateNewWorkerAuth(ctx context.Context) error { if w.WorkerEncryptionPubKey == nil { return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerEncryptionPubKey") } - if w.ControllerEncryptionPrivKey == nil { - return errors.New(ctx, errors.InvalidParameter, op, "missing ControllerEncryptionPrivKey") + if w.CtControllerEncryptionPrivKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing encrypted ControllerEncryptionPrivKey") } if w.KeyId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing KeyId") diff --git a/internal/server/worker_auth_server_led_activation_token_test.go b/internal/server/worker_auth_server_led_activation_token_test.go index e7d28eda0..ef035047e 100644 --- a/internal/server/worker_auth_server_led_activation_token_test.go +++ b/internal/server/worker_auth_server_led_activation_token_test.go @@ -94,13 +94,6 @@ func TestWorkerAuthActivationTokenConstraints(t *testing.T) { }, wantErrContains: "immutable column: worker_auth_server_led_activation_token.token_id: integrity violation", }, - { - name: "modify-create-time", - updateModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) (string, []any) { - return "update worker_auth_server_led_activation_token set creation_time_encrypted = ? where worker_id = ?", []any{in.CreationTimeEncrypted[0:5], in.WorkerId} - }, - wantErrContains: "immutable column: worker_auth_server_led_activation_token.creation_time_encrypted: integrity violation", - }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/server/workerauth_store_test.go b/internal/server/workerauth_store_test.go index d85ce8e69..78b2cc542 100644 --- a/internal/server/workerauth_store_test.go +++ b/internal/server/workerauth_store_test.go @@ -69,7 +69,7 @@ func TestRootCertStore(t *testing.T) { expectedRootCert: &store.RootCertificate{ SerialNumber: 1234567890, PublicKey: publicKey, - PrivateKey: privateKey, + CtPrivateKey: privateKey, Certificate: certificate, NotValidBefore: beforeTimestamp, NotValidAfter: afterTimestamp, @@ -93,7 +93,7 @@ func TestRootCertStore(t *testing.T) { expectedRootCert: &store.RootCertificate{ SerialNumber: 9876543210, PublicKey: publicKey, - PrivateKey: privateKey, + CtPrivateKey: privateKey, Certificate: certificate, NotValidBefore: beforeTimestamp, NotValidAfter: afterTimestamp, @@ -218,8 +218,7 @@ func TestRootCertStore(t *testing.T) { assert.Empty(cmp.Diff(tt.expectedRootCert, cert, protocmp.Transform())) } - deleteWhere := `state = ?` - deleted, err := rw.Delete(ctx, &RootCertificate{}, db.WithWhere(deleteWhere, cert.State)) + deleted, err := rw.Exec(ctx, `delete from worker_auth_ca_certificate where state = ?`, []any{cert.State}) assert.NoError(err) assert.Equal(1, deleted) }) @@ -334,20 +333,6 @@ func TestWorkerAuthStore(t *testing.T) { }, wantErr: false, }, - { - name: "workerauth-store-invalid-key-id", - args: args{ - workerKeyIdentifier: "worker-auth-id-456", - workerId: worker.PublicId, - }, - opt: []Option{ - WithWorkerKeys(workerKeys), - WithControllerEncryptionPrivateKey(controllerKey), - WithNonce(nonce), - WithKeyId("fakeyityfake"), - }, - wantCreateErr: true, - }, { name: "workerauth-no-pkey", args: args{ @@ -410,6 +395,8 @@ func TestWorkerAuthStore(t *testing.T) { require.NoError(err) require.NotNil(wAuth) + require.NoError(wAuth.encrypt(ctx, databaseWrapper)) + err = rw.Create(ctx, wAuth) if tt.wantCreateErr { assert.Error(err) @@ -418,6 +405,9 @@ func TestWorkerAuthStore(t *testing.T) { // Update and create time are automatically set tt.expectedWorkerAuth.CreateTime = wAuth.WorkerAuth.CreateTime tt.expectedWorkerAuth.UpdateTime = wAuth.WorkerAuth.UpdateTime + require.NoError(wAuth.decrypt(ctx, databaseWrapper)) + // Remove ciphertext since that's not included in the expected + wAuth.CtControllerEncryptionPrivKey = nil assert.Equal(tt.expectedWorkerAuth, wAuth.WorkerAuth) assert.Empty(cmp.Diff(tt.expectedWorkerAuth, wAuth, protocmp.Transform())) } @@ -431,10 +421,10 @@ func TestWorkerCertBundle(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - testKey := TestKmsKey(ctx, t, conn, wrapper) + testKey, kmsWrapper := TestKmsKey(ctx, t, conn, wrapper) worker := TestPkiWorker(t, conn, wrapper) - workerAuth := TestWorkerAuth(t, conn, worker, testKey) + workerAuth := TestWorkerAuth(t, conn, worker, kmsWrapper) rootCA := TestRootCertificate(ctx, t, conn, testKey) certBundle := populateBytes(defaultLength) diff --git a/internal/session/credential.go b/internal/session/credential.go index 522cff445..b7d7f6e26 100644 --- a/internal/session/credential.go +++ b/internal/session/credential.go @@ -12,10 +12,11 @@ import ( type Credential []byte type credential struct { - SessionId string - KeyId string - Credential []byte `gorm:"-" wrapping:"pt,credential_data"` - CtCredential []byte `gorm:"column:credential" wrapping:"ct,credential_data"` + SessionId string `gorm:"index:,unique,composite:session_credential_session_id_credential_sha256_uq"` + KeyId string + Credential []byte `gorm:"-" wrapping:"pt,credential_data"` + CtCredential []byte `gorm:"column:credential" wrapping:"ct,credential_data"` + CredentialSha256 []byte `gorm:"index:,unique,composite:session_credential_session_id_credential_sha256_uq"` } // TableName returns the table name. diff --git a/internal/session/options.go b/internal/session/options.go index 4e90a396a..f2ed301f5 100644 --- a/internal/session/options.go +++ b/internal/session/options.go @@ -1,6 +1,8 @@ package session import ( + "crypto/rand" + "io" "time" "github.com/hashicorp/boundary/internal/db" @@ -22,23 +24,25 @@ type Option func(*options) // options = how options are represented type options struct { - withLimit int - withOrderByCreateTime db.OrderBy - withProjectIds []string - withUserId string - withExpirationTime *timestamp.Timestamp - withTestTofu []byte - withListingConvert bool - withSessionIds []string - withDbOpts []db.Option - withWorkerStateDelay time.Duration - withTerminated bool - withPermissions *perms.UserPermissions + withLimit int + withOrderByCreateTime db.OrderBy + withProjectIds []string + withUserId string + withExpirationTime *timestamp.Timestamp + withTestTofu []byte + withSessionIds []string + withDbOpts []db.Option + withWorkerStateDelay time.Duration + withTerminated bool + withPermissions *perms.UserPermissions + withIgnoreDecryptionFailures bool + withRandomReader io.Reader } func getDefaultOptions() options { return options{ withWorkerStateDelay: 10 * time.Second, + withRandomReader: rand.Reader, } } @@ -95,12 +99,6 @@ func WithSessionIds(ids ...string) Option { } } -func withListingConvert(withListingConvert bool) Option { - return func(o *options) { - o.withListingConvert = withListingConvert - } -} - // WithDbOpts passes through given DB options to the DB layer func WithDbOpts(opts ...db.Option) Option { return func(o *options) { @@ -130,3 +128,21 @@ func WithPermissions(p *perms.UserPermissions) Option { o.withPermissions = p } } + +// WithIgnoreDecryptionFailures is used to ignore decryption +// failures when doing lookups. This should be used sparingly. +// It is currently only used to allow a user to cancel a session +// in the presence of a undecryptable TOFU token. +func WithIgnoreDecryptionFailures(ignoreFailures bool) Option { + return func(o *options) { + o.withIgnoreDecryptionFailures = ignoreFailures + } +} + +// WithRandomReader is used to configure the random source +// to use when generating secrets. Defaults to crypto/rand.Reader. +func WithRandomReader(rand io.Reader) Option { + return func(o *options) { + o.withRandomReader = rand + } +} diff --git a/internal/session/options_test.go b/internal/session/options_test.go index a0ed7c02b..5c3b56cbc 100644 --- a/internal/session/options_test.go +++ b/internal/session/options_test.go @@ -1,6 +1,7 @@ package session import ( + "strings" "testing" "github.com/hashicorp/boundary/internal/db" @@ -75,4 +76,19 @@ func Test_GetOpts(t *testing.T) { testOpts.withSessionIds = []string{"s_1", "s_2", "s_3"} assert.Equal(opts, testOpts) }) + t.Run("WithIgnoreDecryptionFailures", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithIgnoreDecryptionFailures(true)) + testOpts := getDefaultOptions() + testOpts.withIgnoreDecryptionFailures = true + assert.Equal(opts, testOpts) + }) + t.Run("WithRandomReader", func(t *testing.T) { + assert := assert.New(t) + reader := strings.NewReader("notrandom") + opts := getOpts(WithRandomReader(reader)) + testOpts := getDefaultOptions() + testOpts.withRandomReader = reader + assert.Equal(opts, testOpts) + }) } diff --git a/internal/session/query.go b/internal/session/query.go index 96447518d..1bae7ecf4 100644 --- a/internal/session/query.go +++ b/internal/session/query.go @@ -407,6 +407,25 @@ and and session_state.start_time < wt_sub_seconds_from_now(@threshold_seconds) ; +` + sessionCredentialRewrapQuery = ` +select distinct + cred.session_id, + cred.key_id, + cred.credential, + cred.credential_sha256 +from session + inner join session_credential cred + on cred.session_id = session.public_id +where session.project_id = ? + and cred.key_id = ? +` + sessionCredentialRewrapUpdate = ` +update session_credential + set credential = ?, + key_id = ? +where session_id = ? + and credential_sha256 = ?; ` ) @@ -424,7 +443,7 @@ values ` ) -func batchInsertsessionCredentialDynamic(creds []*DynamicCredential) (string, []any, error) { +func batchInsertSessionCredentialDynamic(creds []*DynamicCredential) (string, []any, error) { if len(creds) <= 0 { return "", nil, fmt.Errorf("empty slice of DynamicCredential, cannot build query") } diff --git a/internal/session/repository.go b/internal/session/repository.go index 9bcb64467..3800d031d 100644 --- a/internal/session/repository.go +++ b/internal/session/repository.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "io" "sort" "strings" @@ -14,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/util" ) // Clonable provides a cloning interface @@ -29,21 +31,23 @@ type Repository struct { // defaultLimit provides a default for limiting the number of results returned from the repo defaultLimit int - - permissions *perms.UserPermissions + permissions *perms.UserPermissions + randomReader io.Reader } // RepositoryFactory is a function that creates a Repository. type RepositoryFactory func(opt ...Option) (*Repository, error) -// NewRepository creates a new session Repository. Supports the options: WithLimit -// which sets a default limit on results returned by repo operations. +// NewRepository creates a new session Repository. Supports the options: +// - WithLimit, which sets a default limit on results returned by repo operations. +// - WithPermissions +// - WithRandomReader func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { const op = "session.NewRepository" - if r == nil { + if util.IsNil(r) { return nil, errors.New(ctx, errors.InvalidParameter, op, "nil reader") } - if w == nil { + if util.IsNil(w) { return nil, errors.New(ctx, errors.InvalidParameter, op, "nil writer") } if kms == nil { @@ -69,6 +73,7 @@ func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, kms: kms, defaultLimit: opts.withLimit, permissions: opts.withPermissions, + randomReader: opts.withRandomReader, }, nil } @@ -108,9 +113,8 @@ func (r *Repository) listPermissionWhereClauses() ([]string, []any) { return where, args } -func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessionListView, opt ...Option) ([]*Session, error) { +func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessionListView) ([]*Session, error) { const op = "session.(Repository).convertToSessions" - opts := getOpts(opt...) if len(sessionList) == 0 { return nil, nil @@ -134,41 +138,26 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi } prevSessionId = sv.PublicId workingSession = &Session{ - PublicId: sv.PublicId, - UserId: sv.UserId, - HostId: sv.HostId, - TargetId: sv.TargetId, - HostSetId: sv.HostSetId, - AuthTokenId: sv.AuthTokenId, - ProjectId: sv.ProjectId, - Certificate: sv.Certificate, - ExpirationTime: sv.ExpirationTime, - CtTofuToken: sv.CtTofuToken, - TofuToken: sv.TofuToken, // will always be nil since it's not stored in the database. - TerminationReason: sv.TerminationReason, - CreateTime: sv.CreateTime, - UpdateTime: sv.UpdateTime, - Version: sv.Version, - Endpoint: sv.Endpoint, - ConnectionLimit: sv.ConnectionLimit, - KeyId: sv.KeyId, - } - if opts.withListingConvert { - workingSession.CtTofuToken = nil // CtTofuToken should not returned in lists - workingSession.TofuToken = nil // TofuToken should not returned in lists - workingSession.KeyId = "" // KeyId should not be returned in lists - } else { - if len(workingSession.CtTofuToken) > 0 { - databaseWrapper, err := r.kms.GetWrapper(ctx, workingSession.ProjectId, kms.KeyPurposeDatabase) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) - } - if err := workingSession.decrypt(ctx, databaseWrapper); err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("cannot decrypt session value")) - } - } else { - workingSession.CtTofuToken = nil - } + PublicId: sv.PublicId, + UserId: sv.UserId, + HostId: sv.HostId, + TargetId: sv.TargetId, + HostSetId: sv.HostSetId, + AuthTokenId: sv.AuthTokenId, + ProjectId: sv.ProjectId, + Certificate: sv.Certificate, + CtCertificatePrivateKey: nil, // CtCertificatePrivateKey should not be returned in lists + CertificatePrivateKey: nil, // CertificatePrivateKey should not be returned in lists + ExpirationTime: sv.ExpirationTime, + CtTofuToken: nil, // CtTofuToken should not be returned in lists + TofuToken: nil, // TofuToken should not be returned in lists + TerminationReason: sv.TerminationReason, + CreateTime: sv.CreateTime, + UpdateTime: sv.UpdateTime, + Version: sv.Version, + Endpoint: sv.Endpoint, + ConnectionLimit: sv.ConnectionLimit, + KeyId: "", // KeyId should not be returned in lists } } diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index 58e90e924..68ac5408e 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -2,7 +2,6 @@ package session import ( "context" - "crypto/ed25519" "crypto/subtle" "database/sql" "fmt" @@ -13,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" wrapping "github.com/hashicorp/go-kms-wrapping/v2" ) @@ -20,62 +20,62 @@ import ( // its State of "Pending". The following fields must be empty when creating a // session: WorkerId, and PublicId. No options are // currently supported. -func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, workerAddresses []string, _ ...Option) (*Session, ed25519.PrivateKey, error) { +func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, workerAddresses []string, _ ...Option) (*Session, error) { const op = "session.(Repository).CreateSession" if newSession == nil { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing session") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session") } if newSession.PublicId != "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id is not empty") + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id is not empty") } if len(newSession.Certificate) != 0 { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "certificate is not empty") + return nil, errors.New(ctx, errors.InvalidParameter, op, "certificate is not empty") } if newSession.TargetId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id") } if newSession.HostId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing host id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host id") } if newSession.UserId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") } if newSession.HostSetId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing host set id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing host set id") } if newSession.AuthTokenId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token id") } if newSession.ProjectId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") } if newSession.CtTofuToken != nil { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "ct is not empty") + return nil, errors.New(ctx, errors.InvalidParameter, op, "ct is not empty") } if newSession.TofuToken != nil { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "tofu token is not empty") + return nil, errors.New(ctx, errors.InvalidParameter, op, "tofu token is not empty") } if newSession.ExpirationTime == nil || newSession.ExpirationTime.Timestamp.AsTime().IsZero() { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing expiration time") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing expiration time") } if len(workerAddresses) == 0 { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing addresses") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing addresses") } id, err := newId() if err != nil { - return nil, nil, errors.Wrap(ctx, err, op) + return nil, errors.Wrap(ctx, err, op) } - privKey, certBytes, err := newCert(ctx, sessionWrapper, newSession.UserId, id, workerAddresses, newSession.ExpirationTime.Timestamp.AsTime()) + privKey, certBytes, err := newCert(ctx, id, workerAddresses, newSession.ExpirationTime.Timestamp.AsTime(), r.randomReader) if err != nil { - return nil, nil, errors.Wrap(ctx, err, op) + return nil, errors.Wrap(ctx, err, op) } newSession.Certificate = certBytes + newSession.CertificatePrivateKey = privKey newSession.PublicId = id - newSession.KeyId, err = sessionWrapper.KeyId(ctx) - if err != nil { - return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to get session wrapper key id")) + if err := newSession.encrypt(ctx, sessionWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt session")) } var returnedSession *Session @@ -115,7 +115,7 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. } // TODO: after upgrading to gorm v2 this batch insert can be replaced, since gorm v2 supports batch inserts - q, batchInsertArgs, err := batchInsertsessionCredentialDynamic(newSession.DynamicCredentials) + q, batchInsertArgs, err := batchInsertSessionCredentialDynamic(newSession.DynamicCredentials) if err == nil { rows, err := w.Query(ctx, q, batchInsertArgs) if err != nil { @@ -148,20 +148,23 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. }, ) if err != nil { - return nil, nil, errors.Wrap(ctx, err, op) + return nil, errors.Wrap(ctx, err, op) } - return returnedSession, privKey, nil + return returnedSession, nil } // LookupSession will look up a session in the repository and return the session // with its states. Returned States are ordered by start time descending. If the -// session is not found, it will return nil, nil, nil. No options are currently -// supported. -func (r *Repository) LookupSession(ctx context.Context, sessionId string, _ ...Option) (*Session, *AuthzSummary, error) { +// session is not found, it will return nil, nil, nil. If the session has no user +// or project associated with it, decryption of fields will not be performed. +// Supported Options: +// - WithIgnoreDecryptionFailures +func (r *Repository) LookupSession(ctx context.Context, sessionId string, opt ...Option) (*Session, *AuthzSummary, error) { const op = "session.(Repository).LookupSession" if sessionId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") } + opts := getOpts(opt...) session := AllocSession() session.PublicId = sessionId _, err := r.writer.DoTx( @@ -199,6 +202,15 @@ func (r *Repository) LookupSession(ctx context.Context, sessionId string, _ ...O return errors.Wrap(ctx, err, op) } session.Connections = connections + if session.ProjectId == "" || session.UserId == "" { + // Skip decryption if Project ID or UserId is missing, + // since it will just lead to errors, and the session + // is already canceled if either of those are empty. + return nil + } + if err := decryptAndMaybeUpdateSession(ctx, r.kms, &session, w); err != nil && !opts.withIgnoreDecryptionFailures { + return errors.Wrap(ctx, err, op) + } return nil }, ) @@ -208,17 +220,6 @@ func (r *Repository) LookupSession(ctx context.Context, sessionId string, _ ...O } return nil, nil, errors.Wrap(ctx, err, op) } - if len(session.CtTofuToken) > 0 { - databaseWrapper, err := r.kms.GetWrapper(ctx, session.ProjectId, kms.KeyPurposeDatabase) - if err != nil { - return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) - } - if err := session.decrypt(ctx, databaseWrapper); err != nil { - return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("cannot decrypt session value")) - } - } else { - session.CtTofuToken = nil - } authzSummary, err := r.sessionAuthzSummary(ctx, sessionId) if err != nil { @@ -288,7 +289,7 @@ func (r *Repository) ListSessions(ctx context.Context, opt ...Option) ([]*Sessio } sessionsList = append(sessionsList, &s) } - sessions, err := r.convertToSessions(ctx, sessionsList, withListingConvert(true)) + sessions, err := r.convertToSessions(ctx, sessionsList) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -340,7 +341,9 @@ func (r *Repository) DeleteSession(ctx context.Context, publicId string, _ ...Op // session state to "canceling" for the given reason, so the workers can get the // "canceling signal" during their next status heartbeat. CancelSession is // idempotent. -func (r *Repository) CancelSession(ctx context.Context, sessionId string, sessionVersion uint32) (*Session, error) { +// Supported Options: +// - WithIgnoreDecryptionFailures +func (r *Repository) CancelSession(ctx context.Context, sessionId string, sessionVersion uint32, opt ...Option) (*Session, error) { const op = "session.(Repository).CancelSession" if sessionId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") @@ -348,7 +351,7 @@ func (r *Repository) CancelSession(ctx context.Context, sessionId string, sessio if sessionVersion == 0 { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing session version") } - s, ss, err := r.updateState(ctx, sessionId, sessionVersion, StatusCanceling) + s, ss, err := r.updateState(ctx, sessionId, sessionVersion, StatusCanceling, opt...) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -481,19 +484,22 @@ func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sess if err := reader.LookupById(ctx, &foundSession); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", sessionId))) } - databaseWrapper, err := r.kms.GetWrapper(ctx, foundSession.ProjectId, kms.KeyPurposeDatabase) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + if err := decryptAndMaybeUpdateSession(ctx, r.kms, &foundSession, w); err != nil { + return errors.Wrap(ctx, err, op) } if len(foundSession.TofuToken) > 0 && subtle.ConstantTimeCompare(foundSession.TofuToken, tofuToken) != 1 { return errors.New(ctx, errors.TokenMismatch, op, "tofu token mismatch") } updatedSession.TofuToken = tofuToken - if err := updatedSession.encrypt(ctx, databaseWrapper); err != nil { + sessionWrapper, err := r.kms.GetWrapper(ctx, foundSession.ProjectId, kms.KeyPurposeSessions) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get session wrapper")) + } + if err := updatedSession.encrypt(ctx, sessionWrapper); err != nil { return errors.Wrap(ctx, err, op) } - rowsUpdated, err := w.Update(ctx, &updatedSession, []string{"CtTofuToken"}, nil) + rowsUpdated, err := w.Update(ctx, &updatedSession, []string{"CtTofuToken", "KeyId"}, nil) if err != nil { return errors.Wrap(ctx, err, op) } @@ -518,7 +524,9 @@ func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sess // updateState will update the session's state using the session id and its // version. updateState is idempotent. States are ordered by start time // descending. No options are currently supported. -func (r *Repository) updateState(ctx context.Context, sessionId string, sessionVersion uint32, s Status, _ ...Option) (*Session, []*State, error) { +// Supported Options: +// - WithIgnoreDecryptionFailures +func (r *Repository) updateState(ctx context.Context, sessionId string, sessionVersion uint32, s Status, opt ...Option) (*Session, []*State, error) { const op = "session.(Repository).updateState" if sessionId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") @@ -532,6 +540,7 @@ func (r *Repository) updateState(ctx context.Context, sessionId string, sessionV if s == StatusActive { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "you must call ActivateSession to update a session's state to active") } + opts := getOpts(opt...) var rowsAffected int updatedSession := AllocSession() @@ -552,16 +561,8 @@ func (r *Repository) updateState(ctx context.Context, sessionId string, sessionV if rowsUpdated != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated session and %d rows updated", rowsUpdated)) } - if len(updatedSession.CtTofuToken) > 0 { - databaseWrapper, err := r.kms.GetWrapper(ctx, updatedSession.ProjectId, kms.KeyPurposeDatabase) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) - } - if err := updatedSession.decrypt(ctx, databaseWrapper); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("cannot decrypt session value")) - } - } else { - updatedSession.CtTofuToken = nil + if err := decryptAndMaybeUpdateSession(ctx, r.kms, &updatedSession, w); err != nil && !opts.withIgnoreDecryptionFailures { + return errors.Wrap(ctx, err, op) } rowsAffected, err = w.Exec(ctx, updateSessionState, []any{ @@ -680,3 +681,75 @@ func fetchConnections(ctx context.Context, r db.Reader, sessionId string, opt .. } return connections, nil } + +// decryptAndMaybeUpdateSession switches between the database key and session key based on whether +// the session uses a legacy private key or not. It also updates the encrypted session +// in the database (if necessary). Eventually we should be able to remove this function +// and use the session key unconditionally. +func decryptAndMaybeUpdateSession(ctx context.Context, kmsRepo *kms.Kms, session *Session, writer db.Writer) error { + const op = "session.decryptAndMaybeUpdateSession" + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repo") + } + if session == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing session") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + if session.ProjectId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing session project ID") + } + if session.KeyId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing session key ID") + } + if session.UserId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing session user ID") + } + if session.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing session ID") + } + sessionWrapper, err := kmsRepo.GetWrapper(ctx, session.ProjectId, kms.KeyPurposeSessions, kms.WithKeyId(session.KeyId)) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get session wrapper")) + } + if len(session.CtCertificatePrivateKey) > 0 { + // New-style session with private key stored in DB, just decrypt. + if err := session.decrypt(ctx, sessionWrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to decrypt session value")) + } + return nil + } + // No certificate private key present, this is a legacy session with a + // private key derived from the key. Derive it again and store it back + // in the DB. + _, session.CertificatePrivateKey, err = DeriveED25519Key(ctx, sessionWrapper, session.UserId, session.PublicId) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("Error deriving session key")) + } + updatedFields := []string{"CtCertificatePrivateKey", "KeyId"} + if len(session.CtTofuToken) > 0 { + // If the TOFU token was set on this session, we can decrypt it using + // the database key. + databaseWrapper, err := kmsRepo.GetWrapper(ctx, session.ProjectId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := session.decrypt(ctx, databaseWrapper); err != nil { + // Note; we can hit this error if the database key that + // was used to encrypt the TOFU token is no longer available + // in the wrapper. Try to return a useful error to the user. + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to decrypt session TOFU token. You may need to recreate your session.")) + } + updatedFields = append(updatedFields, "CtTofuToken") + } + // Rewrap with the session wrapper. Next time we look up this session + // all values will be encrypted using the session key. + if err := session.encrypt(ctx, sessionWrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to encrypt session value")) + } + if _, err := writer.Update(ctx, session, updatedFields, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update session value")) + } + return nil +} diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 4e4f07922..1b2e25523 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -275,9 +275,9 @@ func TestRepository_CreateSession(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrapper) - kms := kms.TestKms(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) ctx := context.Background() - repo, err := NewRepository(ctx, rw, rw, kms) + repo, err := NewRepository(ctx, rw, rw, kmsCache) require.NoError(t, err) workerAddresses := []string{"1.2.3.4"} @@ -392,6 +392,33 @@ func TestRepository_CreateSession(t *testing.T) { c := TestSessionParams(t, conn, wrapper, iamRepo) return c }(), + workerAddresses: nil, + }, + wantErr: true, + wantIsError: errors.InvalidParameter, + }, + { + name: "empty-expiration-time", + args: args{ + composedOf: func() ComposedOf { + c := TestSessionParams(t, conn, wrapper, iamRepo) + c.ExpirationTime = nil + return c + }(), + workerAddresses: nil, + }, + wantErr: true, + wantIsError: errors.InvalidParameter, + }, + { + name: "zero-expiration-time", + args: args{ + composedOf: func() ComposedOf { + c := TestSessionParams(t, conn, wrapper, iamRepo) + c.ExpirationTime = timestamp.New(time.Time{}) + return c + }(), + workerAddresses: nil, }, wantErr: true, wantIsError: errors.InvalidParameter, @@ -399,6 +426,12 @@ func TestRepository_CreateSession(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var wrapper wrapping.Wrapper + if tt.args.composedOf.ProjectId != "" { + wrapper, err = kmsCache.GetWrapper(context.Background(), tt.args.composedOf.ProjectId, kms.KeyPurposeSessions) + require.NoError(t, err) + } + assert, require := assert.New(t), require.New(t) s := &Session{ UserId: tt.args.composedOf.UserId, @@ -413,7 +446,8 @@ func TestRepository_CreateSession(t *testing.T) { DynamicCredentials: tt.args.composedOf.DynamicCredentials, StaticCredentials: tt.args.composedOf.StaticCredentials, } - ses, privKey, err := repo.CreateSession(context.Background(), wrapper, s, tt.args.workerAddresses) + ses, err := repo.CreateSession(context.Background(), wrapper, s, tt.args.workerAddresses) + if tt.wantErr { assert.Error(err) assert.Nil(ses) @@ -422,7 +456,7 @@ func TestRepository_CreateSession(t *testing.T) { } require.NoError(err) assert.NotNil(ses) - assert.NotNil(privKey) + assert.NotNil(ses.CertificatePrivateKey) assert.NotNil(ses.States) assert.NotNil(ses.CreateTime) assert.NotNil(ses.States[0].StartTime) @@ -433,7 +467,7 @@ func TestRepository_CreateSession(t *testing.T) { assert.Len(ses.DynamicCredentials, len(s.DynamicCredentials)) assert.Len(ses.StaticCredentials, len(s.StaticCredentials)) foundSession, _, err := repo.LookupSession(context.Background(), ses.PublicId) - assert.NoError(err) + require.NoError(err) assert.Equal(keyId, foundSession.KeyId) // Account for slight offsets in nanos @@ -689,7 +723,7 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { connRepo, err := NewConnectionRepository(ctx, rw, rw, kms) require.NoError(t, err) - setupFn := func(limit int32, expireIn time.Duration, leaveOpen bool) *Session { + setupFn := func(t testing.TB, limit int32, expireIn time.Duration, leaveOpen bool) *Session { require.NotEqualf(t, int32(0), limit, "setupFn: limit cannot be zero") exp := timestamppb.New(time.Now().Add(expireIn)) composedOf := TestSessionParams(t, conn, wrapper, iamRepo) @@ -720,23 +754,23 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { } tests := []struct { name string - setup func() testArgs + setup func(testing.TB) testArgs wantErr bool }{ { name: "sessions-with-closed-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 1 wantTermed := map[string]TerminationReason{} sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { // make one with closed connections - s := setupFn(1, time.Hour+1, false) + s := setupFn(t, 1, time.Hour+1, false) wantTermed[s.PublicId] = ConnectionLimit sessions = append(sessions, s) // make one with connection left open - s2 := setupFn(1, time.Hour+1, true) + s2 := setupFn(t, 1, time.Hour+1, true) sessions = append(sessions, s2) } return testArgs{ @@ -747,13 +781,13 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "sessions-with-open-and-closed-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 5 wantTermed := map[string]TerminationReason{} sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { // make one with closed connections - s := setupFn(2, time.Hour+1, false) + s := setupFn(t, 2, time.Hour+1, false) _ = TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 222, "127.0.0.1") sessions = append(sessions, s) wantTermed[s.PublicId] = ConnectionLimit @@ -766,7 +800,7 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "sessions-with-no-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 5 sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { @@ -781,11 +815,11 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "sessions-with-open-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 5 sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { - s := setupFn(1, time.Hour+1, true) + s := setupFn(t, 1, time.Hour+1, true) sessions = append(sessions, s) } return testArgs{ @@ -796,15 +830,15 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "expired-sessions", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 5 wantTermed := map[string]TerminationReason{} sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { // make one with closed connections - s := setupFn(1, time.Millisecond+1, false) + s := setupFn(t, 1, time.Millisecond+1, false) // make one with connection left open - s2 := setupFn(1, time.Millisecond+1, true) + s2 := setupFn(t, 1, time.Millisecond+1, true) sessions = append(sessions, s, s2) wantTermed[s.PublicId] = TimedOut } @@ -816,18 +850,18 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "canceled-sessions-with-closed-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 1 wantTermed := map[string]TerminationReason{} sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { // make one with limit of 3 and closed connections - s := setupFn(3, time.Hour+1, false) + s := setupFn(t, 3, time.Hour+1, false) wantTermed[s.PublicId] = SessionCanceled sessions = append(sessions, s) // make one with connection left open - s2 := setupFn(1, time.Hour+1, true) + s2 := setupFn(t, 1, time.Hour+1, true) sessions = append(sessions, s2) // now cancel the sessions @@ -844,15 +878,15 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { }, { name: "sessions-with-unlimited-connections", - setup: func() testArgs { + setup: func(t testing.TB) testArgs { cnt := 5 wantTermed := map[string]TerminationReason{} sessions := make([]*Session, 0, 5) for i := 0; i < cnt; i++ { // make one with unlimited connections - s := setupFn(-1, time.Hour+1, false) + s := setupFn(t, -1, time.Hour+1, false) // make one with limit of one all connections closed - s2 := setupFn(1, time.Hour+1, false) + s2 := setupFn(t, 1, time.Hour+1, false) sessions = append(sessions, s, s2) wantTermed[s2.PublicId] = ConnectionLimit } @@ -867,7 +901,7 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) db.TestDeleteWhere(t, conn, func() any { i := AllocSession(); return &i }(), "1=1") - args := tt.setup() + args := tt.setup(t) got, err := repo.TerminateCompletedSessions(context.Background()) if tt.wantErr { @@ -1165,6 +1199,22 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { } }(), }, + { + name: "Scope", + cancelFk: func() cancelFk { + s := setupFn() + + t := &iam.Scope{ + Scope: &iamStore.Scope{ + PublicId: s.ProjectId, + }, + } + return cancelFk{ + s: s, + fkType: t, + } + }(), + }, { name: "HostSet", cancelFk: func() cancelFk { @@ -1200,22 +1250,6 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { } }(), }, - { - name: "Scope", - cancelFk: func() cancelFk { - s := setupFn() - - t := &iam.Scope{ - Scope: &iamStore.Scope{ - PublicId: s.ProjectId, - }, - } - return cancelFk{ - s: s, - fkType: t, - } - }(), - }, { name: "canceled-only-once", cancelFk: func() cancelFk { @@ -1240,7 +1274,7 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, _, err := repo.LookupSession(context.Background(), tt.cancelFk.s.PublicId) + s, _, err := repo.LookupSession(context.Background(), tt.cancelFk.s.PublicId, WithIgnoreDecryptionFailures(true)) require.NoError(err) require.NotNil(s) require.NotNil(s.States) @@ -1259,7 +1293,7 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { require.NoError(err) require.Equal(1, rowsDeleted) - s, _, err = repo.LookupSession(context.Background(), tt.cancelFk.s.PublicId) + s, _, err = repo.LookupSession(context.Background(), tt.cancelFk.s.PublicId, WithIgnoreDecryptionFailures(true)) require.NoError(err) require.NotNil(s) require.NotNil(s.States) @@ -1658,3 +1692,99 @@ func TestRepository_deleteTerminated(t *testing.T) { }) } } + +func Test_decryptAndMaybeUpdateSession(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsRepo := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + ctx := context.Background() + + t.Run("errors-with-invalid-kms", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + err := decryptAndMaybeUpdateSession(ctx, nil, s, rw) + require.Error(t, err) + }) + t.Run("errors-with-invalid-session", func(t *testing.T) { + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, nil, rw) + require.Error(t, err) + }) + t.Run("errors-with-invalid-writer", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, nil) + require.Error(t, err) + }) + t.Run("errors-with-invalid-session-project-id", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.ProjectId = "" + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.Error(t, err) + }) + t.Run("errors-with-invalid-session-key-id", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.KeyId = "" + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.Error(t, err) + }) + t.Run("errors-with-invalid-session-user-id", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.UserId = "" + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.Error(t, err) + }) + t.Run("errors-with-invalid-session-public-id", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.PublicId = "" + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.Error(t, err) + }) + t.Run("session-with-local-session-key-succeeds", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.NoError(t, err) + }) + t.Run("session-with-derived-session-key-succeeds", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.CtCertificatePrivateKey = nil + s.CertificatePrivateKey = nil + s.TofuToken = nil + s.CtTofuToken = nil + err := decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.NoError(t, err) + }) + t.Run("session-with-derived-session-key-and-tofu-token-succeeds", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.CtCertificatePrivateKey = nil + s.CertificatePrivateKey = nil + s.TofuToken = []byte("A token") + actualKeyId := s.KeyId + databaseWrapper, err := kmsRepo.GetWrapper(ctx, s.ProjectId, kms.KeyPurposeDatabase) + require.NoError(t, err) + err = s.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + s.KeyId = actualKeyId // Restore this as the encrypt call above will overwrite it. + err = decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.NoError(t, err) + }) + t.Run("session-with-derived-session-key-and-tofu-token-cannot-be-decrypted", func(t *testing.T) { + s := TestDefaultSession(t, conn, wrapper, iamRepo) + s.CtCertificatePrivateKey = nil + s.CertificatePrivateKey = nil + s.TofuToken = []byte("A token") + actualKeyId := s.KeyId + databaseWrapper, err := kmsRepo.GetWrapper(ctx, s.ProjectId, kms.KeyPurposeDatabase) + require.NoError(t, err) + err = s.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + databaseKeyId := s.KeyId + err = kmsRepo.RotateKeys(ctx, s.ProjectId) + require.NoError(t, err) + ok, err := kmsRepo.DestroyKeyVersion(ctx, s.ProjectId, databaseKeyId) + require.NoError(t, err) + assert.True(t, ok) + s.KeyId = actualKeyId // Restore this as the encrypt call above will overwrite it. + err = decryptAndMaybeUpdateSession(ctx, kmsRepo, s, rw) + require.ErrorContains(t, err, "You may need to recreate your session") + }) +} diff --git a/internal/session/repository_test.go b/internal/session/repository_test.go index dc9983e07..73ef34cf6 100644 --- a/internal/session/repository_test.go +++ b/internal/session/repository_test.go @@ -1,10 +1,15 @@ package session import ( + "crypto/rand" + "fmt" + "strings" "testing" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/perms" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/context" @@ -17,11 +22,13 @@ func TestNewRepository(t *testing.T) { wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) ctx := context.Background() + testReader := strings.NewReader("notrandom") type args struct { - r db.Reader - w db.Writer - k *kms.Kms + r db.Reader + w db.Writer + k *kms.Kms + opts []Option } tests := []struct { name string @@ -37,11 +44,14 @@ func TestNewRepository(t *testing.T) { w: rw, k: testKms, }, - want: func() *Repository { - ret, err := NewRepository(ctx, rw, rw, testKms) - require.NoError(t, err) - return ret - }(), + want: &Repository{ + reader: rw, + writer: rw, + kms: testKms, + defaultLimit: db.DefaultLimit, + permissions: nil, + randomReader: rand.Reader, + }, wantErr: false, }, { @@ -64,11 +74,33 @@ func TestNewRepository(t *testing.T) { wantErr: true, wantErrString: "session.NewRepository: nil reader: parameter violation: error #100", }, + { + name: "providing-options", + args: args{ + r: rw, + w: rw, + k: testKms, + opts: []Option{ + WithLimit(100), + WithPermissions(&perms.UserPermissions{}), + WithRandomReader(testReader), + }, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: testKms, + defaultLimit: 100, + permissions: &perms.UserPermissions{}, + randomReader: testReader, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := NewRepository(ctx, tt.args.r, tt.args.w, tt.args.k) + got, err := NewRepository(ctx, tt.args.r, tt.args.w, tt.args.k, tt.args.opts...) if tt.wantErr { require.Error(err) assert.Equal(tt.wantErrString, err.Error()) @@ -79,3 +111,48 @@ func TestNewRepository(t *testing.T) { }) } } + +func TestRepository_convertToSessions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, rootWrapper) + kmsCache := kms.TestKms(t, conn, rootWrapper) + repo, err := NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + composedOf := TestSessionParams(t, conn, rootWrapper, iamRepo) + sess, err := New(composedOf) + require.NoError(t, err) + sessionWrapper, err := kmsCache.GetWrapper(ctx, sess.ProjectId, kms.KeyPurposeSessions) + require.NoError(t, err) + sess, err = repo.CreateSession(ctx, sessionWrapper, sess, []string{"0.0.0.0"}) + require.NoError(t, err) + + query := fmt.Sprintf(sessionList, "", "", "", "") + rows, err := rw.Query(ctx, query, nil) + require.NoError(t, err) + t.Cleanup(func() { + err := rows.Close() + require.NoError(t, err) + }) + + var sessionsList []*sessionListView + for rows.Next() { + var s sessionListView + err := rw.ScanRows(ctx, rows, &s) + require.NoError(t, err) + sessionsList = append(sessionsList, &s) + } + + sessions, err := repo.convertToSessions(ctx, sessionsList) + require.NoError(t, err) + assert.Len(t, sessions, 1) + // Check that encrypted values are redacted + sess.CtCertificatePrivateKey = nil + sess.CertificatePrivateKey = nil + sess.CtTofuToken = nil + sess.TofuToken = nil + sess.KeyId = "" + assert.Equal(t, sessions[0], sess) +} diff --git a/internal/session/rewrapping.go b/internal/session/rewrapping.go new file mode 100644 index 000000000..9260c3afc --- /dev/null +++ b/internal/session/rewrapping.go @@ -0,0 +1,124 @@ +package session + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +func init() { + kms.RegisterTableRewrapFn(defaultSessionTableName, sessionRewrapFn) + kms.RegisterTableRewrapFn("session_credential", sessionCredentialRewrapFn) +} + +func rewrapParameterChecks(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) string { + if dataKeyVersionId == "" { + return "missing data key version id" + } + if scopeId == "" { + return "missing scope id" + } + if util.IsNil(reader) { + return "missing database reader" + } + if util.IsNil(writer) { + return "missing database writer" + } + if kmsRepo == nil { + return "missing kms repository" + } + return "" +} + +func sessionCredentialRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "session.sessionCredentialRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var creds []*credential + // An index exists on (session_id, credential_sha256), so we can query workers via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + rows, err := reader.Query(ctx, sessionCredentialRewrapQuery, []interface{}{scopeId, dataKeyVersionId}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + defer rows.Close() + for rows.Next() { + cred := &credential{} + if err := rows.Scan( + &cred.SessionId, + &cred.KeyId, + &cred.CtCredential, + &cred.CredentialSha256, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to failed to scan row")) + } + creds = append(creds, cred) + } + if err := rows.Err(); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to iterate over retrieved rows")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cred := range creds { + if err := cred.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt session credential")) + } + if err := cred.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt session credential")) + } + if _, err := writer.Exec(ctx, sessionCredentialRewrapUpdate, []interface{}{ + cred.CtCredential, + cred.KeyId, + cred.SessionId, + cred.CredentialSha256, + }); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update session credential row with rewrapped fields")) + } + } + return nil +} + +func sessionRewrapFn(ctx context.Context, dataKeyVersionId string, scopeId string, reader db.Reader, writer db.Writer, kmsRepo *kms.Kms) error { + const op = "session.sessionRewrapFn" + if errStr := rewrapParameterChecks(ctx, dataKeyVersionId, scopeId, reader, writer, kmsRepo); errStr != "" { + return errors.New(ctx, errors.InvalidParameter, op, errStr) + } + var sessions []*Session + // An index exists on (project_id, user_id, termination_reason), so we can query sessions via scope and refine with key id. + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &sessions, "project_id=? and key_id=?", []interface{}{scopeId, dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + for _, session := range sessions { + if session.ProjectId == "" || session.UserId == "" { + // Skip decryption if Project ID or UserId is missing, + // since it will just lead to errors, and the session + // is already canceled. Unset KeyId to allow the key to + // be destroyed. + if _, err := writer.Update(ctx, session, nil, []string{"KeyId"}); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to unset key ID in canceled session")) + } + continue + } + if err := decryptAndMaybeUpdateSession(ctx, kmsRepo, session, writer); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt session")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, session.GetProjectId(), kms.KeyPurposeSessions) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + if err := session.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt session")) + } + if _, err := writer.Update(ctx, session, []string{"CtTofuToken", "CtCertificatePrivateKey", "KeyId"}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update session row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/session/rewrapping_test.go b/internal/session/rewrapping_test.go new file mode 100644 index 000000000..53aa9f4a7 --- /dev/null +++ b/internal/session/rewrapping_test.go @@ -0,0 +1,227 @@ +package session + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_sessionCredentialRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `select distinct cred\.session_id, cred\.key_id, cred\.credential, cred\.credential_sha256 from session inner join session_credential cred on cred\.session_id = session\.public_id where session\.project_id = \$1 and cred\.key_id = \$2`, + ).WillReturnError(errors.New("Query error")) + err := sessionCredentialRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + rw := db.New(conn) + + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + sess := TestSession(t, conn, rootWrapper, ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ProjectId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + cred := &credential{ + SessionId: sess.PublicId, + Credential: []byte("secret"), + } + + kmsWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + require.NoError(t, cred.encrypt(ctx, kmsWrapper)) + require.NoError(t, rw.Create(ctx, cred)) + + // now things are stored in the db, we can rotate and rewrap + require.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + require.NoError(t, sessionCredentialRewrapFn(ctx, cred.KeyId, prj.PublicId, rw, rw, kmsCache)) + + // now we pull the credential back from the db, decrypt it with the new key, and ensure things match + got := &credential{ + SessionId: sess.PublicId, + } + // this is the best way we have to query for this. there's only one index on this table and it's on session id and sha256. since we're testing there should be no others. + rows, err := rw.Query(ctx, `select credential, key_id, credential_sha256 from session_credential where session_id = ?`, []any{got.SessionId}) + require.NoError(t, err) + rowCount := 0 + + for rows.Next() { + rowCount++ + require.NoError(t, rows.Scan(&got.CtCredential, &got.KeyId, &got.CredentialSha256)) + } + assert.Equal(t, 1, rowCount) + + kmsWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase, kms.WithKeyId(got.KeyId)) + require.NoError(t, err) + + newKeyVersionId, err := kmsWrapper2.KeyId(ctx) + require.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + require.NoError(t, got.decrypt(ctx, kmsWrapper2)) + assert.NotEmpty(t, got.KeyId) + assert.NotEqual(t, cred.KeyId, got.KeyId) + assert.Equal(t, newKeyVersionId, got.KeyId) + assert.Equal(t, "secret", string(got.Credential)) + assert.NotEmpty(t, got.CredentialSha256) + assert.NotEqual(t, cred.CredentialSha256, got.CredentialSha256) + assert.NotEqual(t, cred.CtCredential, got.CtCredential) + }) +} + +func TestRewrap_sessionRewrapFn(t *testing.T) { + ctx := context.Background() + t.Run("errors-on-query-error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + wrapper := db.TestWrapper(t) + mock.ExpectQuery( + `SELECT \* FROM "kms_schema_version" WHERE 1=1 ORDER BY "kms_schema_version"\."version" LIMIT 1`, + ).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + kmsCache := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + mock.ExpectQuery( + `SELECT \* FROM "session" WHERE project_id=\$1 and key_id=\$2`, + ).WillReturnError(errors.New("Query error")) + err := sessionRewrapFn(ctx, "some_id", "some_scope", rw, rw, kmsCache) + require.Error(t, err) + }) + t.Run("success", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + rw := db.New(conn) + + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + session := TestSession(t, conn, rootWrapper, ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ProjectId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + // you cannot create a session with a pre-populated token, so do it in an update + session.TofuToken = []byte("token") + sessionWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeSessions) + require.NoError(t, err) + require.NoError(t, session.encrypt(ctx, sessionWrapper)) + _, err = rw.Update(ctx, session, []string{"CtTofuToken", "CertificatePrivateKey", "KeyId"}, nil) + require.NoError(t, err) + + // now things are stored in the db, we can rotate and rewrap + require.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + require.NoError(t, sessionRewrapFn(ctx, session.KeyId, prj.PublicId, rw, rw, kmsCache)) + + // now we pull the session back from the db, decrypt it with the new key, and ensure things match + got := &Session{} + got.PublicId = session.PublicId + require.NoError(t, rw.LookupById(ctx, got)) + + sessionWrapper2, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeSessions, kms.WithKeyId(got.KeyId)) + require.NoError(t, err) + + newKeyVersionId, err := sessionWrapper2.KeyId(ctx) + require.NoError(t, err) + + // decrypt with the new key version and check to make sure things match + require.NoError(t, got.decrypt(ctx, sessionWrapper2)) + assert.NotEmpty(t, got.KeyId) + assert.NotEqual(t, session.KeyId, got.KeyId) + assert.Equal(t, newKeyVersionId, got.KeyId) + assert.Equal(t, "token", string(got.TofuToken)) + assert.NotEqual(t, session.CtTofuToken, got.CtTofuToken) + // there is no hmac, so we're done + }) + t.Run("unsets-key-id-when-user-id-unset", func(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, prj := iam.TestScopes(t, iamRepo) + at := authtoken.TestAuthToken(t, conn, kmsCache, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + session := TestSession(t, conn, rootWrapper, ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ProjectId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + // you cannot create a session with a pre-populated token, so do it in an update + session.TofuToken = []byte("token") + sessionWrapper, err := kmsCache.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeSessions) + require.NoError(t, err) + require.NoError(t, session.encrypt(ctx, sessionWrapper)) + _, err = rw.Update(ctx, session, []string{"CtTofuToken", "CertificatePrivateKey", "KeyId"}, nil) + require.NoError(t, err) + + // Deleting the user unsets the user ID in the session + _, err = iamRepo.DeleteUser(ctx, uId) + require.NoError(t, err) + + // now things are stored in the db, we can rotate and rewrap + require.NoError(t, kmsCache.RotateKeys(ctx, prj.PublicId)) + require.NoError(t, sessionRewrapFn(ctx, session.KeyId, prj.PublicId, rw, rw, kmsCache)) + + // now we pull the session back from the db and check that the key ID is unset + got := &Session{} + got.PublicId = session.PublicId + require.NoError(t, rw.LookupById(ctx, got)) + assert.Empty(t, got.KeyId) + }) +} diff --git a/internal/session/session.go b/internal/session/session.go index b54835682..678a5da1b 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -3,8 +3,8 @@ package session import ( "context" "crypto/ed25519" - "crypto/rand" "crypto/x509" + "io" "math/big" mathrand "math/rand" "net" @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/util" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" "google.golang.org/protobuf/types/known/timestamppb" @@ -76,9 +77,14 @@ type Session struct { // ProjectId for the session ProjectId string `json:"project_id,omitempty" gorm:"default:null"` // Certificate to use when connecting (or if using custom certs, to - // serve as the "login"). Raw DER bytes. Private key is not, and should not be - // stored in the database. + // serve as the "login"). Raw DER bytes. Certificate []byte `json:"certificate,omitempty" gorm:"default:null"` + // CtCertificatePrivateKey is the ciphertext certificate private key which is stored in the database + CtCertificatePrivateKey []byte `json:"ct_certificate_private_key,omitempty" gorm:"column:certificate_private_key;default:null" wrapping:"ct,certificate_private_key"` + // CertificatePrivateKey is the certificate private key in plaintext. + // This may not be set for some sessions, in which case the private + // key should be derived from the encryption key referenced in key_id. + CertificatePrivateKey []byte `json:"certificate_private_key,omitempty" gorm:"-" wrapping:"pt,certificate_private_key"` // ExpirationTime - after this time the connection will be expired, e.g. forcefully terminated ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"` // CtTofuToken is the ciphertext Tofutoken value stored in the database @@ -100,11 +106,8 @@ type Session struct { // Worker filter WorkerFilter string `json:"-" gorm:"default:null"` - // key_id is the key ID that was used for the encryption operation. It can be - // used to identify a specific version of the key needed to decrypt the value, - // which is useful for caching purposes. - // @inject_tag: `gorm:"not_null"` - KeyId string `json:"key_id,omitempty" gorm:"not_null"` + // key_id is the ID of the key version used to encrypt any fields in this struct + KeyId string `json:"key_id,omitempty" gorm:"default:null"` // States for the session which are for read only and are ignored during // write operations @@ -218,6 +221,14 @@ func (s *Session) Clone() any { clone.Certificate = make([]byte, len(s.Certificate)) copy(clone.Certificate, s.Certificate) } + if s.CertificatePrivateKey != nil { + clone.CertificatePrivateKey = make([]byte, len(s.CertificatePrivateKey)) + copy(clone.CertificatePrivateKey, s.CertificatePrivateKey) + } + if s.CtCertificatePrivateKey != nil { + clone.CtCertificatePrivateKey = make([]byte, len(s.CtCertificatePrivateKey)) + copy(clone.CtCertificatePrivateKey, s.CtCertificatePrivateKey) + } if s.ExpirationTime != nil { clone.ExpirationTime = ×tamp.Timestamp{ Timestamp: ×tamppb.Timestamp{ @@ -367,21 +378,21 @@ func contains(ss []string, t string) bool { } // newCert creates a new session certificate. If addresses are supplied, they will be parsed and added to IP or DNS SANs as appropriate. -func newCert(ctx context.Context, wrapper wrapping.Wrapper, userId, jobId string, addresses []string, exp time.Time) (ed25519.PrivateKey, []byte, error) { +func newCert(ctx context.Context, jobId string, addresses []string, exp time.Time, rand io.Reader) (ed25519.PrivateKey, []byte, error) { const op = "session.newCert" - if wrapper == nil { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") - } - if userId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") - } if jobId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing job id") } if len(addresses) == 0 { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing addresses") } - pubKey, privKey, err := DeriveED25519Key(ctx, wrapper, userId, jobId) + if exp.IsZero() { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing expiry") + } + if util.IsNil(rand) { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing random data source") + } + pubKey, privKey, err := ed25519.GenerateKey(rand) if err != nil { return nil, nil, errors.Wrap(ctx, err, op) } @@ -420,7 +431,7 @@ func newCert(ctx context.Context, wrapper wrapping.Wrapper, userId, jobId string } } - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey) + certBytes, err := x509.CreateCertificate(rand, template, template, pubKey, privKey) if err != nil { return nil, nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.GenCert)) } @@ -429,8 +440,35 @@ func newCert(ctx context.Context, wrapper wrapping.Wrapper, userId, jobId string func (s *Session) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { const op = "session.(Session).encrypt" - if err := structwrapping.WrapStruct(ctx, cipher, s, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + if util.IsNil(cipher) { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + // Split encryption per field since they can be independently nil. + if len(s.TofuToken) > 0 { + type tofuToken struct { + TofuToken []byte `wrapping:"pt,tofu_token"` + CtTofuToken []byte `wrapping:"ct,tofu_token"` + } + v := &tofuToken{ + TofuToken: s.TofuToken, + } + if err := structwrapping.WrapStruct(ctx, cipher, v); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to encrypt tofu token")) + } + s.CtTofuToken = v.CtTofuToken + } + if len(s.CertificatePrivateKey) > 0 { + type certPk struct { + CertificatePrivateKey []byte `wrapping:"pt,certificate_private_key"` + CtCertificatePrivateKey []byte `wrapping:"ct,certificate_private_key"` + } + v := &certPk{ + CertificatePrivateKey: s.CertificatePrivateKey, + } + if err := structwrapping.WrapStruct(ctx, cipher, v); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to encrypt certificate private key")) + } + s.CtCertificatePrivateKey = v.CtCertificatePrivateKey } keyId, err := cipher.KeyId(ctx) if err != nil { @@ -442,32 +480,62 @@ func (s *Session) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { func (s *Session) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { const op = "session.(Session).decrypt" - if err := structwrapping.UnwrapStruct(ctx, cipher, s, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + if util.IsNil(cipher) { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") } + // Split decryption per field since they can be independently nil. + if len(s.CtTofuToken) > 0 { + type tofuToken struct { + TofuToken []byte `wrapping:"pt,tofu_token"` + CtTofuToken []byte `wrapping:"ct,tofu_token"` + } + v := &tofuToken{ + CtTofuToken: s.CtTofuToken, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, v); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt tofu token")) + } + s.TofuToken = v.TofuToken + } + if len(s.CtCertificatePrivateKey) > 0 { + type certPk struct { + CertificatePrivateKey []byte `wrapping:"pt,certificate_private_key"` + CtCertificatePrivateKey []byte `wrapping:"ct,certificate_private_key"` + } + v := &certPk{ + CtCertificatePrivateKey: s.CtCertificatePrivateKey, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, v); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt certificate private key")) + } + s.CertificatePrivateKey = v.CertificatePrivateKey + } + return nil } type sessionListView struct { // Session fields - PublicId string `json:"public_id,omitempty" gorm:"primary_key"` - UserId string `json:"user_id,omitempty" gorm:"default:null"` - HostId string `json:"host_id,omitempty" gorm:"default:null"` - TargetId string `json:"target_id,omitempty" gorm:"default:null"` - HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` - AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` - ProjectId string `json:"project_id,omitempty" gorm:"default:null"` - Certificate []byte `json:"certificate,omitempty" gorm:"default:null"` - ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"` - CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"` - TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"` - TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"` - CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"` - UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"` - Version uint32 `json:"version,omitempty" gorm:"default:null"` - Endpoint string `json:"-" gorm:"default:null"` - ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"` - KeyId string `json:"key_id,omitempty" gorm:"not_null"` + PublicId string `json:"public_id,omitempty" gorm:"primary_key"` + UserId string `json:"user_id,omitempty" gorm:"default:null"` + HostId string `json:"host_id,omitempty" gorm:"default:null"` + TargetId string `json:"target_id,omitempty" gorm:"default:null"` + HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` + AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` + ProjectId string `json:"project_id,omitempty" gorm:"default:null"` + Certificate []byte `json:"certificate,omitempty" gorm:"default:null"` + CtCertificatePrivateKey []byte `json:"ct_certificate_private_key,omitempty" gorm:"column:certificate_private_key;default:null" wrapping:"ct,certificate_private_key"` + CertificatePrivateKey []byte `json:"certificate_private_key,omitempty" gorm:"-" wrapping:"pt,certificate_private_key"` + ExpirationTime *timestamp.Timestamp `json:"expiration_time,omitempty" gorm:"default:null"` + CtTofuToken []byte `json:"ct_tofu_token,omitempty" gorm:"column:tofu_token;default:null" wrapping:"ct,tofu_token"` + TofuToken []byte `json:"tofu_token,omitempty" gorm:"-" wrapping:"pt,tofu_token"` + TerminationReason string `json:"termination_reason,omitempty" gorm:"default:null"` + CreateTime *timestamp.Timestamp `json:"create_time,omitempty" gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `json:"update_time,omitempty" gorm:"default:current_timestamp"` + Version uint32 `json:"version,omitempty" gorm:"default:null"` + Endpoint string `json:"-" gorm:"default:null"` + ConnectionLimit int32 `json:"connection_limit,omitempty" gorm:"default:null"` + KeyId string `json:"key_id,omitempty" gorm:"default:null"` // State fields Status string `json:"state,omitempty" gorm:"column:state"` diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 9e048d89b..d90fba65a 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -2,7 +2,11 @@ package session import ( "context" + "crypto" + "crypto/ed25519" + "crypto/rand" "crypto/x509" + "net" "testing" "time" @@ -181,7 +185,7 @@ func TestSession_Create(t *testing.T) { id, err := db.NewPublicId(SessionPrefix) require.NoError(err) got.PublicId = id - _, certBytes, err := newCert(ctx, wrapper, got.UserId, id, tt.args.addresses, composedOf.ExpirationTime.Timestamp.AsTime()) + privKey, certBytes, err := newCert(ctx, id, tt.args.addresses, composedOf.ExpirationTime.Timestamp.AsTime(), rand.Reader) if tt.wantAddrErr { require.Error(err) assert.True(errors.Match(errors.T(tt.wantIsErr), err)) @@ -189,6 +193,7 @@ func TestSession_Create(t *testing.T) { } require.NoError(err) got.Certificate = certBytes + got.CertificatePrivateKey = privKey err = db.New(conn).Create(ctx, got) if tt.wantCreateErr { assert.Error(err) @@ -317,3 +322,39 @@ func TestSession_SetTableName(t *testing.T) { }) } } + +func Test_newCert(t *testing.T) { + t.Parallel() + ctx := context.Background() + jobId := "job-id" + addresses := []string{"127.0.0.1", "localhost"} + expireTime := time.Now().Add(5 * time.Minute) + reader := rand.Reader + + t.Run("fails-on-invalid-job-id", func(t *testing.T) { + _, _, err := newCert(ctx, "", addresses, expireTime, reader) + require.Error(t, err) + }) + t.Run("fails-on-invalid-addresses", func(t *testing.T) { + _, _, err := newCert(ctx, jobId, nil, expireTime, reader) + require.Error(t, err) + }) + t.Run("fails-on-invalid-expiry", func(t *testing.T) { + _, _, err := newCert(ctx, jobId, addresses, time.Time{}, reader) + require.Error(t, err) + }) + t.Run("fails-on-invalid-random-reader", func(t *testing.T) { + _, _, err := newCert(ctx, jobId, addresses, expireTime, nil) + require.Error(t, err) + }) + t.Run("succeeds-on-valid-inputs", func(t *testing.T) { + key, cert, err := newCert(ctx, jobId, addresses, expireTime, reader) + require.NoError(t, err) + parsedCert, err := x509.ParseCertificate(cert) + require.NoError(t, err) + assert.Equal(t, parsedCert.DNSNames, []string{jobId, "localhost"}) + assert.Equal(t, parsedCert.IPAddresses, []net.IP{{127, 0, 0, 1}}) + assert.True(t, parsedCert.NotAfter.Equal(expireTime.Truncate(time.Second)), "NotAfter (%q) != expireTime (%q)", parsedCert.NotAfter.Format(time.RFC3339Nano), expireTime.Format(time.RFC3339Nano)) + assert.Equal(t, parsedCert.PublicKey.(crypto.PublicKey), ed25519.PrivateKey(key).Public()) + }) +} diff --git a/internal/session/testing.go b/internal/session/testing.go index 09857b634..99cfc8125 100644 --- a/internal/session/testing.go +++ b/internal/session/testing.go @@ -3,6 +3,7 @@ package session import ( "context" "crypto/ed25519" + "crypto/rand" "testing" "time" @@ -68,7 +69,7 @@ func TestState(t testing.TB, conn *db.DB, sessionId string, state Status) *State // TestSession creates a test session composed of c in the repository. Options // are passed into New, and withServerId is handled locally. -func TestSession(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, c ComposedOf, opt ...Option) *Session { +func TestSession(t testing.TB, conn *db.DB, rootWrapper wrapping.Wrapper, c ComposedOf, opt ...Option) *Session { t.Helper() ctx := context.Background() opts := getOpts(opt...) @@ -83,12 +84,14 @@ func TestSession(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, c Composed id, err := newId() require.NoError(err) s.PublicId = id - _, certBytes, err := newCert(ctx, wrapper, c.UserId, id, []string{"127.0.0.1", "localhost"}, c.ExpirationTime.Timestamp.AsTime()) + kmsCache := kms.TestKms(t, conn, rootWrapper) + wrapper, err := kmsCache.GetWrapper(ctx, c.ProjectId, kms.KeyPurposeSessions) + require.NoError(err) + privateKey, certBytes, err := newCert(ctx, id, []string{"127.0.0.1", "localhost"}, c.ExpirationTime.Timestamp.AsTime(), rand.Reader) require.NoError(err) s.Certificate = certBytes - if len(s.TofuToken) != 0 { - err = s.encrypt(ctx, wrapper) - } + s.CertificatePrivateKey = privateKey + err = s.encrypt(ctx, wrapper) require.NoError(err) err = rw.Create(ctx, s, opts.withDbOpts...) if e, ok := err.(*errors.Err); err != nil && ok && e.Code == errors.NotUnique { @@ -185,6 +188,6 @@ func TestTofu(t testing.TB) []byte { // TestCert is a temporary test func that intentionally doesn't take testing.T // as a parameter. It's currently used in controller.jobTestingHandler() and // should be deprecated once that function is refactored to use sessions properly. -func TestCert(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.PrivateKey, []byte, error) { - return newCert(context.Background(), wrapper, userId, jobId, []string{"127.0.0.1", "localhost"}, time.Now().Add(5*time.Minute)) +func TestCert(jobId string) (ed25519.PrivateKey, []byte, error) { + return newCert(context.Background(), jobId, []string{"127.0.0.1", "localhost"}, time.Now().Add(5*time.Minute), rand.Reader) } diff --git a/internal/session/testing_test.go b/internal/session/testing_test.go index f129fb64b..262d7cc5f 100644 --- a/internal/session/testing_test.go +++ b/internal/session/testing_test.go @@ -80,12 +80,9 @@ func Test_TestWorker(t *testing.T) { func Test_TestCert(t *testing.T) { assert, require := assert.New(t), require.New(t) - wrapper := db.TestWrapper(t) - userId, err := db.NewPublicId(iam.UserPrefix) - require.NoError(err) sessionId, err := newId() require.NoError(err) - key, cert, err := TestCert(wrapper, userId, sessionId) + key, cert, err := TestCert(sessionId) require.NoError(err) assert.NotNil(key) assert.NotNil(cert) diff --git a/internal/tests/api/credentials/credentials_test.go b/internal/tests/api/credentials/credentials_test.go index fa09f5c5c..8cb159155 100644 --- a/internal/tests/api/credentials/credentials_test.go +++ b/internal/tests/api/credentials/credentials_test.go @@ -4,13 +4,23 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/credentials" "github.com/hashicorp/boundary/api/credentialstores" + "github.com/hashicorp/boundary/api/hostcatalogs" + "github.com/hashicorp/boundary/api/hosts" + "github.com/hashicorp/boundary/api/hostsets" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/api/targets" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/daemon/controller" + _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets/tcp" + "github.com/hashicorp/boundary/internal/daemon/worker" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/testdata" @@ -384,3 +394,112 @@ func TestErrors(t *testing.T) { assert.NotNil(apiErr) assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) } + +// TestUpdateAfterKeyRotation sets up a scenario where a JSON credential +// is re-encrypted with the newest key version during an update, and checks +// that the key version ID is tracked correctly. If the key version ID +// is not updated during the credential update, the data becomes +// irrecoverable since the encryption key is destroyed. +func TestUpdateAfterKeyRotation(t *testing.T) { + require, assert := require.New(t), assert.New(t) + logger := hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + }) + tc := controller.NewTestController(t, &controller.TestControllerOpts{SchedulerRunJobInterval: 100 * time.Millisecond}) + ctx := tc.Context() + t.Cleanup(tc.Shutdown) + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + scopesClient := scopes.NewClient(client) + credsClient := credentials.NewClient(client) + tgClient := targets.NewClient(client) + hostClient := hosts.NewClient(client) + hsClient := hostsets.NewClient(client) + hcClient := hostcatalogs.NewClient(client) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + + hc, err := hcClient.Create(ctx, "static", proj.PublicId, hostcatalogs.WithName("my-host-catalog")) + require.NoError(err) + host, err := hostClient.Create(ctx, hc.Item.Id, hosts.WithName("my-host"), hosts.WithStaticHostAddress("example.com")) + require.NoError(err) + hs, err := hsClient.Create(ctx, hc.Item.Id, hostsets.WithName("my-host-set")) + require.NoError(err) + _, err = hsClient.AddHosts(ctx, hs.Item.Id, 1, []string{host.Item.Id}) + require.NoError(err) + cs, err := credentialstores.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId()) + require.NoError(err) + obj := map[string]any{ + "username": "admin", + "password": "pass", + } + cred, err := credsClient.Create(ctx, credential.JsonSubtype.String(), cs.Item.Id, credentials.WithName("foo"), credentials.WithJsonCredentialObject(obj)) + require.NoError(err) + targ, err := tgClient.Create(ctx, "tcp", proj.PublicId, targets.WithName("my-target"), targets.WithTcpTargetDefaultPort(22)) + require.NoError(err) + _, err = tgClient.AddHostSources(ctx, targ.Item.Id, 1, []string{hs.Item.Id}) + require.NoError(err) + _, err = tgClient.AddCredentialSources(ctx, targ.Item.Id, 2, targets.WithBrokeredCredentialSourceIds([]string{cred.Item.Id})) + w := worker.NewTestWorker(t, &worker.TestWorkerOpts{ + InitialUpstreams: tc.ClusterAddrs(), + Logger: logger.Named("worker"), + WorkerAuthKms: tc.Config().WorkerAuthKms, + Name: "worker", + }) + t.Cleanup(w.Shutdown) + require.NoError(w.Worker().WaitForNextSuccessfulStatusUpdate()) + + // Authorize session, requires decrypting json credential + _, err = tgClient.AuthorizeSession(ctx, targ.Item.Id) + require.NoError(err) + + // Create new key versions + _, err = scopesClient.RotateKeys(ctx, proj.PublicId, false) + require.NoError(err) + + // Update JSON credential, will re-encrypt with new key versions + obj["password"] = "password" + cred, err = credsClient.Update(ctx, cred.Item.Id, 1, credentials.WithJsonCredentialObject(obj)) + require.NoError(err) + + // Create new key versions again + _, err = scopesClient.RotateKeys(ctx, proj.PublicId, false) + require.NoError(err) + + keys, err := scopesClient.ListKeys(ctx, proj.PublicId) + require.NoError(err) + + var destroyKeyVersion *scopes.KeyVersion + + for _, key := range keys.Items { + if key.Purpose == kms.KeyPurposeDatabase.String() { + // Versions are sorted in descending order, this gets the 2nd version. + // It is the keyversion that was used to re-encrypt our JSON credential. + destroyKeyVersion = key.Versions[1] + break + } + } + + // Destroy the older key version + result, err := scopesClient.DestroyKeyVersion(ctx, proj.PublicId, destroyKeyVersion.Id) + require.NoError(err) + // Should start asynchronous rewrapping of the encrypted JSON credential + assert.Equal("pending", result.State) + + for { + jobs, err := scopesClient.ListKeyVersionDestructionJobs(ctx, proj.PublicId) + require.NoError(err) + if len(jobs.Items) == 1 { + break + } + select { + case <-ctx.Done(): + break + case <-time.After(time.Millisecond * 100): + } + } + + // Authorize session, requires decrypting json credential again + _, err = tgClient.AuthorizeSession(ctx, targ.Item.Id) + require.NoError(err) +} diff --git a/internal/tests/api/scopes/scope_test.go b/internal/tests/api/scopes/scope_test.go index 836c36511..06bdaf18e 100644 --- a/internal/tests/api/scopes/scope_test.go +++ b/internal/tests/api/scopes/scope_test.go @@ -1,14 +1,17 @@ package scopes_test import ( + "context" "fmt" "net/http" "testing" + "time" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/internal/daemon/controller" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -152,3 +155,102 @@ func TestErrors(t *testing.T) { assert.NotNil(apiErr) assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) } + +func TestKeyDestruction(t *testing.T) { + ctx := context.Background() + tc := controller.NewTestController(t, &controller.TestControllerOpts{SchedulerRunJobInterval: time.Second}) + t.Cleanup(tc.Shutdown) + c := tc.Client() + c.SetToken(tc.Token().Token) + sc := scopes.NewClient(c) + + keys, err := sc.ListKeys(ctx, "global") + require.NoError(t, err) + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1) + for _, key := range keys.Items { + assert.Len(t, key.Versions, 1) + } + + _, err = sc.RotateKeys(ctx, "global", false) + require.NoError(t, err) + + keys, err = sc.ListKeys(ctx, "global") + require.NoError(t, err) + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1) + for _, key := range keys.Items { + assert.Len(t, key.Versions, 2) + } + + // Root key is always last, by virtue of sorting by ID + rootKeyVersion := keys.Items[len(keys.Items)-1].Versions[1] + var destroyKeyVersion *scopes.KeyVersion + for _, key := range keys.Items { + if key.Purpose == kms.KeyPurposeDatabase.String() { + destroyKeyVersion = key.Versions[1] + } + } + + jobs, err := sc.ListKeyVersionDestructionJobs(ctx, "global") + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + result, err := sc.DestroyKeyVersion(ctx, "global", rootKeyVersion.Id) + require.NoError(t, err) + assert.Equal(t, "completed", result.State) + + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, "global") + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + keys, err = sc.ListKeys(ctx, "global") + require.NoError(t, err) + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1) + for _, key := range keys.Items { + switch key.Purpose { + case "rootKey": + assert.Len(t, key.Versions, 1) + default: + assert.Len(t, key.Versions, 2) + } + } + + result, err = sc.DestroyKeyVersion(ctx, "global", destroyKeyVersion.Id) + require.NoError(t, err) + assert.Equal(t, "pending", result.State) + + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, "global") + require.NoError(t, err) + assert.Len(t, jobs.Items, 1) + + // The configured scheduler monitoring interval is 1 second. The jobs become available 1 + // second after the last successful run. We need to re-encrypt data in 4 different tables, + // and then we need to destroy the key. This job will take between 4 and 5 seconds to run, + // depending on the timing of the first started run. + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + t.Cleanup(cancel) + for { + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, "global") + require.NoError(t, err) + if len(jobs.Items) == 0 { + break + } + select { + case <-time.After(time.Second): + case <-ctx.Done(): + t.Log(jobs.GetItems()[0]) + t.Fatal("Test timed out waiting for destruction to finish") + } + } + + keys, err = sc.ListKeys(ctx, "global") + require.NoError(t, err) + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1) + for _, key := range keys.Items { + switch key.Purpose { + case "rootKey", "database": + assert.Len(t, key.Versions, 1) + default: + assert.Len(t, key.Versions, 2) + } + } +} diff --git a/internal/types/action/action.go b/internal/types/action/action.go index 894d15a87..87323ffaf 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -8,58 +8,62 @@ import ( type Type uint const ( - Unknown Type = 0 - List Type = 1 - Create Type = 2 - Update Type = 3 - Read Type = 4 - Delete Type = 5 - Authenticate Type = 6 - All Type = 7 - AuthorizeSession Type = 8 - AddGrants Type = 9 - RemoveGrants Type = 10 - SetGrants Type = 11 - AddPrincipals Type = 12 - SetPrincipals Type = 13 - RemovePrincipals Type = 14 - Deauthenticate Type = 15 - AddMembers Type = 16 - SetMembers Type = 17 - RemoveMembers Type = 18 - SetPassword Type = 19 - ChangePassword Type = 20 - AddHosts Type = 21 - SetHosts Type = 22 - RemoveHosts Type = 23 - AddHostSets Type = 24 // OBSOLETE - SetHostSets Type = 25 // OBSOLETE - RemoveHostSets Type = 26 // OBSOLETE - Cancel Type = 27 - AddAccounts Type = 28 - SetAccounts Type = 29 - RemoveAccounts Type = 30 - ReadSelf Type = 31 - CancelSelf Type = 32 - ChangeState Type = 33 - DeleteSelf Type = 34 - NoOp Type = 35 - AddCredentialLibraries Type = 36 // DEPRECATED - SetCredentialLibraries Type = 37 // DEPRECATED - RemoveCredentialLibraries Type = 38 // DEPRECATED - AddCredentialSources Type = 39 - SetCredentialSources Type = 40 - RemoveCredentialSources Type = 41 - AddHostSources Type = 42 - SetHostSources Type = 43 - RemoveHostSources Type = 44 - CreateWorkerLed Type = 45 - AddWorkerTags Type = 46 - SetWorkerTags Type = 47 - RemoveWorkerTags Type = 48 - CreateControllerLed Type = 49 - ReinitializeCertificateAuthority Type = 50 - ReadCertificateAuthority Type = 51 + Unknown Type = 0 + List Type = 1 + Create Type = 2 + Update Type = 3 + Read Type = 4 + Delete Type = 5 + Authenticate Type = 6 + All Type = 7 + AuthorizeSession Type = 8 + AddGrants Type = 9 + RemoveGrants Type = 10 + SetGrants Type = 11 + AddPrincipals Type = 12 + SetPrincipals Type = 13 + RemovePrincipals Type = 14 + Deauthenticate Type = 15 + AddMembers Type = 16 + SetMembers Type = 17 + RemoveMembers Type = 18 + SetPassword Type = 19 + ChangePassword Type = 20 + AddHosts Type = 21 + SetHosts Type = 22 + RemoveHosts Type = 23 + AddHostSets Type = 24 // OBSOLETE + SetHostSets Type = 25 // OBSOLETE + RemoveHostSets Type = 26 // OBSOLETE + Cancel Type = 27 + AddAccounts Type = 28 + SetAccounts Type = 29 + RemoveAccounts Type = 30 + ReadSelf Type = 31 + CancelSelf Type = 32 + ChangeState Type = 33 + DeleteSelf Type = 34 + NoOp Type = 35 + AddCredentialLibraries Type = 36 // DEPRECATED + SetCredentialLibraries Type = 37 // DEPRECATED + RemoveCredentialLibraries Type = 38 // DEPRECATED + AddCredentialSources Type = 39 + SetCredentialSources Type = 40 + RemoveCredentialSources Type = 41 + AddHostSources Type = 42 + SetHostSources Type = 43 + RemoveHostSources Type = 44 + CreateWorkerLed Type = 45 + AddWorkerTags Type = 46 + SetWorkerTags Type = 47 + RemoveWorkerTags Type = 48 + CreateControllerLed Type = 49 + ReinitializeCertificateAuthority Type = 50 + ReadCertificateAuthority Type = 51 + ListScopeKeys Type = 52 + RotateScopeKeys Type = 53 + ListScopeKeyVersionDestructionJobs Type = 54 + DestroyScopeKeyVersion Type = 55 // When adding new actions, be sure to update: // @@ -68,57 +72,61 @@ const ( ) var Map = map[string]Type{ - Create.String(): Create, - List.String(): List, - Update.String(): Update, - Read.String(): Read, - Delete.String(): Delete, - Authenticate.String(): Authenticate, - All.String(): All, - AuthorizeSession.String(): AuthorizeSession, - AddGrants.String(): AddGrants, - RemoveGrants.String(): RemoveGrants, - SetGrants.String(): SetGrants, - AddPrincipals.String(): AddPrincipals, - SetPrincipals.String(): SetPrincipals, - RemovePrincipals.String(): RemovePrincipals, - Deauthenticate.String(): Deauthenticate, - AddMembers.String(): AddMembers, - SetMembers.String(): SetMembers, - RemoveMembers.String(): RemoveMembers, - SetPassword.String(): SetPassword, - ChangePassword.String(): ChangePassword, - AddHosts.String(): AddHosts, - SetHosts.String(): SetHosts, - RemoveHosts.String(): RemoveHosts, - AddHostSets.String(): AddHostSets, - SetHostSets.String(): SetHostSets, - RemoveHostSets.String(): RemoveHostSets, - Cancel.String(): Cancel, - AddAccounts.String(): AddAccounts, - SetAccounts.String(): SetAccounts, - RemoveAccounts.String(): RemoveAccounts, - ReadSelf.String(): ReadSelf, - CancelSelf.String(): CancelSelf, - ChangeState.String(): ChangeState, - DeleteSelf.String(): DeleteSelf, - NoOp.String(): NoOp, - AddCredentialLibraries.String(): AddCredentialLibraries, - SetCredentialLibraries.String(): SetCredentialLibraries, - RemoveCredentialLibraries.String(): RemoveCredentialLibraries, - AddCredentialSources.String(): AddCredentialSources, - SetCredentialSources.String(): SetCredentialSources, - RemoveCredentialSources.String(): RemoveCredentialSources, - AddHostSources.String(): AddHostSources, - SetHostSources.String(): SetHostSources, - RemoveHostSources.String(): RemoveHostSources, - CreateWorkerLed.String(): CreateWorkerLed, - AddWorkerTags.String(): AddWorkerTags, - SetWorkerTags.String(): SetWorkerTags, - RemoveWorkerTags.String(): RemoveWorkerTags, - CreateControllerLed.String(): CreateControllerLed, - ReinitializeCertificateAuthority.String(): ReinitializeCertificateAuthority, - ReadCertificateAuthority.String(): ReadCertificateAuthority, + Create.String(): Create, + List.String(): List, + Update.String(): Update, + Read.String(): Read, + Delete.String(): Delete, + Authenticate.String(): Authenticate, + All.String(): All, + AuthorizeSession.String(): AuthorizeSession, + AddGrants.String(): AddGrants, + RemoveGrants.String(): RemoveGrants, + SetGrants.String(): SetGrants, + AddPrincipals.String(): AddPrincipals, + SetPrincipals.String(): SetPrincipals, + RemovePrincipals.String(): RemovePrincipals, + Deauthenticate.String(): Deauthenticate, + AddMembers.String(): AddMembers, + SetMembers.String(): SetMembers, + RemoveMembers.String(): RemoveMembers, + SetPassword.String(): SetPassword, + ChangePassword.String(): ChangePassword, + AddHosts.String(): AddHosts, + SetHosts.String(): SetHosts, + RemoveHosts.String(): RemoveHosts, + AddHostSets.String(): AddHostSets, + SetHostSets.String(): SetHostSets, + RemoveHostSets.String(): RemoveHostSets, + Cancel.String(): Cancel, + AddAccounts.String(): AddAccounts, + SetAccounts.String(): SetAccounts, + RemoveAccounts.String(): RemoveAccounts, + ReadSelf.String(): ReadSelf, + CancelSelf.String(): CancelSelf, + ChangeState.String(): ChangeState, + DeleteSelf.String(): DeleteSelf, + NoOp.String(): NoOp, + AddCredentialLibraries.String(): AddCredentialLibraries, + SetCredentialLibraries.String(): SetCredentialLibraries, + RemoveCredentialLibraries.String(): RemoveCredentialLibraries, + AddCredentialSources.String(): AddCredentialSources, + SetCredentialSources.String(): SetCredentialSources, + RemoveCredentialSources.String(): RemoveCredentialSources, + AddHostSources.String(): AddHostSources, + SetHostSources.String(): SetHostSources, + RemoveHostSources.String(): RemoveHostSources, + CreateWorkerLed.String(): CreateWorkerLed, + AddWorkerTags.String(): AddWorkerTags, + SetWorkerTags.String(): SetWorkerTags, + RemoveWorkerTags.String(): RemoveWorkerTags, + CreateControllerLed.String(): CreateControllerLed, + ReinitializeCertificateAuthority.String(): ReinitializeCertificateAuthority, + ReadCertificateAuthority.String(): ReadCertificateAuthority, + ListScopeKeys.String(): ListScopeKeys, + RotateScopeKeys.String(): RotateScopeKeys, + ListScopeKeyVersionDestructionJobs.String(): ListScopeKeyVersionDestructionJobs, + DestroyScopeKeyVersion.String(): DestroyScopeKeyVersion, } var DeprecatedMap = map[string]Type{ @@ -184,6 +192,10 @@ func (a Type) String() string { "create:controller-led", "reinitialize-certificate-authority", "read-certificate-authority", + "list-keys", + "rotate-keys", + "list-key-version-destruction-jobs", + "destroy-key-version", }[a] } diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index 82dbbb70c..a5389f279 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -115,6 +115,22 @@ func TestAction(t *testing.T) { action: CreateControllerLed, want: "create:controller-led", }, + { + action: ListScopeKeys, + want: "list-keys", + }, + { + action: RotateScopeKeys, + want: "rotate-keys", + }, + { + action: ListScopeKeyVersionDestructionJobs, + want: "list-key-version-destruction-jobs", + }, + { + action: DestroyScopeKeyVersion, + want: "destroy-key-version", + }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { diff --git a/internal/util/template/generate.go b/internal/util/template/generate.go index b833426d3..0250df6c6 100644 --- a/internal/util/template/generate.go +++ b/internal/util/template/generate.go @@ -28,7 +28,7 @@ func New(ctx context.Context, raw string) (*Parsed, error) { ret := &Parsed{ raw: raw, - funcMap: map[string]interface{}{ + funcMap: map[string]any{ "truncateFrom": truncateFrom, }, } @@ -45,7 +45,7 @@ func New(ctx context.Context, raw string) (*Parsed, error) { } // Generate based on the provided template -func (p *Parsed) Generate(ctx context.Context, data interface{}) (string, error) { +func (p *Parsed) Generate(ctx context.Context, data any) (string, error) { const op = "util.template.(Parsed).Generate" switch { diff --git a/sdk/pbs/controller/api/resources/scopes/scope.pb.go b/sdk/pbs/controller/api/resources/scopes/scope.pb.go index 512470374..b20a5f298 100644 --- a/sdk/pbs/controller/api/resources/scopes/scope.pb.go +++ b/sdk/pbs/controller/api/resources/scopes/scope.pb.go @@ -258,6 +258,262 @@ func (x *Scope) GetAuthorizedCollectionActions() map[string]*structpb.ListValue return nil } +// KeyVersion describes a specific version of a key and holds the actual key material +type KeyVersion struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ID of the key version. + Id string `protobuf:"bytes,10,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` + // The iteration of the Key that this version represents. + Version uint32 `protobuf:"varint,20,opt,name=version,proto3" json:"version,omitempty" class:"public"` // @gotags: `class:"public"` + // When this version was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,30,opt,name=created_time,proto3" json:"created_time,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *KeyVersion) Reset() { + *x = KeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_scopes_v1_scope_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyVersion) ProtoMessage() {} + +func (x *KeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_scopes_v1_scope_proto_msgTypes[2] + 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 KeyVersion.ProtoReflect.Descriptor instead. +func (*KeyVersion) Descriptor() ([]byte, []int) { + return file_controller_api_resources_scopes_v1_scope_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyVersion) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *KeyVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *KeyVersion) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +// Key contains all fields related to a Key in a Scope. +type Key struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ID of the Key. + Id string `protobuf:"bytes,10,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` + // Scope information for this resource. + Scope *ScopeInfo `protobuf:"bytes,20,opt,name=scope,proto3" json:"scope,omitempty"` + // The purpose of the Key. + Purpose string `protobuf:"bytes,30,opt,name=purpose,proto3" json:"purpose,omitempty" class:"public"` // @gotags: `class:"public"` + // The time this Key was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,40,opt,name=created_time,proto3" json:"created_time,omitempty" class:"public"` // @gotags: `class:"public"` + // The type of the Key. + Type string `protobuf:"bytes,50,opt,name=type,proto3" json:"type,omitempty" class:"public"` // @gotags: `class:"public"` + // The versions of the key. + Versions []*KeyVersion `protobuf:"bytes,60,rep,name=versions,proto3" json:"versions,omitempty"` +} + +func (x *Key) Reset() { + *x = Key{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_scopes_v1_scope_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Key) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Key) ProtoMessage() {} + +func (x *Key) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_scopes_v1_scope_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 Key.ProtoReflect.Descriptor instead. +func (*Key) Descriptor() ([]byte, []int) { + return file_controller_api_resources_scopes_v1_scope_proto_rawDescGZIP(), []int{3} +} + +func (x *Key) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Key) GetScope() *ScopeInfo { + if x != nil { + return x.Scope + } + return nil +} + +func (x *Key) GetPurpose() string { + if x != nil { + return x.Purpose + } + return "" +} + +func (x *Key) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *Key) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Key) GetVersions() []*KeyVersion { + if x != nil { + return x.Versions + } + return nil +} + +// KeyVersionDestructionJob holds information about a pending key version destruction job. +type KeyVersionDestructionJob struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ID of the Key version this job relates to. + KeyVersionId string `protobuf:"bytes,10,opt,name=key_version_id,json=keyVersionId,proto3" json:"key_version_id,omitempty" class:"public"` // @gotags: `class:"public"` + // Scope information for this resource. + Scope *ScopeInfo `protobuf:"bytes,20,opt,name=scope,proto3" json:"scope,omitempty"` + // The current status of the key version destruction job. One of "pending", "running" or "completed". + Status string `protobuf:"bytes,30,opt,name=status,proto3" json:"status,omitempty" class:"public"` // @gotags: `class:"public"` + // The time this key version destruction job was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,40,opt,name=created_time,proto3" json:"created_time,omitempty" class:"public"` // @gotags: `class:"public"` + // The number of rows that have been successfully re-encrypted with a new key version. + // All rows must be re-encrypted before the key version can be destroyed. + CompletedCount int64 `protobuf:"varint,50,opt,name=completed_count,json=completedCount,proto3" json:"completed_count,omitempty" class:"public"` // @gotags: `class:"public"` + // The total number of rows that need re-encrypting. + TotalCount int64 `protobuf:"varint,60,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *KeyVersionDestructionJob) Reset() { + *x = KeyVersionDestructionJob{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_scopes_v1_scope_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyVersionDestructionJob) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyVersionDestructionJob) ProtoMessage() {} + +func (x *KeyVersionDestructionJob) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_scopes_v1_scope_proto_msgTypes[4] + 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 KeyVersionDestructionJob.ProtoReflect.Descriptor instead. +func (*KeyVersionDestructionJob) Descriptor() ([]byte, []int) { + return file_controller_api_resources_scopes_v1_scope_proto_rawDescGZIP(), []int{4} +} + +func (x *KeyVersionDestructionJob) GetKeyVersionId() string { + if x != nil { + return x.KeyVersionId + } + return "" +} + +func (x *KeyVersionDestructionJob) GetScope() *ScopeInfo { + if x != nil { + return x.Scope + } + return nil +} + +func (x *KeyVersionDestructionJob) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *KeyVersionDestructionJob) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *KeyVersionDestructionJob) GetCompletedCount() int64 { + if x != nil { + return x.CompletedCount + } + return 0 +} + +func (x *KeyVersionDestructionJob) GetTotalCount() int64 { + if x != nil { + return x.TotalCount + } + return 0 +} + var File_controller_api_resources_scopes_v1_scope_proto protoreflect.FileDescriptor var file_controller_api_resources_scopes_v1_scope_proto_rawDesc = []byte{ @@ -341,12 +597,56 @@ var file_controller_api_resources_scopes_v1_scope_proto_rawDesc = []byte{ 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x4e, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3b, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x76, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, + 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x94, 0x02, + 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x75, + 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x75, 0x72, + 0x70, 0x6f, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x32, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x4a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x3c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa7, 0x02, 0x0a, 0x18, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, + 0x62, 0x12, 0x24, 0x0a, 0x0e, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x32, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, + 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x3c, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x4e, + 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -361,29 +661,38 @@ func file_controller_api_resources_scopes_v1_scope_proto_rawDescGZIP() []byte { return file_controller_api_resources_scopes_v1_scope_proto_rawDescData } -var file_controller_api_resources_scopes_v1_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_api_resources_scopes_v1_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_api_resources_scopes_v1_scope_proto_goTypes = []interface{}{ - (*ScopeInfo)(nil), // 0: controller.api.resources.scopes.v1.ScopeInfo - (*Scope)(nil), // 1: controller.api.resources.scopes.v1.Scope - nil, // 2: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry - (*wrapperspb.StringValue)(nil), // 3: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*structpb.ListValue)(nil), // 5: google.protobuf.ListValue + (*ScopeInfo)(nil), // 0: controller.api.resources.scopes.v1.ScopeInfo + (*Scope)(nil), // 1: controller.api.resources.scopes.v1.Scope + (*KeyVersion)(nil), // 2: controller.api.resources.scopes.v1.KeyVersion + (*Key)(nil), // 3: controller.api.resources.scopes.v1.Key + (*KeyVersionDestructionJob)(nil), // 4: controller.api.resources.scopes.v1.KeyVersionDestructionJob + nil, // 5: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry + (*wrapperspb.StringValue)(nil), // 6: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*structpb.ListValue)(nil), // 8: google.protobuf.ListValue } var file_controller_api_resources_scopes_v1_scope_proto_depIdxs = []int32{ - 0, // 0: controller.api.resources.scopes.v1.Scope.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.scopes.v1.Scope.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.scopes.v1.Scope.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.scopes.v1.Scope.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.scopes.v1.Scope.updated_time:type_name -> google.protobuf.Timestamp - 3, // 5: controller.api.resources.scopes.v1.Scope.primary_auth_method_id:type_name -> google.protobuf.StringValue - 2, // 6: controller.api.resources.scopes.v1.Scope.authorized_collection_actions:type_name -> controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry - 5, // 7: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue - 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 + 0, // 0: controller.api.resources.scopes.v1.Scope.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 6, // 1: controller.api.resources.scopes.v1.Scope.name:type_name -> google.protobuf.StringValue + 6, // 2: controller.api.resources.scopes.v1.Scope.description:type_name -> google.protobuf.StringValue + 7, // 3: controller.api.resources.scopes.v1.Scope.created_time:type_name -> google.protobuf.Timestamp + 7, // 4: controller.api.resources.scopes.v1.Scope.updated_time:type_name -> google.protobuf.Timestamp + 6, // 5: controller.api.resources.scopes.v1.Scope.primary_auth_method_id:type_name -> google.protobuf.StringValue + 5, // 6: controller.api.resources.scopes.v1.Scope.authorized_collection_actions:type_name -> controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry + 7, // 7: controller.api.resources.scopes.v1.KeyVersion.created_time:type_name -> google.protobuf.Timestamp + 0, // 8: controller.api.resources.scopes.v1.Key.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 7, // 9: controller.api.resources.scopes.v1.Key.created_time:type_name -> google.protobuf.Timestamp + 2, // 10: controller.api.resources.scopes.v1.Key.versions:type_name -> controller.api.resources.scopes.v1.KeyVersion + 0, // 11: controller.api.resources.scopes.v1.KeyVersionDestructionJob.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 7, // 12: controller.api.resources.scopes.v1.KeyVersionDestructionJob.created_time:type_name -> google.protobuf.Timestamp + 8, // 13: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_controller_api_resources_scopes_v1_scope_proto_init() } @@ -416,6 +725,42 @@ func file_controller_api_resources_scopes_v1_scope_proto_init() { return nil } } + file_controller_api_resources_scopes_v1_scope_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyVersion); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_scopes_v1_scope_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Key); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_scopes_v1_scope_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyVersionDestructionJob); 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{ @@ -423,7 +768,7 @@ func file_controller_api_resources_scopes_v1_scope_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_scopes_v1_scope_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/testing/internal/e2e/tests/static/key_destruction_test.go b/testing/internal/e2e/tests/static/key_destruction_test.go new file mode 100644 index 000000000..36441ba00 --- /dev/null +++ b/testing/internal/e2e/tests/static/key_destruction_test.go @@ -0,0 +1,350 @@ +package static_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestKeyDestructionCli uses the boundary CLI to test key destruction. +func TestKeyDestructionCli(t *testing.T) { + e2e.MaybeSkipTest(t) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "create", + "-name", "testscope", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var scopeReply scopes.ScopeCreateResult + require.NoError(t, json.Unmarshal(output.Stdout, &scopeReply)) + t.Cleanup(func() { + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "delete", + "-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "list-keys", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var keys scopes.KeyListResult + err := json.Unmarshal(output.Stdout, &keys) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + for _, key := range keys.Items { + assert.Len(t, key.Versions, 1) + } + + // Create OIDC auth method to create some encrypted data in the scope + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "create", "oidc", + "-scope-id", scopeReply.Item.Id, + "-api-url-prefix", "http://example.com", + "-client-id", "something", + "-client-secret", "somethingelse", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var amReply authmethods.AuthMethodCreateResult + require.NoError(t, json.Unmarshal(output.Stdout, &amReply)) + t.Cleanup(func() { + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "delete", + "-id", amReply.Item.Id, + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "rotate-keys", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "list-keys", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + err = json.Unmarshal(output.Stdout, &keys) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + var rootKeyVersionToDestroy *scopes.KeyVersion + var databaseKeyVersionToDestroy *scopes.KeyVersion + for _, key := range keys.Items { + // Each key should have a new version after rotation + assert.Len(t, key.Versions, 2) + if key.Purpose == "rootKey" { + rootKeyVersionToDestroy = key.Versions[1] // Key versions are ordered by version, descending + } + if key.Purpose == "database" { + databaseKeyVersionToDestroy = key.Versions[1] + } + } + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "list-key-version-destruction-jobs", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var jobs scopes.KeyVersionDestructionJobListResult + err = json.Unmarshal(output.Stdout, &jobs) + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "destroy-key-version", + "-scope-id", scopeReply.Item.Id, + "-key-version-id", rootKeyVersionToDestroy.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var destruction struct { + Item *scopes.KeyVersionDestructionResult + } + err = json.Unmarshal(output.Stdout, &destruction) + require.NoError(t, err) + // Root key versions should be destroyed synchronously, as they don't require any re-encryption + assert.Equal(t, "completed", destruction.Item.State) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "list-key-version-destruction-jobs", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + err = json.Unmarshal(output.Stdout, &jobs) + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "destroy-key-version", + "-scope-id", scopeReply.Item.Id, + "-key-version-id", databaseKeyVersionToDestroy.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + err = json.Unmarshal(output.Stdout, &destruction) + require.NoError(t, err) + // Most data key versions should be destroyed asynchronously, as they require re-encryption + // of existing data. + assert.Equal(t, "pending", destruction.Item.State) + + // The default scheduler job run interval is 1 minute. The runs become available 1 + // second after the last successful run. We need to re-encrypt data in 1 table, + // and then remove the key. This job should take between 1 and 2 minutes to run, + // depending on the timing of the first started run. + cancelCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) + t.Cleanup(cancel) + for { + output = e2e.RunCommand(cancelCtx, "boundary", + e2e.WithArgs( + "scopes", "list-key-version-destruction-jobs", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + err = json.Unmarshal(output.Stdout, &jobs) + require.NoError(t, err) + if len(jobs.Items) == 0 { + break + } + require.Len(t, jobs.Items, 1) + assert.Equal(t, databaseKeyVersionToDestroy.Id, jobs.Items[0].KeyVersionId) + select { + case <-time.After(5 * time.Second): + case <-cancelCtx.Done(): + t.Fatal("Test timed out waiting for destruction to finish") + } + } + + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "scopes", "list-keys", + "-scope-id", scopeReply.Item.Id, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + err = json.Unmarshal(output.Stdout, &keys) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + for _, key := range keys.Items { + switch key.Purpose { + case "rootKey", "database": + assert.Len(t, key.Versions, 1) + default: + assert.Len(t, key.Versions, 2) + } + } + + t.Logf("Successfully destroyed a root key and data key") +} + +// TestKeyDestructionApi uses the boundary CLI to test key destruction. +func TestKeyDestructionApi(t *testing.T) { + e2e.MaybeSkipTest(t) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + + ctx := context.Background() + sc := scopes.NewClient(client) + + scope, err := sc.Create(ctx, "global", scopes.WithName("testscope")) + require.NoError(t, err) + t.Cleanup(func() { + _, err := sc.Delete(context.Background(), scope.Item.Id) + require.NoError(t, err) + }) + + keys, err := sc.ListKeys(ctx, scope.Item.Id) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + // Assert that all keys have the same number of versions + for _, key := range keys.Items { + assert.Len(t, key.Versions, 1) + } + + // Create OIDC auth method to create some encrypted data in the scope + amc := authmethods.NewClient(client) + am, err := amc.Create(ctx, "oidc", scope.Item.Id, + authmethods.WithOidcAuthMethodApiUrlPrefix("http://example.com"), + authmethods.WithOidcAuthMethodClientId("something"), + authmethods.WithOidcAuthMethodClientSecret("somethingelse"), + ) + require.NoError(t, err) + t.Cleanup(func() { + _, err := amc.Delete(context.Background(), am.Item.Id) + require.NoError(t, err) + }) + + _, err = sc.RotateKeys(ctx, scope.Item.Id, false) + require.NoError(t, err) + + keys, err = sc.ListKeys(ctx, scope.Item.Id) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + // Assert that all keys have the same number of versions, + // one higher than before + var rootKeyVersionToDestroy *scopes.KeyVersion + var databaseKeyVersionToDestroy *scopes.KeyVersion + for _, key := range keys.Items { + // Each key should have a new version after rotation + assert.Len(t, key.Versions, 2) + if key.Purpose == "rootKey" { + // Key versions are ordered by version, descending. + // Pick the second newest version. + rootKeyVersionToDestroy = key.Versions[1] + } + if key.Purpose == "database" { + databaseKeyVersionToDestroy = key.Versions[1] + } + } + + jobs, err := sc.ListKeyVersionDestructionJobs(ctx, scope.Item.Id) + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + result, err := sc.DestroyKeyVersion(ctx, scope.Item.Id, rootKeyVersionToDestroy.Id) + require.NoError(t, err) + assert.Equal(t, "completed", result.State) + + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, scope.Item.Id) + require.NoError(t, err) + assert.Len(t, jobs.Items, 0) + + result, err = sc.DestroyKeyVersion(ctx, scope.Item.Id, databaseKeyVersionToDestroy.Id) + require.NoError(t, err) + assert.Equal(t, "pending", result.State) + + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, scope.Item.Id) + require.NoError(t, err) + assert.Len(t, jobs.Items, 1) + + // The default scheduler job run interval is 1 minute. The runs become available 1 + // second after the last successful run. We need to re-encrypt data in 1 table, + // and then remove the key. This job should take between 1 and 2 minutes to run, + // depending on the timing of the first started run. + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + t.Cleanup(cancel) + for { + jobs, err = sc.ListKeyVersionDestructionJobs(ctx, scope.Item.Id) + require.NoError(t, err) + if len(jobs.Items) == 0 { + break + } + require.Len(t, jobs.Items, 1) + assert.Equal(t, databaseKeyVersionToDestroy.Id, jobs.Items[0].KeyVersionId) + select { + case <-time.After(5 * time.Second): + case <-ctx.Done(): + t.Fatal("Test timed out waiting for destruction to finish") + } + } + + keys, err = sc.ListKeys(ctx, scope.Item.Id) + require.NoError(t, err) + // We expect to see all DEKs and the root key, so DEKs+1 in total. + assert.Len(t, keys.Items, len(kms.ValidDekPurposes())+1, "Expected %d keys to be listed, but found %d. Keys: %v", len(kms.ValidDekPurposes())+1, len(keys.Items), keys.Items) + for _, key := range keys.Items { + switch key.Purpose { + case "rootKey", "database": + assert.Len(t, key.Versions, 1) + default: + assert.Len(t, key.Versions, 2) + } + } + + t.Logf("Successfully destroyed a root key and data key") +}