Key Rotation/Destruction (#2477) (#2607)

* Key Rotation/Destruction (#2477)

* fix: Correct kms.CreateKeys comment

This method does not return anything.

* feat(kms): Add new ListKeys method

The new ListKeys method wraps the underlying KMS wrapper,
returning all the keys in the scope specified.

* feat(scopes): Add new scope actions for key management

Adds list-keys, rotate-keys and revoke-keys actions for scopes.

* feat(scopes): Add ListKeys API endpoint

This new endpoint lists all keys in a scope

* feat(api): Add ListKeys to Scopes client

This has to be a custom function because it is a custom action
and doesn't map well to any of the existing templates, or
generalises well in a way that would make it reasonable
to create a new template.

* feat(cli): Add new scopes list-keys command

* add RotateKeys function to the kms repository

* Add Rotate Keys Endpoint to Scopes Api (#2360)

* add rotate keys endpoint

* add tests for RotateKeys endpoint

* remove that one comment I forgot about

* addressing more PR comments

* Add Rotate Keys CLI Command (#2395)

* add cli command and client

* add bats tests

* fix some info text and pr comments

* write a new rotate keys description

* Add Missing DEK Foreign Keys (#2408)

* add missing fks

* mark key as nullable in gorm

* update tests to use a new wrapper with a real key_id

* fix formatting in test and add keys for auth_token test

* replace key values

* revert postgres_40_01_test.go

* style: reformat sql for readability

* refactor: change type of key_id columns to kms_private_id

* Store key_id and scope_id with oplog entries, re-type columns referencing data key version (#2431)

* fix(db): Correct types of data key version referencers

* fix(oplog): Fix missing error handling

* feat(oplog): Store key_id and scope_id with oplog entries

This migration truncates all existing oplog entries, as
migrating existing data would have been complex and
likely unnecessary.

* Update view references

* Always delete oplog entries when dropping keys

* Fix rebase issues

* Fix sql tests

* Add schema for new kms destruction jobs (#2479)

* feat: Add schema for new kms destruction jobs

The schema lets us manage destruction jobs per-table
while providing a guarantee that only one run is running
at a time.

* Apply suggestions from code review

Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>

* Fix table reference and tests

Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>

* update domain, api, and cli to all include key versions in outputs (#2472)

* update domain, api, and cli to all include key versions in outputs

* fix go sum

* add migration to fix immutable error

* fix final test issue as well as make gen issue

* rebase and fix

* address johan pr comments

* make gen, didn't realize comments were included

* update migrations based on new migration 55 file 03

* Move migrations to 56

* feat: Add key version rewrapping function registry (#2478)

The new RegisterTableRewrapFn can be used by a domain
to register a callback to use when rewrapping data in a
specific table name.

* feat(kms): Add scopeId to RewrapFn (#2539)

This makes it much easier for rewrap functions to look up
the correct wrapper and limit the number of rows searched.

* Update Root Cert Proto Definition (#2542)

* update root cert proto definition to properly identify public_key as the table primary key

* add the test fix as well

* Rewrap Functions for Encrypted Tables (#2532)

* add oidc rewrap

* add argon2 config rewrap

* add auth token rewrapping

* add worker auth cert rewrapping

* check the err

* add username password credential rewrapping

* add ssh private key credential rewrapping

* add vault client certificate rewrap function

* add host catalog secret rewrap function

* cleanup a little

* add host vault token rewrap function

* add session credential rewrap function

* add session rewrap function

* add worker auth rewrap function

* update test to include full assert gamut

* add session rewrap function TEST

* bring all tests up to current standards, make more readable, and include full assert gamut

* rework all rewrap functions to utilize scope id

* update comments, queries, sql refs, etc, in response to PR comments

* forgot a comment

* remove hmac updates and fix a couple comments again

* Rewrapping Registered Tables Test (#2555)

* add the registered table test as well as the two missing rewrap functions

* address pr comments, add db r/w conversions, cmp.diff, sql comment, remove (now) faulty immutable test

* Add ability to list key version destruction jobs, recurring jobs and destroying key versions (#2498)

* feat(kms): Add ability to list data key version destruction jobs

* feat(scopes): Add ListKeyVersionDestructionJobs

* feat(cli): Add list-key-version-destruction-jobs

* feat(kms): Add recurring job that performs table rewrapping

The new recurring job runs for each job table with a registered
rewrapping callback. It attempts to become the running run and
start its rewrapping, gracefully handling sudden interruptions
by resuming it's work if it finds evidence of sudden interruption.

* feat(kms): Add recurring job for destroying key versions

The new job monitors the database for data key version destruction
jobs that have finished rewrapping all its data, performing
the final data key version revocation.

* feat(kms): Add DestroyKeyVersion

DestroyKeyVersion immediately destroys a key version
if it is a root key version or a data key version
which encrypts no data. If the data key version
encrypts data which needs to be rewrapped, it
queues an asynchronous job to complete the rewrapping
operation.

* feat(scopes): Add DestroyKeyVersion API endpoint

* feat(cli): Add scopes destroy-key-version CLI command

* Ensure all secret updates include key ID (#2556)

* feat(all): Ensure all secret updates include key ID

When updating a secret, it is paramount that the key ID
is also updated, as it is the only means we have of ensuring
that we only destroy keys once there is no more data
associated with the key.

* Remove fix to session repository for now

This breaks the current private key derivation method

* add new encrypt/decrypt funcs and made necessary proto changes (#2557)

* add new encrypt/decrypt funcs and made necessary proto changes

* address all PR comments

* fix test panic

* add param checking to rewraps

* fix(schema): Correctly organize migrations again

* Review comments part 1

* Review comments part 2

* Review comments part 3

* Store cert private key, encrypt tofu token (#2583)

* fix(session): store cert private key, encrypt tofu token

Previously, we would derive a session certificate private key from
the session key used in each project, and not store the key. We
would also encrypt the tofu token with the database key but not
store a reference to this key in the database.

This change fixes this by replacing our key derivation with a key
generation step, and instead store the generated key, encrypted,
in the database. We also ensure that any new tofu tokens are
encrypted with the same key.

To handle existing sessions, we lazily rewrite the sessions whenever
a user lists them. There is a minor risk that a user could end up
destroying the database key that encrypts the tofu token before
that session has been rewrapped, but this is going to be rare and
temporary.

* feat(session): Allow the random source for secrets to be configured

* Review comments

* Minor fixes

* More minor fixes

* More small fixes

Co-authored-by: Danielle Miu <dani.miu@hashicorp.com>
Co-authored-by: Danielle <29378233+DanielleMiu@users.noreply.github.com>
Co-authored-by: Michael Gaffney <mike@gaffney.cc>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>

* fix(session): Unset session key ID when scope is deleted

Unsetting the key ID allows the session to live beyond the lifetime of
the scope, while allowing the scope to cascade delete its keys.

* fix(migrations): Rewrite migration 56 tests

These tests can no longer use the normal test helpers
as they try to use the new oplog table structure, so we
have to manually write all the interactions.

* fix(migrations): Rename migration 58 file name

This was the name used in the release branch, update
it to the new migration number.

* fix(e2e): Add some comments and debug to kms tests

* fix(session): Handle empty project and user IDs in read

When a project or user is deleted, the session is automatically
canceled and the respective field on the session is unset.
LookupSession did not handle this case gracefully, and would
error on decryption in this state. Skip decrypting sessions that
do not have either a user or project to avoid this error. The
decrypted values are not used for a canceled session.

* Fix worker test

* Increase test timeout

Co-authored-by: Danielle Miu <dani.miu@hashicorp.com>
Co-authored-by: Danielle <29378233+DanielleMiu@users.noreply.github.com>
Co-authored-by: Michael Gaffney <mike@gaffney.cc>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2022-11-16 18:17:28 -05:00
1 parent 0c51f52222
commit edd323b73a
158 files changed
+12026 -991

No files matched your search

+5
View File
@@ -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
+214
View File
@@ -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
}
+15
View File
@@ -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"`
}
+12
View File
@@ -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"`
}
@@ -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"`
}
+5
View File
@@ -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"
)
+8 -8
View File
@@ -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
+16 -14
View File
@@ -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=
+22
View File
@@ -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",
@@ -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)
+55
View File
@@ -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, <other>), 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
}
+121
View File
@@ -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)
})
}
+1 -1
View File
@@ -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)
+7 -2
View File
@@ -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
@@ -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{},
@@ -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)))
}
+55
View File
@@ -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
}
+84
View File
@@ -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())
})
}
+5 -3
View File
@@ -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())
+55
View File
@@ -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
}
+67
View File
@@ -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())
})
}
+20
View File
@@ -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{
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
+2 -2
View File
@@ -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,
+40
View File
@@ -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 = ?;
`
)
@@ -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)
@@ -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-----
+171
View File
@@ -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
}
@@ -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())
})
}
+92
View File
@@ -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
}
@@ -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())
})
}
@@ -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
}
@@ -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"),
),
)
})
+7
View File
@@ -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
}
@@ -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")
}
+1 -1
View File
@@ -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)
}
@@ -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
}
File diff suppressed because it is too large. Load diff
@@ -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
@@ -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(),
@@ -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")
}
+8
View File
@@ -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()
}
+1 -1
View File
@@ -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)
+35 -29
View File
@@ -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},
@@ -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
@@ -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');
@@ -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');
@@ -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');
@@ -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');
@@ -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,
@@ -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,
@@ -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
@@ -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');
@@ -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');
@@ -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 (
@@ -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;
commit;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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
@@ -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)
@@ -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,
@@ -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())
@@ -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.
}
+2 -1
View File
@@ -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
@@ -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)
@@ -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;
@@ -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
@@ -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;
+21 -1
View File
@@ -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) {
+2 -1
View File
@@ -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()},
+247
View File
@@ -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": {
@@ -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,
},
@@ -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
)
@@ -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",
+55
View File
@@ -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
}
+81
View File
@@ -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())
})
}
@@ -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),
}
}
@@ -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),
}
}
@@ -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()))
}
@@ -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),
}
}
@@ -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),
}
}
@@ -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()))
}
@@ -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()))
}
@@ -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()))
}
@@ -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"
}
@@ -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)
}
+39
View File
@@ -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
}
+27
View File
@@ -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)
}
+66
View File
@@ -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"
}
@@ -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)
}
+356 -1
View File
@@ -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"
+772
View File
@@ -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
}
+7
View File
@@ -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)
})
}
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more