diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec95ec34..60868afde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. sessions after 1 hour. See Deprecations/Changes for some additional details. [PR](https://github.com/hashicorp/boundary/pull/2160). +* event filtering: Change event filters to use lowercase and snake case for data + elements like the rest of Boundary filters do. ### Bug Fixes diff --git a/Makefile b/Makefile index 266328b8c..2488dbdb5 100644 --- a/Makefile +++ b/Makefile @@ -148,6 +148,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/servers/store/controller.pb.go + @protoc-go-inject-tag -input=./internal/servers/store/worker.pb.go + @protoc-go-inject-tag -input=./internal/servers/store/root_certificate.pb.go + @protoc-go-inject-tag -input=./internal/servers/store/worker_auth.pb.go @protoc-go-inject-tag -input=./internal/target/store/target.pb.go @protoc-go-inject-tag -input=./internal/target/targettest/store/target.pb.go @protoc-go-inject-tag -input=./internal/target/tcp/store/target.pb.go @@ -193,6 +197,10 @@ protobuild: @protoc-go-inject-tag -input=./internal/gen/controller/api/services/session_service.pb.go @protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/users/user.pb.go @protoc-go-inject-tag -input=./internal/gen/controller/api/services/user_service.pb.go + @protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/workers/worker.pb.go + @protoc-go-inject-tag -input=./internal/gen/controller/api/services/worker_service.pb.go + @protoc-go-inject-tag -input=./internal/gen/controller/servers/services/server_coordination_service.pb.go + # these protos, services and openapi artifacts are purely for testing purposes @protoc-go-inject-tag -input=./internal/gen/testing/event/event.pb.go diff --git a/api/sessions/session.gen.go b/api/sessions/session.gen.go index b8f91ef0d..301ff994f 100644 --- a/api/sessions/session.gen.go +++ b/api/sessions/session.gen.go @@ -28,7 +28,6 @@ type Session struct { Endpoint string `json:"endpoint,omitempty"` States []*SessionState `json:"states,omitempty"` Status string `json:"status,omitempty"` - WorkerInfo []*WorkerInfo `json:"worker_info,omitempty"` Certificate []byte `json:"certificate,omitempty"` TerminationReason string `json:"termination_reason,omitempty"` AuthorizedActions []string `json:"authorized_actions,omitempty"` diff --git a/api/sessions/workers.gen.go b/api/sessions/workers.gen.go deleted file mode 100644 index 113654da8..000000000 --- a/api/sessions/workers.gen.go +++ /dev/null @@ -1,6 +0,0 @@ -// Code generated by "make api"; DO NOT EDIT. -package sessions - -type WorkerInfo struct { - Address string `json:"address,omitempty"` -} diff --git a/api/util.go b/api/util.go index 4dd69368c..67669db03 100644 --- a/api/util.go +++ b/api/util.go @@ -1,13 +1,5 @@ package api -import ( - "github.com/fatih/structs" -) - -func init() { - structs.DefaultTagName = "json" -} - type GenericResult interface { GetItem() interface{} GetResponse() *Response diff --git a/api/workers/option.gen.go b/api/workers/option.gen.go new file mode 100644 index 000000000..ca1fb0aa4 --- /dev/null +++ b/api/workers/option.gen.go @@ -0,0 +1,112 @@ +package workers + +import ( + "strconv" + "strings" + + "github.com/hashicorp/boundary/api" +) + +// Option is a func that sets optional attributes for a call. This does not need +// to be used directly, but instead option arguments are built from the +// functions in this package. WithX options set a value to that given in the +// argument; DefaultX options indicate that the value should be set to its +// default. When an API call is made options are processed in ther order they +// appear in the function call, so for a given argument X, a succession of WithX +// or DefaultX calls will result in the last call taking effect. +type Option func(*options) + +type options struct { + postMap map[string]interface{} + queryMap map[string]string + withAutomaticVersioning bool + withSkipCurlOutput bool + withFilter string + withRecursive bool +} + +func getDefaultOptions() options { + return options{ + postMap: make(map[string]interface{}), + queryMap: make(map[string]string), + } +} + +func getOpts(opt ...Option) (options, []api.Option) { + opts := getDefaultOptions() + for _, o := range opt { + if o != nil { + o(&opts) + } + } + var apiOpts []api.Option + if opts.withSkipCurlOutput { + apiOpts = append(apiOpts, api.WithSkipCurlOutput(true)) + } + if opts.withFilter != "" { + opts.queryMap["filter"] = opts.withFilter + } + if opts.withRecursive { + opts.queryMap["recursive"] = strconv.FormatBool(opts.withRecursive) + } + return opts, apiOpts +} + +// If set, and if the version is zero during an update, the API will perform a +// fetch to get the current version of the resource and populate it during the +// update call. This is convenient but opens up the possibility for subtle +// order-of-modification issues, so use carefully. +func WithAutomaticVersioning(enable bool) Option { + return func(o *options) { + o.withAutomaticVersioning = enable + } +} + +// WithSkipCurlOutput tells the API to not use the current call for cURL output. +// Useful for when we need to look up versions. +func WithSkipCurlOutput(skip bool) Option { + return func(o *options) { + o.withSkipCurlOutput = true + } +} + +// WithFilter tells the API to filter the items returned using the provided +// filter term. The filter should be in a format supported by +// hashicorp/go-bexpr. +func WithFilter(filter string) Option { + return func(o *options) { + o.withFilter = strings.TrimSpace(filter) + } +} + +// WithRecursive tells the API to use recursion for listing operations on this +// resource +func WithRecursive(recurse bool) Option { + return func(o *options) { + o.withRecursive = true + } +} + +func WithDescription(inDescription string) Option { + return func(o *options) { + o.postMap["description"] = inDescription + } +} + +func DefaultDescription() Option { + return func(o *options) { + o.postMap["description"] = nil + } +} + +func WithName(inName string) Option { + return func(o *options) { + o.postMap["name"] = inName + } +} + +func DefaultName() Option { + return func(o *options) { + o.postMap["name"] = nil + } +} diff --git a/api/workers/worker.gen.go b/api/workers/worker.gen.go new file mode 100644 index 000000000..1a021c000 --- /dev/null +++ b/api/workers/worker.gen.go @@ -0,0 +1,330 @@ +// Code generated by "make api"; DO NOT EDIT. +package workers + +import ( + "context" + "errors" + "fmt" + "net/url" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" +) + +type Worker struct { + Id string `json:"id,omitempty"` + ScopeId string `json:"scope_id,omitempty"` + Scope *scopes.ScopeInfo `json:"scope,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CreatedTime time.Time `json:"created_time,omitempty"` + UpdatedTime time.Time `json:"updated_time,omitempty"` + Version uint32 `json:"version,omitempty"` + Address string `json:"address,omitempty"` + CanonicalTags map[string][]string `json:"canonical_tags,omitempty"` + ConfigTags map[string][]string `json:"config_tags,omitempty"` + LastStatusTime time.Time `json:"last_status_time,omitempty"` + WorkerGeneratedAuthToken string `json:"worker_generated_auth_token,omitempty"` + ActiveConnectionCount uint32 `json:"active_connection_count,omitempty"` + Type string `json:"type,omitempty"` + AuthorizedActions []string `json:"authorized_actions,omitempty"` + + response *api.Response +} + +type WorkerReadResult struct { + Item *Worker + response *api.Response +} + +func (n WorkerReadResult) GetItem() interface{} { + return n.Item +} + +func (n WorkerReadResult) GetResponse() *api.Response { + return n.response +} + +type WorkerCreateResult = WorkerReadResult +type WorkerUpdateResult = WorkerReadResult + +type WorkerDeleteResult struct { + response *api.Response +} + +// GetItem will always be nil for WorkerDeleteResult +func (n WorkerDeleteResult) GetItem() interface{} { + return nil +} + +func (n WorkerDeleteResult) GetResponse() *api.Response { + return n.response +} + +type WorkerListResult struct { + Items []*Worker + response *api.Response +} + +func (n WorkerListResult) GetItems() interface{} { + return n.Items +} + +func (n WorkerListResult) GetResponse() *api.Response { + return n.response +} + +// Client is a client for this collection +type Client struct { + client *api.Client +} + +// Creates a new client for this collection. The submitted API client is cloned; +// modifications to it after generating this client will not have effect. If you +// need to make changes to the underlying API client, use ApiClient() to access +// it. +func NewClient(c *api.Client) *Client { + return &Client{client: c.Clone()} +} + +// ApiClient returns the underlying API client +func (c *Client) ApiClient() *api.Client { + return c.client +} + +func (c *Client) CreateWorkerLed(ctx context.Context, workerGeneratedAuthToken string, scopeId string, opt ...Option) (*WorkerCreateResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into CreateWorkerLed request") + } + + opts, apiOpts := getOpts(opt...) + + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + if workerGeneratedAuthToken == "" { + return nil, fmt.Errorf("empty workerGeneratedAuthToken value passed into CreateWorkerLed request") + } else { + opts.postMap["worker_generated_auth_token"] = workerGeneratedAuthToken + } + + opts.postMap["scope_id"] = scopeId + + req, err := c.client.NewRequest(ctx, "POST", "workers:create:worker-led", opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating CreateWorkerLed 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 CreateWorkerLed call: %w", err) + } + + target := new(WorkerCreateResult) + target.Item = new(Worker) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding CreateWorkerLed response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Read(ctx context.Context, id string, opt ...Option) (*WorkerReadResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Read request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("workers/%s", url.PathEscape(id)), nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Read 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, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error performing client request during Read call: %w", err) + } + + target := new(WorkerReadResult) + target.Item = new(Worker) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding Read response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Update(ctx context.Context, id string, version uint32, opt ...Option) (*WorkerUpdateResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Update request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + if version == 0 { + if !opts.withAutomaticVersioning { + return nil, errors.New("zero version number passed into Update request and automatic versioning not specified") + } + existingTarget, existingErr := c.Read(ctx, id, append([]Option{WithSkipCurlOutput(true)}, opt...)...) + if existingErr != nil { + if api.AsServerError(existingErr) != nil { + return nil, fmt.Errorf("error from controller when performing initial check-and-set read: %w", existingErr) + } + return nil, fmt.Errorf("error performing initial check-and-set read: %w", existingErr) + } + if existingTarget == nil { + return nil, errors.New("nil resource response found when performing initial check-and-set read") + } + if existingTarget.Item == nil { + return nil, errors.New("nil resource found when performing initial check-and-set read") + } + version = existingTarget.Item.Version + } + + opts.postMap["version"] = version + + req, err := c.client.NewRequest(ctx, "PATCH", fmt.Sprintf("workers/%s", url.PathEscape(id)), opts.postMap, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Update 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 Update call: %w", err) + } + + target := new(WorkerUpdateResult) + target.Item = new(Worker) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding Update response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} + +func (c *Client) Delete(ctx context.Context, id string, opt ...Option) (*WorkerDeleteResult, error) { + if id == "" { + return nil, fmt.Errorf("empty id value passed into Delete request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("workers/%s", url.PathEscape(id)), nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating Delete 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 Delete call: %w", err) + } + + apiErr, err := resp.Decode(nil) + if err != nil { + return nil, fmt.Errorf("error decoding Delete response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + + target := &WorkerDeleteResult{ + response: resp, + } + return target, nil +} + +func (c *Client) List(ctx context.Context, scopeId string, opt ...Option) (*WorkerListResult, error) { + if scopeId == "" { + return nil, fmt.Errorf("empty scopeId value passed into List request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + opts, apiOpts := getOpts(opt...) + opts.queryMap["scope_id"] = scopeId + + req, err := c.client.NewRequest(ctx, "GET", "workers", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating List 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 List call: %w", err) + } + + target := new(WorkerListResult) + apiErr, err := resp.Decode(target) + if err != nil { + return nil, fmt.Errorf("error decoding List response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + target.response = resp + return target, nil +} diff --git a/globals/fields.go b/globals/fields.go index 84c687501..f8577afdc 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -1,74 +1,86 @@ package globals const ( - IdField = "id" - VersionField = "version" - NameField = "name" - DescriptionField = "description" - CreatedTimeField = "created_time" - UpdatedTimeField = "updated_time" - TypeField = "type" - AttributesField = "attributes" - ScopeIdField = "scope_id" - ScopeField = "scope" - AuthMethodIdField = "auth_method_id" - AccountIdField = "account_id" - UserIdField = "user_id" - IsPrimaryField = "is_primary" - AuthorizedActionsField = "authorized_actions" - AuthorizedCollectionActionsField = "authorized_collection_actions" - ExpirationTimeField = "expiration_time" - ApproximateLastUsedTimeField = "approximate_last_used_time" - MembersField = "members" - MemberIdsField = "member_ids" - HostCatalogIdField = "host_catalog_id" - HostSetIdsField = "host_set_ids" - HostSourceIdsField = "host_source_ids" - HostIdsField = "host_ids" - PrincipalIdsField = "principal_ids" - PrincipalsField = "principals" - GrantScopeIdField = "grant_scope_id" - GrantsField = "grants" - GrantStringsField = "grant_strings" - PrimaryAuthMethodIdField = "primary_auth_method_id" - TargetIdField = "target_id" - HostIdField = "host_id" - HostSetIdField = "host_set_id" - HostSetsField = "host_sets" - HostSourcesField = "host_sources" - AuthTokenIdField = "auth_token_id" - EndpointField = "endpoint" - CertificateField = "certificate" - TerminationReasonField = "termination_reason" - StatusField = "status" - StatesField = "states" - SessionConnectionLimitField = "session_connection_limit" - SessionMaxSecondsField = "session_max_seconds" - WorkerFilterField = "worker_filter" - AccountIdsField = "account_ids" - AccountsField = "accounts" - LoginNameField = "login_name" - FullNameField = "full_name" - PrimaryAccountIdField = "primary_account_id" - EmailField = "email" - ManagedGroupIdsField = "managed_group_ids" - FilterField = "filter" - CredentialStoreIdField = "credential_store_id" - ApplicationCredentialSourceIdsField = "application_credential_source_ids" - ApplicationCredentialSourcesField = "application_credential_sources" - PreferredEndpointsField = "preferred_endpoints" - SyncIntervalSecondsField = "sync_interval_seconds" - PluginIdField = "plugin_id" - PluginField = "plugin" - PluginNameField = "plugin_name" - IpAddressesField = "ip_addresses" - DnsNamesField = "dns_names" - SecretsHmacField = "secrets_hmac" - ExternalIdField = "external_id" - EgressCredentialSourceIdsField = "egress_credential_source_ids" - EgressCredentialSourcesField = "egress_credential_sources" - ConnectionsField = "connections" - CredentialTypeField = "credential_type" - CredentialMappingOverridesField = "credential_mapping_overrides" - MetricNamespace = "boundary" + IdField = "id" + VersionField = "version" + NameField = "name" + DescriptionField = "description" + CreatedTimeField = "created_time" + UpdatedTimeField = "updated_time" + TypeField = "type" + AttributesField = "attributes" + ScopeIdField = "scope_id" + ScopeField = "scope" + AuthMethodIdField = "auth_method_id" + AccountIdField = "account_id" + UserIdField = "user_id" + IsPrimaryField = "is_primary" + AuthorizedActionsField = "authorized_actions" + AuthorizedCollectionActionsField = "authorized_collection_actions" + ExpirationTimeField = "expiration_time" + ApproximateLastUsedTimeField = "approximate_last_used_time" + MembersField = "members" + MemberIdsField = "member_ids" + HostCatalogIdField = "host_catalog_id" + HostSetIdsField = "host_set_ids" + HostSourceIdsField = "host_source_ids" + HostIdsField = "host_ids" + PrincipalIdsField = "principal_ids" + PrincipalsField = "principals" + GrantScopeIdField = "grant_scope_id" + GrantsField = "grants" + GrantStringsField = "grant_strings" + PrimaryAuthMethodIdField = "primary_auth_method_id" + TargetIdField = "target_id" + HostIdField = "host_id" + HostSetIdField = "host_set_id" + HostSetsField = "host_sets" + HostSourcesField = "host_sources" + AuthTokenIdField = "auth_token_id" + EndpointField = "endpoint" + CertificateField = "certificate" + TerminationReasonField = "termination_reason" + StatusField = "status" + StatesField = "states" + SessionConnectionLimitField = "session_connection_limit" + SessionMaxSecondsField = "session_max_seconds" + WorkerFilterField = "worker_filter" + AccountIdsField = "account_ids" + AccountsField = "accounts" + LoginNameField = "login_name" + FullNameField = "full_name" + PrimaryAccountIdField = "primary_account_id" + EmailField = "email" + ManagedGroupIdsField = "managed_group_ids" + FilterField = "filter" + CredentialStoreIdField = "credential_store_id" + ApplicationCredentialLibraryIdsField = "application_credential_library_ids" + ApplicationCredentialLibrariesField = "application_credential_libraries" + ApplicationCredentialSourceIdsField = "application_credential_source_ids" + ApplicationCredentialSourcesField = "application_credential_sources" + PreferredEndpointsField = "preferred_endpoints" + SyncIntervalSecondsField = "sync_interval_seconds" + PluginIdField = "plugin_id" + PluginField = "plugin" + PluginNameField = "plugin_name" + IpAddressesField = "ip_addresses" + DnsNamesField = "dns_names" + SecretsHmacField = "secrets_hmac" + ExternalIdField = "external_id" + EgressCredentialSourceIdsField = "egress_credential_source_ids" + EgressCredentialSourcesField = "egress_credential_sources" + ConnectionsField = "connections" + CredentialTypeField = "credential_type" + CredentialMappingOverridesField = "credential_mapping_overrides" + MetricNamespace = "boundary" + LastStatusTimeField = "last_status_time" + AddressField = "address" + CanonicalAddressField = "canonical_address" + TagsField = "tags" + CanonicalTagsField = "canonical_tags" + ConfigTagsField = "config_tags" + ConfigurationField = "configuration" + WorkerGeneratedAuthTokenField = "worker_generated_auth_token" + WorkerProvidedConfigurationField = "worker_provided_configuration" + ActiveConnectionCountField = "active_connection_count" ) diff --git a/globals/kms.go b/globals/kms.go index 1787f7fe3..739d12e7d 100644 --- a/globals/kms.go +++ b/globals/kms.go @@ -1,8 +1,9 @@ package globals const ( - KmsPurposeRoot = "root" - KmsPurposeWorkerAuth = "worker-auth" - KmsPurposeRecovery = "recovery" - KmsPurposeConfig = "config" + KmsPurposeRoot = "root" + KmsPurposeWorkerAuth = "worker-auth" + KmsPurposeWorkerAuthStorage = "worker-auth-storage" + KmsPurposeRecovery = "recovery" + KmsPurposeConfig = "config" ) diff --git a/go.mod b/go.mod index b1b3ffbde..99b0ebca1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ replace github.com/hashicorp/boundary/api => ./api replace github.com/hashicorp/boundary/sdk => ./sdk +//replace github.com/hashicorp/nodeenrollment => ../nodeenrollment + require ( github.com/armon/go-metrics v0.3.9 // indirect github.com/fatih/color v1.13.0 @@ -15,7 +17,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.15.1 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe github.com/golang/protobuf v1.5.2 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0 github.com/hashicorp/boundary/api v0.0.24 @@ -40,7 +42,7 @@ require ( github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 github.com/hashicorp/go-secure-stdlib/password v0.1.1 - github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0 + github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1 github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 github.com/hashicorp/go-sockaddr v1.0.2 @@ -65,11 +67,13 @@ require ( github.com/pires/go-proxyproto v0.6.1 github.com/pkg/errors v0.9.1 github.com/posener/complete v1.2.3 + github.com/prometheus/client_golang v1.12.1 github.com/ryanuber/go-glob v1.0.0 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.2 github.com/zalando/go-keyring v0.2.1 go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/tools v0.1.10 @@ -86,16 +90,15 @@ require ( require github.com/hashicorp/go-dbw v0.0.0-20220412153211-c470aec9369f // this is a branch and should be updated before merging require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220515130442-cac0b5ac133b - github.com/prometheus/client_golang v1.12.1 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + github.com/hashicorp/nodeenrollment v0.1.0 ) require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/AlecAivazis/survey/v2 v2.2.9 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect @@ -170,6 +173,7 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect @@ -187,7 +191,7 @@ require ( 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 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/sqlite v1.2.4 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 3ed7620cd..ec430c6cb 100644 --- a/go.sum +++ b/go.sum @@ -582,8 +582,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -715,8 +715,8 @@ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 h1:Tz6v3Jb2DRnDCfifRSjYKG github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= -github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0 h1:TszfJJ1vJWdvPkPxYivP5TUTkOaXziE1jTKGmfOck94= -github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0/go.mod h1:Io+g8iYs//ESo5x2Gy1rTTYaIHUyCZD7h16K7DiOUI4= +github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1 h1:s47ax4xEIStj1YV7TRz6zS8LlNrTU86aEhL8pfSOG4U= +github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1/go.mod h1:Mh8pP9IH9qnBQSSH3PYtSkGq3+KlIuw5kAS9W8++BLc= github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 h1:SMGUnbpAcat8rIKHkBPjfv81yC46a8eCNZ2hsR2l1EI= github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= @@ -740,6 +740,8 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/nodeenrollment v0.1.0 h1:mhMVbrHM6NTDcpte/mOilP3Fd/HrcctshGs4/82EO6U= +github.com/hashicorp/nodeenrollment v0.1.0/go.mod h1:LIPKi+g0g/vl3xhpbzugCalHSxX1PMeqnatkAsxRgyM= github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= @@ -1178,6 +1180,8 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sethvargo/go-diceware v0.3.0 h1:UVVEfmN/uF50JfWAN7nbY6CiAlp5xeSx+5U0lWKkMCQ= +github.com/sethvargo/go-diceware v0.3.0/go.mod h1:lH5Q/oSPMivseNdhMERAC7Ti5oOPqsaVddU1BcN1CY0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -1229,8 +1233,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1908,8 +1913,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/driver/postgres v1.2.2 h1:Ka9W6feOU+rPM9m007eYLMD4QoZuYGBnQ3Jp0faGSwg= gorm.io/driver/postgres v1.2.2/go.mod h1:Ik3tK+a3FMp8ORZl29v4b3M0RsgXsaeMXh9s9eVMXco= diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 1f387b654..dd7fa32c3 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/sessions" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/users" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers" "google.golang.org/protobuf/proto" ) @@ -35,6 +36,12 @@ type structureInfo struct { fields []fieldInfo } +type requiredParam struct { + Name string + Typ string + PostType string +} + type fieldInfo struct { Name string ProtoName string @@ -88,9 +95,8 @@ type structInfo struct { // useful to avoid collisions nameOverride string - // typeOnCreate indicates that create will be creating a concrete - // implementation of an abstract type and thus a type field is necessary - typeOnCreate bool + // extraRequiredParams allows adding extra required parameters to templates + extraRequiredParams []requiredParam // recursiveListing indicates that the collection supports recursion when // listing @@ -150,7 +156,7 @@ var inputStructs = []*structInfo{ outFile: "scopes/scope.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -186,7 +192,7 @@ var inputStructs = []*structInfo{ outFile: "users/user.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -214,7 +220,7 @@ var inputStructs = []*structInfo{ outFile: "groups/group.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -252,7 +258,7 @@ var inputStructs = []*structInfo{ outFile: "roles/role.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -294,14 +300,20 @@ var inputStructs = []*structInfo{ outFile: "authmethods/authmethods.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, listTemplate, }, - pluralResourceName: "auth-methods", - typeOnCreate: true, + pluralResourceName: "auth-methods", + extraRequiredParams: []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + }, versionEnabled: true, createResponseTypes: true, recursiveListing: true, @@ -322,7 +334,7 @@ var inputStructs = []*structInfo{ outFile: "accounts/account.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -350,7 +362,7 @@ var inputStructs = []*structInfo{ outFile: "managedgroups/managedgroups.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -386,15 +398,21 @@ var inputStructs = []*structInfo{ outFile: "credentialstores/credential_store.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, listTemplate, }, - pluralResourceName: "credential-stores", - parentTypeName: "scope", - typeOnCreate: true, + pluralResourceName: "credential-stores", + parentTypeName: "scope", + extraRequiredParams: []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + }, versionEnabled: true, createResponseTypes: true, recursiveListing: true, @@ -425,7 +443,7 @@ var inputStructs = []*structInfo{ outFile: "credentiallibraries/credential_library.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -456,17 +474,23 @@ var inputStructs = []*structInfo{ outFile: "credentials/credential.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, listTemplate, }, + extraRequiredParams: []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + }, pluralResourceName: "credentials", parentTypeName: "credential-store", versionEnabled: true, createResponseTypes: true, - typeOnCreate: true, }, // Host related resources @@ -475,7 +499,7 @@ var inputStructs = []*structInfo{ outFile: "hostcatalogs/host_catalog.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -496,8 +520,14 @@ var inputStructs = []*structInfo{ SkipDefault: true, }, }, - pluralResourceName: "host-catalogs", - typeOnCreate: true, + pluralResourceName: "host-catalogs", + extraRequiredParams: []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + }, versionEnabled: true, createResponseTypes: true, recursiveListing: true, @@ -507,7 +537,7 @@ var inputStructs = []*structInfo{ outFile: "hosts/host.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -528,7 +558,7 @@ var inputStructs = []*structInfo{ outFile: "hostsets/host_set.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -591,7 +621,7 @@ var inputStructs = []*structInfo{ outFile: "targets/target.gen.go", templates: []*template.Template{ clientTemplate, - createTemplate, + commonCreateTemplate, readTemplate, updateTemplate, deleteTemplate, @@ -639,8 +669,14 @@ var inputStructs = []*structInfo{ FieldType: "[]string", }, }, - versionEnabled: true, - typeOnCreate: true, + versionEnabled: true, + extraRequiredParams: []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + }, createResponseTypes: true, recursiveListing: true, }, @@ -648,10 +684,6 @@ var inputStructs = []*structInfo{ inProto: &sessions.SessionState{}, outFile: "sessions/state.gen.go", }, - { - inProto: &sessions.WorkerInfo{}, - outFile: "sessions/workers.gen.go", - }, { inProto: &sessions.Connection{}, outFile: "sessions/connection.gen.go", @@ -677,4 +709,37 @@ var inputStructs = []*structInfo{ fieldFilter: []string{"private_key"}, recursiveListing: true, }, + { + inProto: &workers.Worker{}, + outFile: "workers/worker.gen.go", + templates: []*template.Template{ + clientTemplate, + template.Must(template.New("").Funcs( + template.FuncMap{ + "snakeCase": snakeCase, + "funcName": func() string { + return "CreateWorkerLed" + }, + "apiAction": func() string { + return ":create:worker-led" + }, + }, + ).Parse(createTemplateStr)), + readTemplate, + updateTemplate, + deleteTemplate, + listTemplate, + }, + extraRequiredParams: []requiredParam{ + { + Name: "workerGeneratedAuthToken", + Typ: "string", + PostType: "worker_generated_auth_token", + }, + }, + pluralResourceName: "workers", + createResponseTypes: true, + recursiveListing: true, + versionEnabled: true, + }, } diff --git a/internal/api/genapi/parse.go b/internal/api/genapi/parse.go index e60c703f7..9062e3aa8 100644 --- a/internal/api/genapi/parse.go +++ b/internal/api/genapi/parse.go @@ -70,7 +70,7 @@ func parsePBs() { name = fmt.Sprintf("%s.%s", pkg, name) } switch name { - case "v1.AuthorizedCollectionActionsEntry": + case "v1.AuthorizedCollectionActionsEntry", "v1.CanonicalTagsEntry", "v1.TagsEntry", "v1.ConfigTagsEntry": fi.FieldType = "map[string][]string" default: fi.FieldType = sliceText + ptr + name diff --git a/internal/api/genapi/templates.go b/internal/api/genapi/templates.go index 71a6086df..0fce42d72 100644 --- a/internal/api/genapi/templates.go +++ b/internal/api/genapi/templates.go @@ -46,7 +46,7 @@ type templateInput struct { SliceSubtypes map[string]sliceSubtypeInfo ExtraFields []fieldInfo VersionEnabled bool - TypeOnCreate bool + ExtraRequiredParams []requiredParam CreateResponseTypes bool RecursiveListing bool } @@ -65,7 +65,7 @@ func fillTemplates() { ParentTypeName: in.parentTypeName, ExtraFields: in.extraFields, VersionEnabled: in.versionEnabled, - TypeOnCreate: in.typeOnCreate, + ExtraRequiredParams: in.extraRequiredParams, CreateResponseTypes: in.createResponseTypes, RecursiveListing: in.recursiveListing, } @@ -346,14 +346,10 @@ func (c *Client) Delete(ctx context.Context, id string, opt... Option) (*{{ .Nam } `)) -var createTemplate = template.Must(template.New("").Funcs( - template.FuncMap{ - "snakeCase": snakeCase, - }, -).Parse(` -func (c *Client) Create (ctx context.Context, {{ if .TypeOnCreate }} resourceType string, {{ end }} {{ .CollectionFunctionArg }} string, opt... Option) (*{{ .Name }}CreateResult, error) { +const createTemplateStr = ` +func (c *Client) {{ funcName }} (ctx context.Context, {{ range .ExtraRequiredParams }} {{ .Name }} {{ .Typ }}, {{ end }} {{ .CollectionFunctionArg }} string, opt... Option) (*{{ .Name }}CreateResult, error) { if {{ .CollectionFunctionArg }} == "" { - return nil, fmt.Errorf("empty {{ .CollectionFunctionArg }} value passed into Create request") + return nil, fmt.Errorf("empty {{ .CollectionFunctionArg }} value passed into {{ funcName }} request") } opts, apiOpts := getOpts(opt...) @@ -361,17 +357,17 @@ func (c *Client) Create (ctx context.Context, {{ if .TypeOnCreate }} resourceTyp if c.client == nil { return nil, fmt.Errorf("nil client") } - {{ if .TypeOnCreate }} if resourceType == "" { - return nil, fmt.Errorf("empty resourceType value passed into Create request") + {{ range .ExtraRequiredParams }} if {{ .Name }} == "" { + return nil, fmt.Errorf("empty {{ .Name }} value passed into {{ funcName }} request") } else { - opts.postMap["type"] = resourceType + opts.postMap["{{ .PostType }}"] = {{ .Name }} }{{ end }} opts.postMap["{{ snakeCase .CollectionFunctionArg }}"] = {{ .CollectionFunctionArg }} - req, err := c.client.NewRequest(ctx, "POST", "{{ .CollectionPath }}", opts.postMap, apiOpts...) + req, err := c.client.NewRequest(ctx, "POST", "{{ .CollectionPath }}{{ apiAction }}", opts.postMap, apiOpts...) if err != nil { - return nil, fmt.Errorf("error creating Create request: %w", err) + return nil, fmt.Errorf("error creating {{ funcName }} request: %w", err) } {{ if ( eq .CollectionPath "\"scopes\"" ) }} opts.queryMap["scope_id"] = scopeId @@ -386,14 +382,14 @@ func (c *Client) Create (ctx context.Context, {{ if .TypeOnCreate }} resourceTyp resp, err := c.client.Do(req) if err != nil { - return nil, fmt.Errorf("error performing client request during Create call: %w", err) + return nil, fmt.Errorf("error performing client request during {{ funcName }} call: %w", err) } target := new({{ .Name }}CreateResult) target.Item = new({{ .Name }}) apiErr, err := resp.Decode(target.Item) if err != nil { - return nil, fmt.Errorf("error decoding Create response: %w", err) + return nil, fmt.Errorf("error decoding {{ funcName }} response: %w", err) } if apiErr != nil { return nil, apiErr @@ -401,7 +397,19 @@ func (c *Client) Create (ctx context.Context, {{ if .TypeOnCreate }} resourceTyp target.response = resp return target, nil } -`)) +` + +var commonCreateTemplate = template.Must(template.New("").Funcs( + template.FuncMap{ + "snakeCase": snakeCase, + "funcName": func() string { + return "Create" + }, + "apiAction": func() string { + return "" + }, + }, +).Parse(createTemplateStr)) var updateTemplate = template.Must(template.New("").Parse(` func (c *Client) Update(ctx context.Context, id string, version uint32, opt... Option) (*{{ .Name }}UpdateResult, error) { diff --git a/internal/cmd/base/listener.go b/internal/cmd/base/listener.go index ef4d82eb5..df148b6f1 100644 --- a/internal/cmd/base/listener.go +++ b/internal/cmd/base/listener.go @@ -36,6 +36,7 @@ type WorkerAuthInfo struct { Name string `json:"name"` Description string `json:"description"` ConnectionNonce string `json:"connection_nonce"` + ProxyAddress string `json:"proxy_address"` } // Factory is the factory function to create a listener. diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index d5c4735e0..25a241886 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -22,24 +22,25 @@ type Option func(*Options) // Options - how Options are represented. type Options struct { - withNoTokenScope bool - withNoTokenValue bool - withSkipDatabaseDestruction bool - withSkipAuthMethodCreation bool - withSkipOidcAuthMethodCreation bool - withSkipScopesCreation bool - withSkipHostResourcesCreation bool - withSkipTargetCreation bool - withContainerImage string - withDialect string - withDatabaseTemplate string - withEventerConfig *event.EventerConfig - withEventFlags *EventFlags - withEventWrapper wrapping.Wrapper - withAttributeFieldPrefix string - withStatusCode int - withHostPlugin func() (string, plugin.HostPluginServiceClient) - withEventGating bool + withNoTokenScope bool + withNoTokenValue bool + withSkipDatabaseDestruction bool + withSkipAuthMethodCreation bool + withSkipOidcAuthMethodCreation bool + withSkipScopesCreation bool + withSkipHostResourcesCreation bool + withSkipTargetCreation bool + withContainerImage string + withDialect string + withDatabaseTemplate string + withEventerConfig *event.EventerConfig + withEventFlags *EventFlags + withEventWrapper wrapping.Wrapper + withAttributeFieldPrefix string + withStatusCode int + withHostPlugin func() (string, plugin.HostPluginServiceClient) + withEventGating bool + withSkipWorkerAuthKmsInstantiation bool } func getDefaultOptions() Options { @@ -188,3 +189,11 @@ func WithEventGating(with bool) Option { o.withEventGating = with } } + +// WithSkipWorkerAuthKmsInstantiation causes KMS methods used for worker-auth to +// not be processed, useful for dev mode +func WithSkipWorkerAuthKmsInstantiation(with bool) Option { + return func(o *Options) { + o.withSkipWorkerAuthKmsInstantiation = with + } +} diff --git a/internal/cmd/base/server_test.go b/internal/cmd/base/server_test.go index a97303679..8f9d63833 100644 --- a/internal/cmd/base/server_test.go +++ b/internal/cmd/base/server_test.go @@ -55,8 +55,11 @@ func TestServer_SetupKMSes(t *testing.T) { purposes: []string{globals.KmsPurposeRoot}, }, { - name: "multi purpose", - purposes: []string{globals.KmsPurposeRoot, globals.KmsPurposeRecovery, globals.KmsPurposeWorkerAuth, globals.KmsPurposeConfig}, + name: "multi purpose", + purposes: []string{ + globals.KmsPurposeRoot, globals.KmsPurposeRecovery, globals.KmsPurposeWorkerAuth, + globals.KmsPurposeWorkerAuthStorage, globals.KmsPurposeConfig, + }, }, } logger := hclog.Default() @@ -89,6 +92,8 @@ func TestServer_SetupKMSes(t *testing.T) { assert.NotNil(s.RootKms) case globals.KmsPurposeWorkerAuth: assert.NotNil(s.WorkerAuthKms) + case globals.KmsPurposeWorkerAuthStorage: + assert.NotNil(s.WorkerAuthStorageKms) case globals.KmsPurposeRecovery: assert.NotNil(s.RecoveryKms) } diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index eebce3f25..91cb44566 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -10,6 +10,7 @@ import ( "net" "os" "os/signal" + "path/filepath" "sort" "strconv" "strings" @@ -61,6 +62,9 @@ const ( // TODO: This value is temporary, it will be removed once we have a better // story/direction on attributes and system defaults. statusGracePeriodEnvVar = "BOUNDARY_STATUS_GRACE_PERIOD" + + // File name to use for storing workerAuth requests + WorkerAuthReqFile = "auth_request_token" ) func init() { @@ -82,11 +86,12 @@ type Server struct { StderrLock *sync.Mutex Eventer *event.Eventer - RootKms wrapping.Wrapper - WorkerAuthKms wrapping.Wrapper - RecoveryKms wrapping.Wrapper - Kms *kms.Kms - SecureRandomReader io.Reader + RootKms wrapping.Wrapper + WorkerAuthKms wrapping.Wrapper + WorkerAuthStorageKms wrapping.Wrapper + RecoveryKms wrapping.Wrapper + Kms *kms.Kms + SecureRandomReader io.Reader PrometheusRegisterer prometheus.Registerer @@ -341,6 +346,29 @@ func (b *Server) RemovePidFile(pidPath string) error { return os.Remove(pidPath) } +func (b *Server) StoreWorkerAuthReq(authReq, workerAuthReqPath string) error { + // Quit fast if no workerAuthReqFile + if workerAuthReqPath == "" { + return nil + } + workerAuthReqFilePath := filepath.Join(workerAuthReqPath, WorkerAuthReqFile) + + // Open the workerAuthReq file + workerAuthReqFile, err := os.OpenFile(workerAuthReqFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640) + if err != nil { + return fmt.Errorf("could not open file for worker auth request: %w", err) + } + defer workerAuthReqFile.Close() + + // Write out the workerAuthRequest + _, err = workerAuthReqFile.WriteString(authReq) + if err != nil { + return fmt.Errorf("could not write to file for worker auth request: %w", err) + } + + return nil +} + func (b *Server) PrintInfo(ui cli.Ui) { verInfo := version.Get() if verInfo.Version != "" { @@ -517,7 +545,9 @@ func (b *Server) SetupListeners(ui cli.Ui, config *configutil.SharedConfig, allo // SetupKMSes takes in a parsed config, does some minor checking on purposes, // and sends each off to configutil to instantiate a wrapper. -func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Config) error { +func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Config, opt ...Option) error { + opts := getOpts(opt...) + sharedConfig := config.SharedConfig var pluginLogger hclog.Logger var err error @@ -527,7 +557,11 @@ func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Confi switch purpose { case "": return errors.New("KMS block missing 'purpose'") - case globals.KmsPurposeRoot, globals.KmsPurposeWorkerAuth, globals.KmsPurposeConfig: + case globals.KmsPurposeWorkerAuth: + if opts.withSkipWorkerAuthKmsInstantiation { + continue + } + case globals.KmsPurposeRoot, globals.KmsPurposeConfig, globals.KmsPurposeWorkerAuthStorage: case globals.KmsPurposeRecovery: if config.Controller != nil && config.DevRecoveryKey != "" { kms.Config["key"] = config.DevRecoveryKey @@ -592,6 +626,8 @@ func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Confi b.RootKms = wrapper case globals.KmsPurposeWorkerAuth: b.WorkerAuthKms = wrapper + case globals.KmsPurposeWorkerAuthStorage: + b.WorkerAuthStorageKms = wrapper case globals.KmsPurposeRecovery: b.RecoveryKms = wrapper case globals.KmsPurposeConfig: diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 13eb7db21..3b0088616 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/commands/targetscmd" "github.com/hashicorp/boundary/internal/cmd/commands/userscmd" "github.com/hashicorp/boundary/internal/cmd/commands/version" + "github.com/hashicorp/boundary/internal/cmd/commands/workerscmd" "github.com/mitchellh/cli" ) @@ -1071,5 +1072,47 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "remove-accounts", }, nil }, + + "workers": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + }, nil + }, + "workers create": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, + "workers create worker-led": func() (cli.Command, error) { + return &workerscmd.WorkerLedCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, + "workers read": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + Func: "read", + }, nil + }, + "workers update": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "workers delete": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + Func: "delete", + }, nil + }, + "workers list": func() (cli.Command, error) { + return &workerscmd.Command{ + Command: base.NewCommand(ui), + Func: "list", + }, nil + }, } } diff --git a/internal/cmd/commands/accountscmd/accounts.gen.go b/internal/cmd/commands/accountscmd/accounts.gen.go index 3e1ee1768..c350ab2cd 100644 --- a/internal/cmd/commands/accountscmd/accounts.gen.go +++ b/internal/cmd/commands/accountscmd/accounts.gen.go @@ -117,7 +117,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/authmethodscmd/authmethods.gen.go b/internal/cmd/commands/authmethodscmd/authmethods.gen.go index 967da8645..6eec33d34 100644 --- a/internal/cmd/commands/authmethodscmd/authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/authmethods.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/authtokenscmd/authtokens.gen.go b/internal/cmd/commands/authtokenscmd/authtokens.gen.go index 2cf42c934..e7454237a 100644 --- a/internal/cmd/commands/authtokenscmd/authtokens.gen.go +++ b/internal/cmd/commands/authtokenscmd/authtokens.gen.go @@ -115,6 +115,12 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp + case "create": + return cli.RunResultHelp + + case "update": + return cli.RunResultHelp + } c.plural = "auth token" diff --git a/internal/cmd/commands/credentiallibrariescmd/credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/credentiallibraries.gen.go index 2a8d809e0..e83a1c9d3 100644 --- a/internal/cmd/commands/credentiallibrariescmd/credentiallibraries.gen.go +++ b/internal/cmd/commands/credentiallibrariescmd/credentiallibraries.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/credentialscmd/credentials.gen.go b/internal/cmd/commands/credentialscmd/credentials.gen.go index 7726a2e80..7123fe254 100644 --- a/internal/cmd/commands/credentialscmd/credentials.gen.go +++ b/internal/cmd/commands/credentialscmd/credentials.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/credentialstorescmd/credentialstores.gen.go b/internal/cmd/commands/credentialstorescmd/credentialstores.gen.go index 162d88311..54366424a 100644 --- a/internal/cmd/commands/credentialstorescmd/credentialstores.gen.go +++ b/internal/cmd/commands/credentialstorescmd/credentialstores.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index ac2561eb4..fe8b9b8bc 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -25,12 +25,18 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/observability/event" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/store" "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/hashicorp/nodeenrollment/types" "github.com/mitchellh/cli" + "github.com/mr-tron/base58" "github.com/posener/complete" + "go.uber.org/atomic" + "google.golang.org/protobuf/proto" ) var ( @@ -84,6 +90,10 @@ type Command struct { flagEveryEventDenyFilters []string flagCreateLoopbackHostPlugin bool flagPluginExecutionDir string + flagUseKmsWorkerAuthMethod bool + flagWorkerAuthStorageDir string + flagWorkerAuthStorageSkipCleanup bool + flagWorkerAuthRotationInterval time.Duration } func (c *Command) Synopsis() string { @@ -327,6 +337,23 @@ func (c *Command) Flags() *base.FlagSets { EnvVar: "BOUNDARY_DEV_PLUGIN_EXECUTION_DIR", Usage: "Specifies where Boundary should write plugins that it is executing; if not set defaults to system temp directory.", }) + f.BoolVar(&base.BoolVar{ + Name: "use-kms-worker-auth-method", + Target: &c.flagUseKmsWorkerAuthMethod, + Usage: "If set, the original KMS-based method of worker auth will be used to connect the initial dev worker to the controller.", + }) + + f.StringVar(&base.StringVar{ + Name: "worker-auth-storage-dir", + Target: &c.flagWorkerAuthStorageDir, + Usage: "Specifies the directory to store worker authentication credentials in dev mode.", + }) + + f.BoolVar(&base.BoolVar{ + Name: "worker-auth-storage-skip-cleanup", + Target: &c.flagWorkerAuthStorageSkipCleanup, + Usage: "Prevents deletion of temp worker credential storage directory if set", + }) f.BoolVar(&base.BoolVar{ Name: "create-loopback-host-plugin", @@ -334,6 +361,12 @@ func (c *Command) Flags() *base.FlagSets { Hidden: true, }) + f.DurationVar(&base.DurationVar{ + Name: "worker-auth-rotation-interval", + Target: &c.flagWorkerAuthRotationInterval, + Hidden: true, + }) + return set } @@ -384,6 +417,7 @@ func (c *Command) Run(args []string) int { c.DevUnprivilegedPassword = c.flagUnprivilegedPassword c.DevTargetDefaultPort = c.flagTargetDefaultPort c.Config.Plugins.ExecutionDir = c.flagPluginExecutionDir + c.Config.Worker.AuthStoragePath = c.flagWorkerAuthStorageDir if c.flagIdSuffix != "" { if len(c.flagIdSuffix) != 10 { c.UI.Error("Invalid ID suffix, must be exactly 10 characters") @@ -449,7 +483,7 @@ func (c *Command) Run(args []string) int { if c.flagControllerClusterListenAddr != "" { l.Address = c.flagControllerClusterListenAddr if !c.flagControllerOnly { - c.Config.Worker.Controllers = []string{l.Address} + c.Config.Worker.InitialUpstreams = []string{l.Address} } } else { l.Address = "127.0.0.1:9201" @@ -540,7 +574,7 @@ func (c *Command) Run(args []string) int { if c.flagRecoveryKey != "" { c.Config.DevRecoveryKey = c.flagRecoveryKey } - if err := c.SetupKMSes(c.Context, c.UI, c.Config); err != nil { + if err := c.SetupKMSes(c.Context, c.UI, c.Config, base.WithSkipWorkerAuthKmsInstantiation(!c.flagUseKmsWorkerAuthMethod)); err != nil { c.UI.Error(err.Error()) return base.CommandUserError } @@ -548,14 +582,18 @@ func (c *Command) Run(args []string) int { c.UI.Error("Controller KMS not found after parsing KMS blocks") return base.CommandUserError } - if c.WorkerAuthKms == nil { - c.UI.Error("Worker Auth KMS not found after parsing KMS blocks") - return base.CommandUserError + if c.flagUseKmsWorkerAuthMethod { + if c.WorkerAuthKms == nil { + c.UI.Error("Worker Auth KMS not found after parsing KMS blocks") + return base.CommandUserError + } + c.Config.Worker.Name = "" + c.Config.Worker.Description = "" + c.InfoKeys = append(c.InfoKeys, "[Worker-Auth] AEAD Key Bytes") + c.Info["[Worker-Auth] AEAD Key Bytes"] = c.Config.DevWorkerAuthKey } c.InfoKeys = append(c.InfoKeys, "[Controller] AEAD Key Bytes") c.Info["[Controller] AEAD Key Bytes"] = c.Config.DevControllerKey - c.InfoKeys = append(c.InfoKeys, "[Worker-Auth] AEAD Key Bytes") - c.Info["[Worker-Auth] AEAD Key Bytes"] = c.Config.DevWorkerAuthKey c.InfoKeys = append(c.InfoKeys, "[Recovery] AEAD Key Bytes") c.Info["[Recovery] AEAD Key Bytes"] = c.Config.DevRecoveryKey @@ -592,7 +630,7 @@ func (c *Command) Run(args []string) int { opts = append(opts, base.WithContainerImage(c.flagContainerImage)) } if err := c.CreateDevDatabase(c.Context, opts...); err != nil { - c.UI.Error(fmt.Errorf("Error creating dev database container %w", err).Error()) + c.UI.Error(fmt.Errorf("Error creating dev database container: %w", err).Error()) return base.CommandCliError } @@ -613,12 +651,6 @@ func (c *Command) Run(args []string) int { } } - c.PrintInfo(c.UI) - if err := c.ReleaseLogGate(); err != nil { - c.UI.Error(fmt.Errorf("Error releasing event gate: %w", err).Error()) - return base.CommandCliError - } - { c.EnabledPlugins = append(c.EnabledPlugins, base.EnabledPluginHostAws, base.EnabledPluginHostAzure) conf := &controller.Config{ @@ -644,6 +676,8 @@ func (c *Command) Run(args []string) int { } } + errorEncountered := atomic.NewBool(false) + if !c.flagControllerOnly { conf := &worker.Config{ RawConfig: c.Config, @@ -657,6 +691,10 @@ func (c *Command) Run(args []string) int { return base.CommandCliError } + if c.flagWorkerAuthRotationInterval > 0 { + c.worker.TestOverrideAuthRotationPeriod = c.flagWorkerAuthRotationInterval + } + if err := c.worker.Start(); err != nil { retErr := fmt.Errorf("Error starting worker: %w", err) if err := c.worker.Shutdown(); err != nil { @@ -669,6 +707,54 @@ func (c *Command) Run(args []string) int { } return base.CommandCliError } + + if !c.flagUseKmsWorkerAuthMethod { + req := c.worker.WorkerAuthRegistrationRequest + if req == "" { + c.UI.Error("No worker auth registration request found at worker start time") + return base.CommandCliError + } + c.InfoKeys = append(c.InfoKeys, "worker auth registration request") + c.Info["worker auth registration request"] = req + c.InfoKeys = append(c.InfoKeys, "worker auth current key id") + c.Info["worker auth current key id"] = c.worker.WorkerAuthCurrentKeyId.Load() + c.InfoKeys = append(c.InfoKeys, "worker auth storage path") + c.Info["worker auth storage path"] = c.worker.WorkerAuthStorage.BaseDir() + if err := c.StoreWorkerAuthReq(c.worker.WorkerAuthRegistrationRequest, c.worker.WorkerAuthStorage.BaseDir()); err != nil { + // Shutdown on failure + retErr := fmt.Errorf("Error storing worker auth request: %w", err) + if err := c.worker.Shutdown(); err != nil { + c.UI.Error(retErr.Error()) + retErr = fmt.Errorf("Error shutting down worker: %w", err) + } + c.UI.Error(retErr.Error()) + if err := c.controller.Shutdown(); err != nil { + c.UI.Error(fmt.Errorf("Error with controller shutdown: %w", err).Error()) + } + return base.CommandCliError + } + go func() { + for { + select { + case <-c.Context.Done(): + return + case <-time.After(time.Second): + if err := authorizeWorker(c.Context, c.controller, req); err != nil { + c.UI.Error(fmt.Errorf("Error authorizing node: %w", err).Error()) + errorEncountered.Store(true) + return + } + return + } + } + }() + } + } + + c.PrintInfo(c.UI) + if err := c.ReleaseLogGate(); err != nil { + c.UI.Error(fmt.Errorf("Error releasing event gate: %w", err).Error()) + return base.CommandCliError } opsServer, err := ops.NewServer(c.Logger, c.controller, c.Listeners...) @@ -683,7 +769,7 @@ func (c *Command) Run(args []string) int { // Wait for shutdown shutdownTriggered := false - for !shutdownTriggered { + for !shutdownTriggered && !errorEncountered.Load() { select { case <-c.ShutdownCh: c.UI.Output("==> Boundary dev environment shutdown triggered, interrupt again to force") @@ -704,11 +790,23 @@ func (c *Command) Run(args []string) int { } }() + shutdownTriggered = true + + case <-c.SigUSR2Ch: + buf := make([]byte, 32*1024*1024) + n := runtime.Stack(buf[:], true) + event.WriteSysEvent(context.TODO(), op, "goroutine trace", "stack", string(buf[:n])) + } + + if shutdownTriggered { if c.Config.Controller != nil { c.opsServer.WaitIfHealthExists(c.Config.Controller.GracefulShutdownWaitDuration, c.UI) } if !c.flagControllerOnly { + if !c.flagWorkerAuthStorageSkipCleanup { + c.worker.WorkerAuthStorage.Cleanup() + } if err := c.worker.Shutdown(); err != nil { c.UI.Error(fmt.Errorf("Error shutting down worker: %w", err).Error()) } @@ -722,15 +820,36 @@ func (c *Command) Run(args []string) int { if err != nil { c.UI.Error(fmt.Errorf("Failed to shutdown ops listeners: %w", err).Error()) } - - shutdownTriggered = true - - case <-c.SigUSR2Ch: - buf := make([]byte, 32*1024*1024) - n := runtime.Stack(buf[:], true) - event.WriteSysEvent(context.TODO(), op, "goroutine trace", "stack", string(buf[:n])) } } return base.CommandSuccess } + +func authorizeWorker(ctx context.Context, c *controller.Controller, request string) error { + reqBytes, err := base58.FastBase58Decoding(request) + if err != nil { + return fmt.Errorf("error base58-decoding fetch node creds next proto value: %w", err) + } + // Decode the proto into the request + req := new(types.FetchNodeCredentialsRequest) + if err := proto.Unmarshal(reqBytes, req); err != nil { + return fmt.Errorf("error unmarshaling common name value: %w", err) + } + + serversRepo, err := c.ServersRepoFn() + if err != nil { + return fmt.Errorf("error fetching servers repo: %w", err) + } + + _, err = serversRepo.CreateWorker(ctx, &servers.Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + }, + }, servers.WithFetchNodeCredentialsRequest(req)) + if err != nil { + return fmt.Errorf("error creating worker: %w", err) + } + + return err +} diff --git a/internal/cmd/commands/dev/flags_test.go b/internal/cmd/commands/dev/flags_test.go new file mode 100644 index 000000000..f52baea7d --- /dev/null +++ b/internal/cmd/commands/dev/flags_test.go @@ -0,0 +1,72 @@ +package dev + +import ( + "bufio" + "os" + "testing" + + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" +) + +func TestCommand_Flags(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + serverCmdUi := &base.BoundaryUI{ + Ui: &cli.ColoredUi{ + ErrorColor: cli.UiColorRed, + WarnColor: cli.UiColorYellow, + Ui: &cli.BasicUi{ + Reader: bufio.NewReader(os.Stdin), + Writer: os.Stdout, + }, + }, + Format: "table", + } + base.NewServer(base.NewCommand(serverCmdUi)) + + cmd := &Command{Server: base.NewServer(base.NewCommand(serverCmdUi))} + got := cmd.Flags() + completions := got.Completions() + + assert.Contains(completions, "-log-level") + assert.Contains(completions, "-log-format") + assert.Contains(completions, "-id-suffix") + assert.Contains(completions, "-password") + assert.Contains(completions, "-login-name") + assert.Contains(completions, "-unprivileged-password") + assert.Contains(completions, "-unprivileged-login-name") + assert.Contains(completions, "-api-listen-address") + assert.Contains(completions, "-host-address") + assert.Contains(completions, "-target-default-port") + assert.Contains(completions, "-target-session-connection-limit") + assert.Contains(completions, "-target-session-max-seconds") + assert.Contains(completions, "-cluster-listen-address") + assert.Contains(completions, "-controller-public-cluster-address") + assert.Contains(completions, "-ops-listen-address") + assert.Contains(completions, "-controller-only") + assert.Contains(completions, "-proxy-listen-address") + assert.Contains(completions, "-worker-public-address") + assert.Contains(completions, "-worker-auth-key") + assert.Contains(completions, "-disable-database-destruction") + assert.Contains(completions, "-combine-logs") + assert.Contains(completions, "-ui-passthrough-dir") + assert.Contains(completions, "-recovery-key") + assert.Contains(completions, "-database-url") + assert.Contains(completions, "-container-image") + assert.Contains(completions, "-event-format") + assert.Contains(completions, "-observation-events") + assert.Contains(completions, "-audit-events") + assert.Contains(completions, "-system-events") + assert.Contains(completions, "-audit-events") + assert.Contains(completions, "-event-allow-filter") + assert.Contains(completions, "-event-deny-filter") + assert.Contains(completions, "-plugin-execution-dir") + assert.Contains(completions, "-use-kms-worker-auth-method") + assert.Contains(completions, "-worker-auth-storage-dir") + assert.Contains(completions, "-worker-auth-storage-skip-cleanup") + + // keep adding assertions for other flags which should be set as a result of cmd.Flags() +} diff --git a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go index 439956b52..2dbe07472 100644 --- a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/hostscmd/hosts.gen.go b/internal/cmd/commands/hostscmd/hosts.gen.go index 3212c337c..4316dba34 100644 --- a/internal/cmd/commands/hostscmd/hosts.gen.go +++ b/internal/cmd/commands/hostscmd/hosts.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/hostsetscmd/hostsets.gen.go b/internal/cmd/commands/hostsetscmd/hostsets.gen.go index fc4bd76c0..5df30d0be 100644 --- a/internal/cmd/commands/hostsetscmd/hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/hostsets.gen.go @@ -117,7 +117,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/managedgroupscmd/managedgroups.gen.go b/internal/cmd/commands/managedgroupscmd/managedgroups.gen.go index 913dfcac0..0a2656650 100644 --- a/internal/cmd/commands/managedgroupscmd/managedgroups.gen.go +++ b/internal/cmd/commands/managedgroupscmd/managedgroups.gen.go @@ -115,7 +115,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/server/listener_reload_test.go b/internal/cmd/commands/server/listener_reload_test.go index 77398aa0d..8d77baf28 100644 --- a/internal/cmd/commands/server/listener_reload_test.go +++ b/internal/cmd/commands/server/listener_reload_test.go @@ -62,6 +62,7 @@ kms "aead" { listener "tcp" { purpose = "api" + address = "127.0.0.1:9500" tls_cert_file = "%s/bundle.pem" tls_key_file = "%s/bundle.pem" cors_enabled = true @@ -69,10 +70,12 @@ listener "tcp" { } listener "tcp" { + address = "127.0.0.1:9501" purpose = "cluster" } listener "tcp" { + address = "127.0.0.1:9502" purpose = "proxy" } ` @@ -91,7 +94,9 @@ func TestServer_ReloadListener(t *testing.T) { } defer os.RemoveAll(td) - controllerKey, workerAuthKey, recoveryKey := config.DevKeyGeneration() + controllerKey := config.DevKeyGeneration() + workerAuthKey := config.DevKeyGeneration() + recoveryKey := config.DevKeyGeneration() cmd := testServerCommand(t, testServerCommandOpts{ CreateDevDatabase: true, @@ -131,7 +136,7 @@ func TestServer_ReloadListener(t *testing.T) { }() testCertificateSerial := func(serial string) { - conn, err := tls.Dial("tcp", "127.0.0.1:9200", &tls.Config{ + conn, err := tls.Dial("tcp", "127.0.0.1:9500", &tls.Config{ RootCAs: certPool, }) require.NoError(err) diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go index f91291b9f..cf819ae9a 100644 --- a/internal/cmd/commands/server/server.go +++ b/internal/cmd/commands/server/server.go @@ -32,6 +32,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/pluginutil/v2" + "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" "github.com/posener/complete" "go.uber.org/atomic" @@ -177,6 +178,8 @@ func (c *Command) Run(args []string) int { base.StartMemProfiler(c.Context) + // Note: the checks directly after this must remain where they are because + // they rely on the state of configured KMSes. if err := c.SetupKMSes(c.Context, c.UI, c.Config); err != nil { c.UI.Error(err.Error()) return base.CommandUserError @@ -187,9 +190,23 @@ func (c *Command) Run(args []string) int { return base.CommandUserError } } - if c.WorkerAuthKms == nil { - c.UI.Error("Worker Auth KMS not found after parsing KMS blocks") - return base.CommandUserError + if c.Config.Worker != nil { + switch c.WorkerAuthKms { + case nil: + if c.Config.Worker.AuthStoragePath == "" { + c.UI.Error("No worker auth KMS specified and no worker auth storage path specified.") + return base.CommandUserError + } + if c.Config.Worker.Name != "" || c.Config.Worker.Description != "" { + c.UI.Error("Worker config cannot contain name or description when using PKI-based worker authentication; it must be set via the API.") + return base.CommandUserError + } + default: + if c.Config.Worker.Name == "" { + c.UI.Error("Worker is using KMS auth but has no name set. It must be the unique name of this instance.") + return base.CommandUserError + } + } } if c.Config.DefaultMaxRequestDuration != 0 { @@ -267,6 +284,9 @@ func (c *Command) Run(args []string) int { c.UI.Error(`Config activates worker but no listener with "proxy" purpose found`) return base.CommandUserError } + if c.Config.Worker.ControllersRaw != nil { + c.UI.Warn("The \"controllers\" field for worker config is deprecated. Please use \"initial_upstreams\" instead.") + } if err := c.SetupWorkerPublicAddress(c.Config, ""); err != nil { c.UI.Error(err.Error()) @@ -276,25 +296,25 @@ func (c *Command) Run(args []string) int { c.Info["worker public proxy addr"] = c.Config.Worker.PublicAddr if c.Config.Controller != nil { - switch len(c.Config.Worker.Controllers) { + switch len(c.Config.Worker.InitialUpstreams) { case 0: if c.Config.Controller.PublicClusterAddr != "" { clusterAddr = c.Config.Controller.PublicClusterAddr } - c.Config.Worker.Controllers = []string{clusterAddr} + c.Config.Worker.InitialUpstreams = []string{clusterAddr} case 1: - if c.Config.Worker.Controllers[0] == clusterAddr { + if c.Config.Worker.InitialUpstreams[0] == clusterAddr { break } if c.Config.Controller.PublicClusterAddr != "" && - c.Config.Worker.Controllers[0] == c.Config.Controller.PublicClusterAddr { + c.Config.Worker.InitialUpstreams[0] == c.Config.Controller.PublicClusterAddr { break } // Best effort see if it's a domain name and if not assume it must match - host, _, err := net.SplitHostPort(c.Config.Worker.Controllers[0]) + host, _, err := net.SplitHostPort(c.Config.Worker.InitialUpstreams[0]) if err != nil && strings.Contains(err.Error(), "missing port in address") { err = nil - host = c.Config.Worker.Controllers[0] + host = c.Config.Worker.InitialUpstreams[0] } if err == nil { ip := net.ParseIP(host) @@ -305,17 +325,17 @@ func (c *Command) Run(args []string) int { } fallthrough default: - c.UI.Error(`When running a combined controller and worker, it's invalid to specify a "controllers" key in the worker block with any value other than the controller cluster address/port when using IPs rather than DNS names`) + c.UI.Error(`When running a combined controller and worker, it's invalid to specify a "initial_upstreams" or "controllers" key in the worker block with any values other than the controller cluster or upstream worker address/port when using IPs rather than DNS names`) return base.CommandUserError } } - for _, controller := range c.Config.Worker.Controllers { - host, _, err := net.SplitHostPort(controller) + for _, upstream := range c.Config.Worker.InitialUpstreams { + host, _, err := net.SplitHostPort(upstream) if err != nil { if strings.Contains(err.Error(), "missing port in address") { - host = controller + host = upstream } else { - c.UI.Error(fmt.Errorf("Invalid controller address %q: %w", controller, err).Error()) + c.UI.Error(fmt.Errorf("Invalid worker upstream address %q: %w", upstream, err).Error()) return base.CommandUserError } } @@ -329,11 +349,19 @@ func (c *Command) Run(args []string) int { errMsg = "a multicast" } if errMsg != "" { - c.UI.Error(fmt.Sprintf("Controller address %q is invalid: cannot be %s address", controller, errMsg)) + c.UI.Error(fmt.Sprintf("Worker upstream address %q is invalid: cannot be %s address", upstream, errMsg)) return base.CommandUserError } } } + + if c.Config.HCPBClusterId != "" { + _, err := uuid.ParseUUID(c.Config.HCPBClusterId) + if err != nil { + c.UI.Error(fmt.Errorf("Invalid HCPB cluster id %q: %w", c.Config.HCPBClusterId, err).Error()) + return base.CommandUserError + } + } } if err := c.SetupListeners(c.UI, c.Config.SharedConfig, []string{"api", "cluster", "proxy", "ops"}); err != nil { c.UI.Error(err.Error()) @@ -449,12 +477,6 @@ func (c *Command) Run(args []string) int { } }() - c.PrintInfo(c.UI) - if err := c.ReleaseLogGate(); err != nil { - c.UI.Error(fmt.Errorf("Error releasing event gate: %w", err).Error()) - return base.CommandCliError - } - if c.Config.Controller != nil { c.EnabledPlugins = append(c.EnabledPlugins, base.EnabledPluginHostAws, base.EnabledPluginHostAzure) if err := c.StartController(c.Context); err != nil { @@ -473,6 +495,34 @@ func (c *Command) Run(args []string) int { } return base.CommandCliError } + + if c.WorkerAuthKms == nil && c.worker.WorkerAuthRegistrationRequest != "" { + c.InfoKeys = append(c.InfoKeys, "worker auth registration request") + c.Info["worker auth registration request"] = c.worker.WorkerAuthRegistrationRequest + c.InfoKeys = append(c.InfoKeys, "worker auth current key id") + c.Info["worker auth current key id"] = c.worker.WorkerAuthCurrentKeyId.Load() + + // Write the WorkerAuth request to a file + if err := c.StoreWorkerAuthReq(c.worker.WorkerAuthRegistrationRequest, c.worker.WorkerAuthStorage.BaseDir()); err != nil { + // Shutdown on failure + retErr := fmt.Errorf("Error storing worker auth request: %w", err) + if err := c.worker.Shutdown(); err != nil { + c.UI.Error(retErr.Error()) + retErr = fmt.Errorf("Error shutting down worker: %w", err) + } + c.UI.Error(retErr.Error()) + if err := c.controller.Shutdown(); err != nil { + c.UI.Error(fmt.Errorf("Error with controller shutdown: %w", err).Error()) + } + return base.CommandCliError + } + } + } + + c.PrintInfo(c.UI) + if err := c.ReleaseLogGate(); err != nil { + c.UI.Error(fmt.Errorf("Error releasing event gate: %w", err).Error()) + return base.CommandCliError } opsServer, err := ops.NewServer(c.Logger, c.controller, c.Listeners...) @@ -520,10 +570,6 @@ func (c *Command) ParseFlagsAndConfig(args []string) int { c.UI.Error("Controller has no name set. It must be the unique name of this instance.") return base.CommandUserError } - if c.Config.Worker != nil && c.Config.Worker.Name == "" { - c.UI.Error("Worker has no name set. It must be the unique name of this instance.") - return base.CommandUserError - } return base.CommandSuccess } @@ -633,6 +679,14 @@ func (c *Command) StartWorker() error { return fmt.Errorf("Error initializing worker: %w", err) } + if c.WorkerAuthKms == nil { + if c.worker.WorkerAuthStorage == nil { + return fmt.Errorf("No worker auth KMS specified and no worker auth storage found") + } + c.InfoKeys = append(c.InfoKeys, "worker auth storage path") + c.Info["worker auth storage path"] = c.worker.WorkerAuthStorage.BaseDir() + } + if err := c.worker.Start(); err != nil { retErr := fmt.Errorf("Error starting worker: %w", err) if err := c.worker.Shutdown(); err != nil { diff --git a/internal/cmd/commands/server/worker_shutdown_reload_test.go b/internal/cmd/commands/server/worker_shutdown_reload_test.go index 20475a3ad..546840464 100644 --- a/internal/cmd/commands/server/worker_shutdown_reload_test.go +++ b/internal/cmd/commands/server/worker_shutdown_reload_test.go @@ -14,9 +14,10 @@ import ( "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/api/targets" - "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/tests/helper" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/testing/controller" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -24,54 +25,13 @@ import ( _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets/tcp" ) -const shutdownReloadApiAddr = "http://127.0.0.1:9203" - -const shutdownReloadControllerConfig = ` -disable_mlock = true - -controller { - name = "dev-controller" - description = "A default controller created in dev mode" - database { - url = "%s" - } -} - -kms "aead" { - purpose = "root" - aead_type = "aes-gcm" - key = "%s" - key_id = "global_root" -} - -kms "aead" { - purpose = "worker-auth" - aead_type = "aes-gcm" - key = "%s" - key_id = "global_worker-auth" -} - -listener "tcp" { - purpose = "api" - address = "127.0.0.1:9203" - tls_disable = true - cors_enabled = true - cors_allowed_origins = ["*"] -} - -listener "tcp" { - purpose = "cluster" - address = "127.0.0.1:9204" -} -` - -const shutdownReloadWorkerConfig = ` +const shutdownReloadWorkerProvidedConfiguration = ` disable_mlock = true worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1:9204"] + initial_upstreams = ["%s"] } kms "aead" { @@ -89,35 +49,16 @@ listener "tcp" { func TestServer_ShutdownWorker(t *testing.T) { require := require.New(t) - controllerKey, workerAuthKey, _ := config.DevKeyGeneration() - // Start the controller - controllerCmd := testServerCommand(t, testServerCommandOpts{ - CreateDevDatabase: true, - ControllerKey: controllerKey, - UseDevAuthMethod: true, - UseDevTarget: true, - }) - t.Cleanup(func() { - if controllerCmd.DevDatabaseCleanupFunc != nil { - require.NoError(controllerCmd.DevDatabaseCleanupFunc()) - } - }) - controllerCmd.presetConfig = atomic.NewString(fmt.Sprintf(shutdownReloadControllerConfig, controllerCmd.DatabaseUrl, controllerKey, workerAuthKey)) - - // Use code channel so that we can use test assertions on the returned integer. - // It is illegal to call `t.FailNow()` from a goroutine. - // https://pkg.go.dev/testing#T.FailNow - controllerCodeChan := make(chan int) - go func() { - controllerCodeChan <- controllerCmd.Run(nil) - }() - - waitCh(t, controllerCmd.startedCh) + rootWrapper, _ := wrapperWithKey(t) + recoveryWrapper, _ := wrapperWithKey(t) + workerAuthWrapper, key := wrapperWithKey(t) + testController := controller.NewTestController(t, controller.WithWorkerAuthKms(workerAuthWrapper), controller.WithRootKms(rootWrapper), controller.WithRecoveryKms(recoveryWrapper)) + defer testController.Shutdown() // Start the worker workerCmd := testServerCommand(t, testServerCommandOpts{}) - workerCmd.presetConfig = atomic.NewString(fmt.Sprintf(shutdownReloadWorkerConfig, workerAuthKey)) + workerCmd.presetConfig = atomic.NewString(fmt.Sprintf(shutdownReloadWorkerProvidedConfiguration, testController.ClusterAddrs()[0], key)) workerCodeChan := make(chan int) go func() { @@ -125,26 +66,32 @@ func TestServer_ShutdownWorker(t *testing.T) { }() waitCh(t, workerCmd.startedCh) + // Give the worker time for initial status or we can fail because no worker + // is ready for the connection yet + time.Sleep(5 * time.Second) + // Set up the target ctx := context.Background() - client := buildClient(t, shutdownReloadApiAddr) + client := buildClient(t, testController.ApiAddrs()[0]) setAuthToken(ctx, t, client) tcl := targets.NewClient(client) - tgt, err := tcl.Read(ctx, "ttcp_1234567890") + tgtL, err := tcl.List(ctx, scope.Global.String(), targets.WithRecursive(true)) require.NoError(err) + require.Len(tgtL.Items, 1) + tgt := tgtL.Items[0] require.NotNil(tgt) // Create test server, update default port on target ts := helper.NewTestTcpServer(t) require.NotNil(ts) defer ts.Close() - tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port())) + tgtR, err := tcl.Update(ctx, tgt.Id, tgt.Version, targets.WithTcpTargetDefaultPort(ts.Port())) require.NoError(err) - require.NotNil(tgt) + require.NotNil(tgtR) // Authorize and connect - sess := helper.NewTestSession(ctx, t, tcl, "ttcp_1234567890") + sess := helper.NewTestSession(ctx, t, tcl, tgt.Id) sConn := sess.Connect(ctx, t) // Run initial send/receive test, make sure things are working @@ -160,14 +107,7 @@ func TestServer_ShutdownWorker(t *testing.T) { // Connection should fail, and the session should be closed on the DB. sConn.TestSendRecvFail(t) - sess.ExpectConnectionStateOnController(ctx, t, controllerCmd.controller.ConnectionRepoFn, session.StatusClosed) - - // We're done! Shutdown the controller, and that's it. - close(controllerCmd.ShutdownCh) - if <-controllerCodeChan != 0 { - output := controllerCmd.UI.(*cli.MockUi).ErrorWriter.String() + controllerCmd.UI.(*cli.MockUi).OutputWriter.String() - require.FailNow(output, "command exited with non-zero error code") - } + sess.ExpectConnectionStateOnController(ctx, t, testController.Controller().ConnectionRepoFn, session.StatusClosed) } // largely copied from controller/testing.go diff --git a/internal/cmd/commands/server/worker_tags_reload_test.go b/internal/cmd/commands/server/worker_tags_reload_test.go index 66da2faed..0fbd9a13f 100644 --- a/internal/cmd/commands/server/worker_tags_reload_test.go +++ b/internal/cmd/commands/server/worker_tags_reload_test.go @@ -10,12 +10,19 @@ package server import ( + "context" + "crypto/rand" + "encoding/base64" "fmt" + "os" + "path/filepath" "sync" "testing" "time" - "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/testing/controller" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -24,26 +31,6 @@ import ( const workerBaseConfig = ` disable_mlock = true -telemetry { - prometheus_retention_time = "24h" - disable_hostname = true -} - -controller { - name = "dev-controller" - description = "A default controller created in dev mode" - database { - url = "%s" - } -} - -kms "aead" { - purpose = "root" - aead_type = "aes-gcm" - key = "%s" - key_id = "global_root" -} - kms "aead" { purpose = "worker-auth" aead_type = "aes-gcm" @@ -51,75 +38,54 @@ kms "aead" { key_id = "global_worker-auth" } -kms "aead" { - purpose = "recovery" - aead_type = "aes-gcm" - key = "%s" - key_id = "global_recovery" -} - -listener "tcp" { - purpose = "api" - address = "127.0.0.1:9203" - tls_disable = true - cors_enabled = true - cors_allowed_origins = ["*"] -} - -listener "tcp" { - purpose = "cluster" - address = "127.0.0.1:9204" -} - listener "tcp" { purpose = "proxy" - address = "127.0.0.1:9205" + address = "127.0.0.1:9405" } - ` const tag1Config = ` worker { - name = "dev-worker" + name = "test" description = "A default worker created in dev mode" - controllers = ["127.0.0.1:9204"] + initial_upstreams = ["%s"] tags { type = ["dev", "local"] } + auth_storage_path = "%s" } ` const tag2Config = ` worker { - name = "dev-worker" + name = "test" description = "A default worker created in dev mode" - controllers = ["127.0.0.1:9204"] + initial_upstreams = ["%s"] tags { foo = ["bar", "baz"] } + auth_storage_path = "%s" } ` func TestServer_ReloadWorkerTags(t *testing.T) { t.Parallel() require := require.New(t) + + rootWrapper, _ := wrapperWithKey(t) + recoveryWrapper, _ := wrapperWithKey(t) + workerAuthWrapper, key := wrapperWithKey(t) + testController := controller.NewTestController(t, controller.WithWorkerAuthKms(workerAuthWrapper), controller.WithRootKms(rootWrapper), controller.WithRecoveryKms(recoveryWrapper)) + defer testController.Shutdown() + + authStoragePath, err := os.MkdirTemp("", "") + require.NoError(err) + t.Cleanup(func() { os.RemoveAll(authStoragePath) }) + wg := &sync.WaitGroup{} - controllerKey, workerAuthKey, recoveryKey := config.DevKeyGeneration() - - cmd := testServerCommand(t, testServerCommandOpts{ - CreateDevDatabase: true, - ControllerKey: controllerKey, - UseDevAuthMethod: true, - UseDevTarget: true, - }) - defer func() { - if cmd.DevDatabaseCleanupFunc != nil { - require.NoError(cmd.DevDatabaseCleanupFunc()) - } - }() - - cmd.presetConfig = atomic.NewString(fmt.Sprintf(workerBaseConfig+tag1Config, cmd.DatabaseUrl, controllerKey, workerAuthKey, recoveryKey)) + cmd := testServerCommand(t, testServerCommandOpts{}) + cmd.presetConfig = atomic.NewString(fmt.Sprintf(workerBaseConfig+tag1Config, key, testController.ClusterAddrs()[0], filepath.Join(authStoragePath, "tag1"))) wg.Add(1) go func() { @@ -138,22 +104,21 @@ func TestServer_ReloadWorkerTags(t *testing.T) { fetchWorkerTags := func(name string, key string, values []string) { t.Helper() - serversRepo, err := cmd.controller.ServersRepoFn() + serversRepo, err := testController.Controller().ServersRepoFn() require.NoError(err) - tags, err := serversRepo.ListTagsForServers(cmd.Context, []string{name}) + w, err := serversRepo.LookupWorkerByName(testController.Context(), name) require.NoError(err) - require.Len(tags, 2) - require.Equal(key, tags[0].Key) - require.Equal(values[0], tags[0].Value) - require.Equal(key, tags[1].Key) - require.Equal(values[1], tags[1].Value) + require.NotNil(w) + v, ok := w.CanonicalTags()[key] + require.True(ok) + require.ElementsMatch(values, v) } // Give time to populate up to the controller time.Sleep(10 * time.Second) - fetchWorkerTags("dev-worker", "type", []string{"dev", "local"}) + fetchWorkerTags("test", "type", []string{"dev", "local"}) - cmd.presetConfig.Store(fmt.Sprintf(workerBaseConfig+tag2Config, cmd.DatabaseUrl, controllerKey, workerAuthKey, recoveryKey)) + cmd.presetConfig.Store(fmt.Sprintf(workerBaseConfig+tag2Config, key, testController.ClusterAddrs()[0], filepath.Join(authStoragePath, "tag2"))) cmd.SighupCh <- struct{}{} select { @@ -163,9 +128,31 @@ func TestServer_ReloadWorkerTags(t *testing.T) { } time.Sleep(10 * time.Second) - fetchWorkerTags("dev-worker", "foo", []string{"bar", "baz"}) + fetchWorkerTags("test", "foo", []string{"bar", "baz"}) close(cmd.ShutdownCh) wg.Wait() } + +// TestWrapper initializes an AEAD wrapping.Wrapper for testing the oplog +func wrapperWithKey(t testing.TB) (wrapping.Wrapper, string) { + t.Helper() + rootKey := make([]byte, 32) + n, err := rand.Read(rootKey) + if err != nil { + t.Fatal(err) + } + if n != 32 { + t.Fatal(n) + } + root := aead.NewWrapper() + _, err = root.SetConfig(context.Background(), wrapping.WithKeyId(base64.StdEncoding.EncodeToString(rootKey))) + if err != nil { + t.Fatal(err) + } + if err := root.SetAesGcmKeyBytes(rootKey); err != nil { + t.Fatal(err) + } + return root, base64.StdEncoding.EncodeToString(rootKey) +} diff --git a/internal/cmd/commands/sessionscmd/funcs.go b/internal/cmd/commands/sessionscmd/funcs.go index 0910106c1..4cc7bdf2d 100644 --- a/internal/cmd/commands/sessionscmd/funcs.go +++ b/internal/cmd/commands/sessionscmd/funcs.go @@ -227,19 +227,6 @@ func printItemTable(result api.GenericResult) string { } } - var workerInfoMaps []map[string]interface{} - if len(item.WorkerInfo) > 0 { - for _, wi := range item.WorkerInfo { - m := map[string]interface{}{ - "Address": wi.Address, - } - workerInfoMaps = append(workerInfoMaps, m) - } - if l := len("Address"); l > maxLength { - maxLength = l - } - } - ret := []string{ "", "Session information:", @@ -275,17 +262,5 @@ func printItemTable(result api.GenericResult) string { } } - if len(item.WorkerInfo) > 0 { - ret = append(ret, - fmt.Sprintf(" Worker Info: %s", ""), - ) - for _, m := range workerInfoMaps { - ret = append(ret, - base.WrapMap(4, maxLength, m), - "", - ) - } - } - return base.WrapForHelpText(ret) } diff --git a/internal/cmd/commands/sessionscmd/sessions.gen.go b/internal/cmd/commands/sessionscmd/sessions.gen.go index 5732f46f6..5308fd5da 100644 --- a/internal/cmd/commands/sessionscmd/sessions.gen.go +++ b/internal/cmd/commands/sessionscmd/sessions.gen.go @@ -112,6 +112,12 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp + case "create": + return cli.RunResultHelp + + case "update": + return cli.RunResultHelp + } c.plural = "session" diff --git a/internal/cmd/commands/targetscmd/targets.gen.go b/internal/cmd/commands/targetscmd/targets.gen.go index 4d6a2d72e..3b75cec70 100644 --- a/internal/cmd/commands/targetscmd/targets.gen.go +++ b/internal/cmd/commands/targetscmd/targets.gen.go @@ -123,7 +123,10 @@ func (c *Command) Run(args []string) int { case "": return cli.RunResultHelp - case "create", "update": + case "create": + return cli.RunResultHelp + + case "update": return cli.RunResultHelp } diff --git a/internal/cmd/commands/workerscmd/funcs.go b/internal/cmd/commands/workerscmd/funcs.go new file mode 100644 index 000000000..380a83f86 --- /dev/null +++ b/internal/cmd/commands/workerscmd/funcs.go @@ -0,0 +1,192 @@ +package workerscmd + +import ( + "fmt" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "": + return base.WrapForHelpText([]string{ + "Usage: boundary workers [sub command] [options] [args]", + "", + " This command allows operations on Boundary worker resources. Example:", + "", + " Read a worker:", + "", + ` $ boundary workers read -id w_1234567890`, + "", + " Please see the workers subcommand help for detailed usage information.", + }) + default: + helpStr = helpMap[c.Func]() + } + return helpStr + c.Flags().Help() +} + +func (c *Command) printListTable(items []*workers.Worker) string { + if len(items) == 0 { + return "No workers found" + } + + var output []string + output = []string{ + "", + "Worker information:", + } + for i, item := range items { + if i > 0 { + output = append(output, "") + } + if item.Id != "" { + output = append(output, + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), + ) + } + if c.FlagRecursive && item.ScopeId != "" { + output = append(output, + fmt.Sprintf(" Scope ID: %s", item.ScopeId), + ) + } + if item.Type != "" { + output = append(output, + fmt.Sprintf(" Type: %s", item.Type), + ) + } + if item.Version > 0 { + output = append(output, + fmt.Sprintf(" Version: %d", item.Version), + ) + } + if item.Name != "" { + output = append(output, + fmt.Sprintf(" Name: %s", item.Name), + ) + } + if item.Description != "" { + output = append(output, + fmt.Sprintf(" Description: %s", item.Description), + ) + } + if item.Address != "" { + output = append(output, + fmt.Sprintf(" Address: %s", item.Address), + ) + } + if !item.LastStatusTime.IsZero() { + output = append(output, + fmt.Sprintf(" Last Status Time: %s", item.LastStatusTime.Format(time.RFC1123)), + ) + } + + if len(item.AuthorizedActions) > 0 { + output = append(output, + " Authorized Actions:", + base.WrapSlice(6, item.AuthorizedActions), + ) + } + } + + return base.WrapForHelpText(output) +} + +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*workers.Worker) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if item.Address != "" { + nonAttributeMap["Address"] = item.Address + } + if !item.LastStatusTime.IsZero() { + nonAttributeMap["Last Status Time"] = item.LastStatusTime + } + + resultMap := result.GetResponse().Map + if count, ok := resultMap[globals.ActiveConnectionCountField]; ok { + nonAttributeMap["Active Connection Count"] = count + } + + maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) + + ret := []string{ + "", + "Worker information:", + base.WrapMap(2, maxLength+2, nonAttributeMap), + } + + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) + } + + if len(item.CanonicalTags) > 0 || len(item.ConfigTags) > 0 { + ret = append(ret, + "", + " Tags:", + ) + if len(item.ConfigTags) > 0 { + tagMap := make(map[string]any, len(item.ConfigTags)) + for k, v := range item.ConfigTags { + tagMap[k] = v + } + ret = append(ret, + " Worker Configuration:", + base.WrapMap(6, 2, tagMap), + ) + } + if len(item.CanonicalTags) > 0 { + tagMap := make(map[string]any, len(item.CanonicalTags)) + for k, v := range item.CanonicalTags { + tagMap[k] = v + } + ret = append(ret, + " Canonical:", + base.WrapMap(6, 2, tagMap), + ) + } + } + + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + return base.WrapForHelpText(ret) +} diff --git a/internal/cmd/commands/workerscmd/worker-led_funcs.go b/internal/cmd/commands/workerscmd/worker-led_funcs.go new file mode 100644 index 000000000..5c07dda6a --- /dev/null +++ b/internal/cmd/commands/workerscmd/worker-led_funcs.go @@ -0,0 +1,63 @@ +package workerscmd + +import ( + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func init() { + extraWorkerLedActionsFlagsMapFunc = extraWorkerLedActionsFlagsMapFuncImpl + extraWorkerLedFlagsFunc = extraWorkerLedFlagsFuncImpl + executeExtraWorkerLedActions = executeExtraWorkerLedActionsImpl +} + +type extraWorkerLedCmdVars struct { + flagWorkerGeneratedAuthToken string +} + +func extraWorkerLedActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "create": {"worker-generated-auth-token"}, + } +} + +func (c *WorkerLedCommand) extraWorkerLedHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary workers create worker-led [options] [args]", + "", + " Create a worker using the worker-led approach by providing an auth token from the worker. Example:", + "", + ` $ boundary workers create worker-led -name us-east-1-1 -worker-generated-auth-token "`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraWorkerLedFlagsFuncImpl(c *WorkerLedCommand, set *base.FlagSets, f *base.FlagSet) { + f = set.NewFlagSet("Worker Creation Options") + + for _, name := range flagsWorkerLedMap[c.Func] { + switch name { + case "worker-generated-auth-token": + f.StringVar(&base.StringVar{ + Name: "worker-generated-auth-token", + Target: &c.flagWorkerGeneratedAuthToken, + Usage: "The auth token provided by the worker to use to register it to Boundary", + }) + } + } +} + +func executeExtraWorkerLedActionsImpl(c *WorkerLedCommand, origResult api.GenericResult, origError error, workerClient *workers.Client, version uint32, opts []workers.Option) (api.GenericResult, error) { + switch c.Func { + case "create": + return workerClient.CreateWorkerLed(c.Context, c.flagWorkerGeneratedAuthToken, c.FlagScopeId, opts...) + } + return origResult, origError +} diff --git a/internal/cmd/commands/workerscmd/worker-led_workers.gen.go b/internal/cmd/commands/workerscmd/worker-led_workers.gen.go new file mode 100644 index 000000000..caa2c17e9 --- /dev/null +++ b/internal/cmd/commands/workerscmd/worker-led_workers.gen.go @@ -0,0 +1,244 @@ +// Code generated by "make cli"; DO NOT EDIT. +package workerscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initWorkerLedFlags() { + flagsOnce.Do(func() { + extraFlags := extraWorkerLedActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsWorkerLedMap[k] = append(flagsWorkerLedMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*WorkerLedCommand)(nil) + _ cli.CommandAutocomplete = (*WorkerLedCommand)(nil) +) + +type WorkerLedCommand struct { + *base.Command + + Func string + + plural string + + extraWorkerLedCmdVars +} + +func (c *WorkerLedCommand) AutocompleteArgs() complete.Predictor { + initWorkerLedFlags() + return complete.PredictAnything +} + +func (c *WorkerLedCommand) AutocompleteFlags() complete.Flags { + initWorkerLedFlags() + return c.Flags().Completions() +} + +func (c *WorkerLedCommand) Synopsis() string { + if extra := extraWorkerLedSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "worker" + + synopsisStr = fmt.Sprintf("%s %s", "worker-led-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *WorkerLedCommand) Help() string { + initWorkerLedFlags() + + var helpStr string + helpMap := common.HelpMap("worker") + + switch c.Func { + + default: + + helpStr = c.extraWorkerLedHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsWorkerLedMap = map[string][]string{ + + "create": {"scope-id", "name", "description"}, +} + +func (c *WorkerLedCommand) Flags() *base.FlagSets { + if len(flagsWorkerLedMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "worker-led-type worker", flagsWorkerLedMap, c.Func) + + extraWorkerLedFlagsFunc(c, set, f) + + return set +} + +func (c *WorkerLedCommand) Run(args []string) int { + initWorkerLedFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + case "update": + return cli.RunResultHelp + + } + + c.plural = "worker-led-type worker" + switch c.Func { + case "list": + c.plural = "worker-led-type workers" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsWorkerLedMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []workers.Option + + if strutil.StrListContains(flagsWorkerLedMap[c.Func], "scope-id") { + switch c.Func { + + case "create": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_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 + } + workersClient := workers.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, workers.DefaultName()) + default: + opts = append(opts, workers.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, workers.DefaultDescription()) + default: + opts = append(opts, workers.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, workers.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, workers.WithFilter(c.FlagFilter)) + } + + var version uint32 + + if ok := extraWorkerLedFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var result api.GenericResult + + switch c.Func { + + } + + result, err = executeExtraWorkerLedActions(c, result, err, workersClient, version, opts) + + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + var opts []base.Option + + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural), opts...) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError + } + + output, err := printCustomWorkerLedActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(result)) + + case "json": + if ok := c.PrintJsonItem(result); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +var ( + extraWorkerLedActionsFlagsMapFunc = func() map[string][]string { return nil } + extraWorkerLedSynopsisFunc = func(*WorkerLedCommand) string { return "" } + extraWorkerLedFlagsFunc = func(*WorkerLedCommand, *base.FlagSets, *base.FlagSet) {} + extraWorkerLedFlagsHandlingFunc = func(*WorkerLedCommand, *base.FlagSets, *[]workers.Option) bool { return true } + executeExtraWorkerLedActions = func(_ *WorkerLedCommand, inResult api.GenericResult, inErr error, _ *workers.Client, _ uint32, _ []workers.Option) (api.GenericResult, error) { + return inResult, inErr + } + printCustomWorkerLedActionOutput = func(*WorkerLedCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/workerscmd/workers.gen.go b/internal/cmd/commands/workerscmd/workers.gen.go new file mode 100644 index 000000000..d9434dae6 --- /dev/null +++ b/internal/cmd/commands/workerscmd/workers.gen.go @@ -0,0 +1,314 @@ +// Code generated by "make cli"; DO NOT EDIT. +package workerscmd + +import ( + "errors" + "fmt" + "sync" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initFlags() { + flagsOnce.Do(func() { + extraFlags := extraActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsMap[k] = append(flagsMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*Command)(nil) + _ cli.CommandAutocomplete = (*Command)(nil) +) + +type Command struct { + *base.Command + + Func string + + plural string +} + +func (c *Command) AutocompleteArgs() complete.Predictor { + initFlags() + return complete.PredictAnything +} + +func (c *Command) AutocompleteFlags() complete.Flags { + initFlags() + return c.Flags().Completions() +} + +func (c *Command) Synopsis() string { + if extra := extraSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "worker" + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *Command) Help() string { + initFlags() + + var helpStr string + helpMap := common.HelpMap("worker") + + switch c.Func { + + case "read": + helpStr = helpMap[c.Func]() + c.Flags().Help() + + case "update": + helpStr = helpMap[c.Func]() + c.Flags().Help() + + case "delete": + helpStr = helpMap[c.Func]() + c.Flags().Help() + + case "list": + helpStr = helpMap[c.Func]() + c.Flags().Help() + + default: + + helpStr = c.extraHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsMap = map[string][]string{ + + "read": {"id"}, + + "update": {"id", "name", "description", "version"}, + + "delete": {"id"}, + + "list": {"scope-id", "filter", "recursive"}, +} + +func (c *Command) Flags() *base.FlagSets { + if len(flagsMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "worker", flagsMap, c.Func) + + extraFlagsFunc(c, set, f) + + return set +} + +func (c *Command) Run(args []string) int { + initFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + case "create": + return cli.RunResultHelp + + } + + c.plural = "worker" + switch c.Func { + case "list": + c.plural = "workers" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []workers.Option + + if strutil.StrListContains(flagsMap[c.Func], "scope-id") { + switch c.Func { + + case "list": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_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 + } + workersClient := workers.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, workers.DefaultName()) + default: + opts = append(opts, workers.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, workers.DefaultDescription()) + default: + opts = append(opts, workers.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, workers.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, workers.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, workers.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var result api.GenericResult + + var listResult api.GenericListResult + + switch c.Func { + + case "read": + result, err = workersClient.Read(c.Context, c.FlagId, opts...) + + case "update": + result, err = workersClient.Update(c.Context, c.FlagId, version, opts...) + + case "delete": + result, err = workersClient.Delete(c.Context, c.FlagId, opts...) + + case "list": + listResult, err = workersClient.List(c.Context, c.FlagScopeId, opts...) + + } + + result, err = executeExtraActions(c, result, err, workersClient, version, opts) + + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + var opts []base.Option + + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural), opts...) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError + } + + output, err := printCustomActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + case "delete": + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(result); !ok { + return base.CommandCliError + } + + case "table": + c.UI.Output("The delete operation completed successfully.") + } + + return base.CommandSuccess + + case "list": + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItems(listResult); !ok { + return base.CommandCliError + } + + case "table": + listedItems := listResult.GetItems().([]*workers.Worker) + c.UI.Output(c.printListTable(listedItems)) + } + + return base.CommandSuccess + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(result)) + + case "json": + if ok := c.PrintJsonItem(result); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +var ( + flagsOnce = new(sync.Once) + + extraActionsFlagsMapFunc = func() map[string][]string { return nil } + extraSynopsisFunc = func(*Command) string { return "" } + extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]workers.Option) bool { return true } + executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *workers.Client, _ uint32, _ []workers.Option) (api.GenericResult, error) { + return inResult, inErr + } + printCustomActionOutput = func(*Command) (bool, error) { return false, nil } +) diff --git a/internal/cmd/common/help.go b/internal/cmd/common/help.go index cea87b356..20f6eb026 100644 --- a/internal/cmd/common/help.go +++ b/internal/cmd/common/help.go @@ -38,6 +38,7 @@ func HelpMap(resType string) map[string]func() string { resource.Host.String(): "h", resource.Session.String(): "s", resource.Target.String(): "t", + resource.Worker.String(): "w", } return map[string]func() string{ "base": func() string { diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 832782a4e..95c048d49 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -14,9 +14,9 @@ import ( "strings" "time" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/observability/event" wrapping "github.com/hashicorp/go-kms-wrapping/v2" - "github.com/hashicorp/go-secure-stdlib/base62" configutil "github.com/hashicorp/go-secure-stdlib/configutil/v2" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" @@ -57,6 +57,13 @@ kms "aead" { key_id = "global_worker-auth" } +kms "aead" { + purpose = "worker-auth-storage" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_worker-auth-storage" +} + kms "aead" { purpose = "recovery" aead_type = "aes-gcm" @@ -87,9 +94,9 @@ listener "tcp" { } worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags { type = ["dev", "local"] } @@ -105,17 +112,21 @@ type Config struct { Controller *Controller `hcl:"controller"` // Dev-related options - DevController bool `hcl:"-"` - DevUiPassthroughDir string `hcl:"-"` - DevControllerKey string `hcl:"-"` - DevWorkerAuthKey string `hcl:"-"` - DevRecoveryKey string `hcl:"-"` + DevController bool `hcl:"-"` + DevUiPassthroughDir string `hcl:"-"` + DevControllerKey string `hcl:"-"` + DevWorkerAuthKey string `hcl:"-"` + DevWorkerAuthStorageKey string `hcl:"-"` + DevRecoveryKey string `hcl:"-"` // Eventing configuration for the controller Eventing *event.EventerConfig `hcl:"events"` // Plugin-related options Plugins Plugins `hcl:"plugins"` + + // Internal field for use with HCP deployments. Used if controllers/ initial_upstreams is not set + HCPBClusterId string `hcl:"hcp_boundary_cluster_id"` } type Controller struct { @@ -170,8 +181,12 @@ type Worker struct { // We use a raw interface here so that we can take in a string // value pointing to an env var or file. We then resolve that - // and get the actual controller addresses. - Controllers []string `hcl:"-"` + // and get the actual upstream controller or worker addresses. + InitialUpstreams []string `hcl:"-"` + InitialUpstreamsRaw any `hcl:"initial_upstreams"` + + // The ControllersRaw field is deprecated and users should use InitialUpstreamsRaw instead. + // TODO: remove this field when support is discontinued. ControllersRaw interface{} `hcl:"controllers"` // We use a raw interface for parsing so that people can use JSON-like @@ -187,6 +202,9 @@ type Worker struct { // // TODO: This field is currently internal. StatusGracePeriodDuration time.Duration `hcl:"-"` + + // AuthStoragePath represents the location a worker stores its node credentials, if set + AuthStoragePath string `hcl:"auth_storage_path"` } func (w *Worker) InitNameIfEmpty() (string, error) { @@ -202,9 +220,10 @@ func (w *Worker) InitNameIfEmpty() (string, error) { func initNameIfEmpty(name *string) error { if *name == "" { var err error - if *name, err = base62.Random(10); err != nil { + if *name, err = db.NewPublicId("w"); err != nil { return err } + *name = strings.ToLower(*name) } return nil } @@ -230,8 +249,8 @@ func DevWorker() (*Config, error) { return parsed, nil } -func DevKeyGeneration() (string, string, string) { - var numBytes int64 = 96 +func DevKeyGeneration() string { + var numBytes int64 = 32 randBuf := new(bytes.Buffer) n, err := randBuf.ReadFrom(&io.LimitedReader{ R: rand.Reader, @@ -241,21 +260,21 @@ func DevKeyGeneration() (string, string, string) { panic(err) } if n != numBytes { - panic(fmt.Errorf("expected to read 64 bytes, read %d", n)) + panic(fmt.Errorf("expected to read 32 bytes, read %d", n)) } - controllerKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[0:32]) - workerAuthKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[32:64]) - recoveryKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[64:numBytes]) - - return controllerKey, workerAuthKey, recoveryKey + devKey := base64.StdEncoding.EncodeToString(randBuf.Bytes())[:numBytes] + return devKey } // DevController is a Config that is used for dev mode of Boundary // controllers func DevController() (*Config, error) { - controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration() + controllerKey := DevKeyGeneration() + workerAuthKey := DevKeyGeneration() + workerAuthStorageKey := DevKeyGeneration() + recoveryKey := DevKeyGeneration() - hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig, controllerKey, workerAuthKey, recoveryKey) + hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig, controllerKey, workerAuthKey, workerAuthStorageKey, recoveryKey) parsed, err := Parse(hclStr) if err != nil { return nil, fmt.Errorf("error parsing dev config: %w", err) @@ -263,13 +282,17 @@ func DevController() (*Config, error) { parsed.DevController = true parsed.DevControllerKey = controllerKey parsed.DevWorkerAuthKey = workerAuthKey + parsed.DevWorkerAuthStorageKey = workerAuthStorageKey parsed.DevRecoveryKey = recoveryKey return parsed, nil } func DevCombined() (*Config, error) { - controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration() - hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig+devWorkerExtraConfig, controllerKey, workerAuthKey, recoveryKey) + controllerKey := DevKeyGeneration() + workerAuthKey := DevKeyGeneration() + workerAuthStorageKey := DevKeyGeneration() + recoveryKey := DevKeyGeneration() + hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig+devWorkerExtraConfig, controllerKey, workerAuthKey, workerAuthStorageKey, recoveryKey) parsed, err := Parse(hclStr) if err != nil { return nil, fmt.Errorf("error parsing dev config: %w", err) @@ -277,6 +300,7 @@ func DevCombined() (*Config, error) { parsed.DevController = true parsed.DevControllerKey = controllerKey parsed.DevWorkerAuthKey = workerAuthKey + parsed.DevWorkerAuthStorageKey = workerAuthStorageKey parsed.DevRecoveryKey = recoveryKey return parsed, nil } @@ -505,9 +529,9 @@ func Parse(d string) (*Config, error) { } } - result.Worker.Controllers, err = parseWorkerControllers(result) + result.Worker.InitialUpstreams, err = parseWorkerUpstreams(result) if err != nil { - return nil, fmt.Errorf("Failed to parse worker controllers: %w", err) + return nil, fmt.Errorf("Failed to parse worker upstreams: %w", err) } } @@ -567,35 +591,52 @@ func Parse(d string) (*Config, error) { return result, nil } -func parseWorkerControllers(c *Config) ([]string, error) { +// supportControllersRawConfig returns either initialUpstreamsRaw or controllersRaw depending on which is populated. Errors when both fields are populated. +// +func supportControllersRawConfig(initialUpstreamsRaw, controllersRaw any) (any, error) { + switch { + case initialUpstreamsRaw == nil && controllersRaw != nil: + return controllersRaw, nil + case initialUpstreamsRaw != nil && controllersRaw != nil: + return nil, fmt.Errorf("both initial_upstreams and controllers fields are populated") + } + return initialUpstreamsRaw, nil +} + +func parseWorkerUpstreams(c *Config) ([]string, error) { if c == nil || c.Worker == nil { return nil, fmt.Errorf("config or worker field is nil") } - if c.Worker.ControllersRaw == nil { + if c.Worker.InitialUpstreamsRaw == nil && c.Worker.ControllersRaw == nil { + // return nil here so that other address sources can be provided outside of config return nil, nil } + rawUpstreams, err := supportControllersRawConfig(c.Worker.InitialUpstreamsRaw, c.Worker.ControllersRaw) + if err != nil { + return nil, err + } - switch t := c.Worker.ControllersRaw.(type) { + switch t := rawUpstreams.(type) { case []interface{}: // An array was configured directly in Boundary's HCL Config file. - var controllers []string - err := mapstructure.WeakDecode(c.Worker.ControllersRaw, &controllers) + var upstreams []string + err := mapstructure.WeakDecode(rawUpstreams, &upstreams) if err != nil { - return nil, fmt.Errorf("failed to decode worker controllers block into config field: %w", err) + return nil, fmt.Errorf("failed to decode worker initial_upstreams block into config field: %w", err) } - return controllers, nil + return upstreams, nil case string: - controllersStr, err := parseutil.ParsePath(t) + upstreamsStr, err := parseutil.ParsePath(t) if err != nil { return nil, fmt.Errorf("bad env var or file pointer: %w", err) } - var addrs []string - err = json.Unmarshal([]byte(controllersStr), &addrs) + var upstreams []string + err = json.Unmarshal([]byte(upstreamsStr), &upstreams) if err != nil { return nil, fmt.Errorf("failed to unmarshal env/file contents: %w", err) } - return addrs, nil + return upstreams, nil default: typ := reflect.TypeOf(t) diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 9a0210f24..6afefae26 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -61,6 +61,14 @@ func TestDevController(t *testing.T) { "key_id": "global_worker-auth", }, }, + { + Type: "aead", + Purpose: []string{"worker-auth-storage"}, + Config: map[string]string{ + "aead_type": "aes-gcm", + "key_id": "global_worker-auth-storage", + }, + }, { Type: "aead", Purpose: []string{"recovery"}, @@ -84,9 +92,11 @@ func TestDevController(t *testing.T) { exp.Seals[0].Config["key"] = actual.Seals[0].Config["key"] exp.Seals[1].Config["key"] = actual.Seals[1].Config["key"] exp.Seals[2].Config["key"] = actual.Seals[2].Config["key"] + exp.Seals[3].Config["key"] = actual.Seals[3].Config["key"] exp.DevControllerKey = actual.Seals[0].Config["key"] exp.DevWorkerAuthKey = actual.Seals[1].Config["key"] - exp.DevRecoveryKey = actual.Seals[2].Config["key"] + exp.DevWorkerAuthStorageKey = actual.Seals[2].Config["key"] + exp.DevRecoveryKey = actual.Seals[3].Config["key"] assert.Equal(t, exp, actual) @@ -180,10 +190,10 @@ func TestDevWorker(t *testing.T) { }, }, Worker: &Worker{ - Name: "dev-worker", - Description: "A default worker created in dev mode", - Controllers: []string{"127.0.0.1"}, - ControllersRaw: []interface{}{"127.0.0.1"}, + Name: "w_1234567890", + Description: "A default worker created in dev mode", + InitialUpstreams: []string{"127.0.0.1"}, + InitialUpstreamsRaw: []interface{}{"127.0.0.1"}, Tags: map[string][]string{ "type": {"dev", "local"}, }, @@ -201,9 +211,9 @@ func TestDevWorker(t *testing.T) { } worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags = ["type=dev", "type=local"] } ` @@ -221,9 +231,9 @@ func TestDevWorker(t *testing.T) { } worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags { type = "local" } @@ -246,9 +256,9 @@ func TestDevWorker(t *testing.T) { } worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags = ["tyPe=dev", "type=local"] } ` @@ -263,9 +273,9 @@ func TestDevWorker(t *testing.T) { } worker { - name = "dev-worker" + name = "w_1234567890" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags = ["type=dev", "type=loCal"] } ` @@ -282,7 +292,7 @@ func TestDevWorker(t *testing.T) { worker { name = "dev-work\u0000er" description = "A default worker created in dev mode" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] tags = ["type=dev", "type=local"] } ` @@ -291,6 +301,170 @@ func TestDevWorker(t *testing.T) { assert.Error(t, err) } +func TestDevCombined(t *testing.T) { + actual, err := DevCombined() + if err != nil { + t.Fatal(err) + } + + truePointer := new(bool) + *truePointer = true + exp := &Config{ + Eventing: event.DefaultEventerConfig(), + SharedConfig: &configutil.SharedConfig{ + DisableMlock: true, + Listeners: []*listenerutil.ListenerConfig{ + { + Type: "tcp", + Purpose: []string{"api"}, + TLSDisable: true, + CorsEnabled: truePointer, + CorsAllowedOrigins: []string{"*"}, + }, + { + Type: "tcp", + Purpose: []string{"cluster"}, + }, + { + Type: "tcp", + Purpose: []string{"ops"}, + TLSDisable: true, + }, + { + Type: "tcp", + Purpose: []string{"proxy"}, + }, + }, + Seals: []*configutil.KMS{ + { + Type: "aead", + Purpose: []string{"root"}, + Config: map[string]string{ + "aead_type": "aes-gcm", + "key_id": "global_root", + }, + }, + { + Type: "aead", + Purpose: []string{"worker-auth"}, + Config: map[string]string{ + "aead_type": "aes-gcm", + "key_id": "global_worker-auth", + }, + }, + { + Type: "aead", + Purpose: []string{"worker-auth-storage"}, + Config: map[string]string{ + "aead_type": "aes-gcm", + "key_id": "global_worker-auth-storage", + }, + }, + { + Type: "aead", + Purpose: []string{"recovery"}, + Config: map[string]string{ + "aead_type": "aes-gcm", + "key_id": "global_recovery", + }, + }, + }, + }, + Controller: &Controller{ + Name: "dev-controller", + Description: "A default controller created in dev mode", + }, + DevController: true, + Worker: &Worker{ + Name: "w_1234567890", + Description: "A default worker created in dev mode", + InitialUpstreams: []string{"127.0.0.1"}, + InitialUpstreamsRaw: []interface{}{"127.0.0.1"}, + Tags: map[string][]string{ + "type": {"dev", "local"}, + }, + }, + } + + exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig + exp.Listeners[1].RawConfig = actual.Listeners[1].RawConfig + exp.Listeners[2].RawConfig = actual.Listeners[2].RawConfig + exp.Listeners[3].RawConfig = actual.Listeners[3].RawConfig + exp.Seals[0].Config["key"] = actual.Seals[0].Config["key"] + exp.Seals[1].Config["key"] = actual.Seals[1].Config["key"] + exp.Seals[2].Config["key"] = actual.Seals[2].Config["key"] + exp.Seals[3].Config["key"] = actual.Seals[3].Config["key"] + exp.DevControllerKey = actual.Seals[0].Config["key"] + exp.DevWorkerAuthKey = actual.Seals[1].Config["key"] + exp.DevWorkerAuthStorageKey = actual.Seals[2].Config["key"] + exp.DevRecoveryKey = actual.Seals[3].Config["key"] + exp.Worker.TagsRaw = actual.Worker.TagsRaw + assert.Equal(t, exp, actual) +} + +func TestDevWorkerCredentialStorageDir(t *testing.T) { + t.Parallel() + tests := []struct { + name string + devWorkerProvidedConfiguration string + storagePath string + }{ + { + name: "Relative Storage Directory", + devWorkerProvidedConfiguration: ` + listener "tcp" { + purpose = "proxy" + } + + worker { + name = "w_1234567890" + description = "A default worker created in dev mode" + initial_upstreams = ["127.0.0.1"] + tags { + type = ["dev", "local"] + } + auth_storage_path = ".." + } + `, + storagePath: "..", + }, + { + name: "Nonexistent Storage Directory", + devWorkerProvidedConfiguration: ` + listener "tcp" { + purpose = "proxy" + } + + worker { + name = "w_1234567890" + description = "A default worker created in dev mode" + initial_upstreams = ["127.0.0.1"] + tags { + type = ["dev", "local"] + } + auth_storage_path = "nonexistent_dir/here" + } + `, + storagePath: "nonexistent_dir/here", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsed, err := Parse(devConfig + tt.devWorkerProvidedConfiguration) + require.NoError(t, err) + require.Equal(t, tt.storagePath, parsed.Worker.AuthStoragePath) + }) + } +} + +func TestDevKeyGeneration(t *testing.T) { + t.Parallel() + dk := DevKeyGeneration() + numBytes := 32 + require.Equal(t, numBytes, len(dk)) + require.NotEqual(t, dk, DevKeyGeneration()) +} + func TestParsingName(t *testing.T) { t.Parallel() config := ` @@ -315,18 +489,18 @@ func TestParsingName(t *testing.T) { { name: "no env", templateController: "foobar", - templateWorker: "barfoo", + templateWorker: "test_worker_barfoo", expectedController: "foobar", - expectedWorker: "barfoo", + expectedWorker: "test_worker_barfoo", }, { name: "env", templateController: fmt.Sprintf("env://%s", controllerEnv), templateWorker: fmt.Sprintf("env://%s", workerEnv), envController: "foobar2", - envWorker: "barfoo2", + envWorker: "env_name_barfoo2", expectedController: "foobar2", - expectedWorker: "barfoo2", + expectedWorker: "env_name_barfoo2", }, } for _, tt := range cases { @@ -390,7 +564,7 @@ func TestWorkerTags(t *testing.T) { name: "no tags", in: ` worker { - name = "testworker" + name = "w_1234567890" } `, expWorkerTags: nil, @@ -400,7 +574,7 @@ func TestWorkerTags(t *testing.T) { name: "empty tags", in: ` worker { - name = "testworker" + name = "w_1234567890" tags = {} } `, @@ -411,7 +585,7 @@ func TestWorkerTags(t *testing.T) { name: "empty tags 2", in: ` worker { - name = "testworker" + name = "w_1234567890" tags = [] } `, @@ -865,118 +1039,152 @@ func TestController_EventingConfig(t *testing.T) { } } -func TestWorkerControllers(t *testing.T) { +func TestWorkerUpstreams(t *testing.T) { tests := []struct { - name string - in string - stateFn func(t *testing.T) - expWorkerControllers []string - expErr bool - expErrIs error - expErrStr string + name string + in string + stateFn func(t *testing.T) + expWorkerUpstreams []string + expErr bool + expErrIs error + expErrStr string }{ { - name: "No Controllers", + name: "No Upstreams", in: ` worker { name = "test" } `, - expWorkerControllers: nil, - expErr: false, + expWorkerUpstreams: nil, + expErr: false, }, { - name: "One Controller", + name: "One Upstream", in: ` worker { name = "test" - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] } `, - expWorkerControllers: []string{"127.0.0.1"}, - expErr: false, + expWorkerUpstreams: []string{"127.0.0.1"}, + expErr: false, }, { - name: "Multiple controllers", + name: "Multiple Upstreams", in: ` worker { name = "test" - controllers = ["127.0.0.1", "127.0.0.2", "127.0.0.3"] + initial_upstreams = ["127.0.0.1", "127.0.0.2", "127.0.0.3"] } `, - expWorkerControllers: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, - expErr: false, + expWorkerUpstreams: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, + expErr: false, }, { name: "Using env var", in: ` worker { name = "test" - controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS" } `, - stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_CONTROLLERS", `["127.0.0.1", "127.0.0.2", "127.0.0.3"]`) }, - expWorkerControllers: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, - expErr: false, + stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_UPSTREAMS", `["127.0.0.1", "127.0.0.2", "127.0.0.3"]`) }, + expWorkerUpstreams: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, + expErr: false, }, { name: "Using env var - invalid input 1", in: ` worker { name = "test" - controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS" } `, stateFn: func(t *testing.T) { - controllers := ` + upstreams := ` worker { - controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] } ` - t.Setenv("BOUNDARY_WORKER_CONTROLLERS", controllers) + t.Setenv("BOUNDARY_WORKER_UPSTREAMS", upstreams) }, - expWorkerControllers: nil, - expErr: true, - expErrStr: "Failed to parse worker controllers: failed to unmarshal env/file contents: invalid character 'w' looking for beginning of value", + expWorkerUpstreams: nil, + expErr: true, + expErrStr: "Failed to parse worker upstreams: failed to unmarshal env/file contents: invalid character 'w' looking for beginning of value", }, { name: "Using env var - invalid input 2", in: ` worker { name = "test" - controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS" } `, - stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_CONTROLLERS", `controllers = ["127.0.0.1"]`) }, - expWorkerControllers: nil, - expErr: true, - expErrStr: "Failed to parse worker controllers: failed to unmarshal env/file contents: invalid character 'c' looking for beginning of value", + stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_UPSTREAMS", `initial_upstreams = ["127.0.0.1"]`) }, + expWorkerUpstreams: nil, + expErr: true, + expErrStr: "Failed to parse worker upstreams: failed to unmarshal env/file contents: invalid character 'i' looking for beginning of value", }, { name: "Unsupported object", in: ` worker { name = "test" - controllers = { + initial_upstreams = { ip = "127.0.0.1" ip = "127.0.0.2" } } `, - expWorkerControllers: nil, - expErr: true, - expErrStr: "Failed to parse worker controllers: unexpected type \"[]map[string]interface {}\"", + expWorkerUpstreams: nil, + expErr: true, + expErrStr: "Failed to parse worker upstreams: unexpected type \"[]map[string]interface {}\"", }, { - name: "worker controllers set to invalid url", + name: "Worker initial_upstreams set to invalid url", in: ` worker { name = "test" - controllers = "env://\x00" + initial_upstreams = "env://\x00" }`, - expWorkerControllers: nil, - expErr: true, - expErrIs: parseutil.ErrNotAUrl, + expWorkerUpstreams: nil, + expErr: true, + expErrIs: parseutil.ErrNotAUrl, + }, + { + name: "Worker using deprecated controllers field", + in: ` + worker { + name = "test" + controllers = ["127.0.0.1", "127.0.0.2", "127.0.0.3"] + }`, + expWorkerUpstreams: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, + expErr: false, + }, + { + name: "Different values in controllers and initial_upstreams field", + in: ` + worker { + name = "test" + controllers = ["127.0.0.1", "127.0.0.2", "127.0.0.3"] + initial_upstreams = ["127.0.0.1"] + }`, + expWorkerUpstreams: nil, + expErr: true, + expErrStr: "Failed to parse worker upstreams: both initial_upstreams and controllers fields are populated", + }, + { + name: "Identical values in controllers and initial_upstreams field", + in: ` + worker { + name = "test" + controllers = ["127.0.0.1"] + initial_upstreams = ["127.0.0.1"] + }`, + expWorkerUpstreams: nil, + expErr: true, + expErrStr: "Failed to parse worker upstreams: both initial_upstreams and controllers fields are populated", }, } @@ -1000,7 +1208,7 @@ func TestWorkerControllers(t *testing.T) { require.NoError(t, err) require.NotNil(t, c) require.NotNil(t, c.Worker) - require.EqualValues(t, tt.expWorkerControllers, c.Worker.Controllers) + require.EqualValues(t, tt.expWorkerUpstreams, c.Worker.InitialUpstreams) }) } } diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index e9536467c..27e3c5882 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -30,10 +30,6 @@ type cmdInfo struct { // output env var and print HasExampleCliOutput bool - // IsAbstractType triggers some behavior specialized for abstract types, - // e.g. those that have subcommands for create/update - IsAbstractType bool - // HasId controls whether to add ID emptiness checking. Note that some // commands that allow name/scope id or name/scope name handling may skip // this in favor of custom logic. @@ -79,6 +75,10 @@ type cmdInfo struct { // IsPluginType controls whether standard plugin flags are generated IsPluginType bool + + // SkipClientCallActions allows skipping creation of an actual client + // call for an action in favor of custom logic in extra actions + SkipClientCallActions []string } var inputStructs = map[string][]*cmdInfo{ @@ -89,7 +89,6 @@ var inputStructs = map[string][]*cmdInfo{ StdActions: []string{"read", "delete", "list"}, HasExtraCommandVars: true, HasExtraHelpFunc: true, - IsAbstractType: true, Container: "AuthMethod", HasId: true, HasName: true, @@ -130,7 +129,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.AuthMethod.String(), Pkg: "authmethods", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, HasExtraHelpFunc: true, Container: "Scope", HasId: true, @@ -179,7 +177,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.CredentialStore.String(), Pkg: "credentialstores", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, HasExtraHelpFunc: true, Container: "Scope", HasId: true, @@ -221,7 +218,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.CredentialLibrary.String(), Pkg: "credentiallibraries", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, HasExtraHelpFunc: true, Container: "CredentialStore", HasId: true, @@ -247,7 +243,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.Credential.String(), Pkg: "credentials", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, HasExtraHelpFunc: true, Container: "CredentialStore", HasId: true, @@ -288,7 +283,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.HostCatalog.String(), Pkg: "hostcatalogs", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, HasExtraHelpFunc: true, Container: "Scope", HasId: true, @@ -332,7 +326,6 @@ var inputStructs = map[string][]*cmdInfo{ StdActions: []string{"read", "delete", "list"}, HasExtraCommandVars: true, HasExtraHelpFunc: true, - IsAbstractType: true, Container: "HostCatalog", HasId: true, HasName: true, @@ -374,7 +367,6 @@ var inputStructs = map[string][]*cmdInfo{ Pkg: "hosts", StdActions: []string{"read", "delete", "list"}, HasExtraHelpFunc: true, - IsAbstractType: true, Container: "HostCatalog", HasId: true, HasName: true, @@ -400,7 +392,6 @@ var inputStructs = map[string][]*cmdInfo{ ResourceType: resource.ManagedGroup.String(), Pkg: "managedgroups", StdActions: []string{"read", "delete", "list"}, - IsAbstractType: true, Container: "AuthMethod", HasId: true, HasName: true, @@ -468,7 +459,6 @@ var inputStructs = map[string][]*cmdInfo{ HasExtraCommandVars: true, HasExtraHelpFunc: true, HasExampleCliOutput: true, - IsAbstractType: true, HasName: true, HasDescription: true, Container: "Scope", @@ -504,4 +494,32 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"update", "add-accounts", "remove-accounts", "set-accounts"}, }, }, + "workers": { + { + ResourceType: resource.Worker.String(), + Pkg: "workers", + StdActions: []string{"read", "update", "delete", "list"}, + HasExtraHelpFunc: true, + HasId: true, + Container: "Scope", + HasName: true, + HasDescription: true, + VersionedActions: []string{"update"}, + }, + { + ResourceType: resource.Worker.String(), + Pkg: "workers", + StdActions: []string{"create"}, + SubActionPrefix: "worker-led", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + Container: "Scope", + HasDescription: true, + NeedsSubtypeInCreate: true, + SkipClientCallActions: []string{"create"}, + }, + }, } diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index 230e70a06..4cb5fd21e 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -229,8 +229,12 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { switch c.Func { case "": return cli.RunResultHelp - {{ if .IsAbstractType }} - case "create", "update": + {{ if (not (hasAction .StdActions "create" ) )}} + case "create": + return cli.RunResultHelp + {{ end }} + {{ if (not (hasAction .StdActions "update" ) )}} + case "update": return cli.RunResultHelp {{ end }} } @@ -403,9 +407,11 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { switch c.Func { {{ range $i, $action := $input.StdActions }} {{ if eq $action "create" }} + {{ if ( not ( hasAction $input.SkipClientCallActions "create") ) }} case "create": result, err = {{ $input.Pkg }}Client.Create(c.Context, {{ if (and $input.SubActionPrefix $input.NeedsSubtypeInCreate) }}"{{ $input.SubActionPrefix }}",{{ end }} c.Flag{{ $input.Container }}Id, opts...) {{ end }} + {{ end }} {{ if eq $action "read" }} case "read": result, err = {{ $input.Pkg }}Client.Read(c.Context, c.FlagId, opts...) diff --git a/internal/credential/vault/setup_manual_test.go b/internal/credential/vault/setup_manual_test.go index f8a5cb97c..7a872b182 100644 --- a/internal/credential/vault/setup_manual_test.go +++ b/internal/credential/vault/setup_manual_test.go @@ -91,10 +91,10 @@ func TestSetupSleepyDevEnvironment(t *testing.T) { "foo": {"bar"}, } w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() addr := c1.ApiAddrs()[0] diff --git a/internal/daemon/cluster/handlers/multihop_service.go b/internal/daemon/cluster/handlers/multihop_service.go new file mode 100644 index 000000000..a5407b4d0 --- /dev/null +++ b/internal/daemon/cluster/handlers/multihop_service.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/multihop" + "github.com/hashicorp/nodeenrollment/registration" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/tls" + "github.com/hashicorp/nodeenrollment/types" + "github.com/hashicorp/nodeenrollment/util/temperror" +) + +type multihopServiceServer struct { + multihop.UnimplementedMultihopServiceServer + + storage nodeenrollment.Storage + direct bool + client *atomic.Value + options []nodeenrollment.Option +} + +// NewMultihopServiceServer creates a new service implementing +// MultihopServiceServer, storing values used for the implementing functions. +func NewMultihopServiceServer(storage nodeenrollment.Storage, direct bool, client *atomic.Value, opt ...nodeenrollment.Option) (*multihopServiceServer, error) { + const op = "cluster.handlers.NewMultihopServiceServer" + + switch { + case direct && nodeenrollment.IsNil(storage): + return nil, fmt.Errorf("%s: running on controller and nil storage provided", op) + case !direct && client == nil: + return nil, fmt.Errorf("%s: running on worker and nil client provided", op) + } + + return &multihopServiceServer{ + storage: storage, + direct: direct, + client: client, + options: opt, + }, nil +} + +var _ multihop.MultihopServiceServer = (*multihopServiceServer)(nil) + +// FetchNodeCredentials implements the MultihopServiceServer interface. If it's +// direct (e.g. running on a controller) it handles the request directly, +// otherwise sends it to its next hop. +func (m *multihopServiceServer) FetchNodeCredentials(ctx context.Context, req *types.FetchNodeCredentialsRequest) (*types.FetchNodeCredentialsResponse, error) { + const op = "cluster.handlers.(multihopServiceServer).FetchNodeCredentials" + switch m.direct { + case true: + return registration.FetchNodeCredentials(ctx, m.storage, req, m.options...) + + default: + client := m.client.Load() + if client == nil { + return nil, fmt.Errorf("%s: error fetching multihop connection, client is nil", op) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return nil, temperror.New(errors.New("client could not be understood as a multihop service client")) + } + return multihopClient.FetchNodeCredentials(ctx, req) + } +} + +// GenerateServerCertificates implements the MultihopServiceServer interface. If +// it's direct (e.g. running on a controller) it handles the request directly, +// otherwise sends it to its next hop. +func (m *multihopServiceServer) GenerateServerCertificates(ctx context.Context, req *types.GenerateServerCertificatesRequest) (*types.GenerateServerCertificatesResponse, error) { + const op = "cluster.handlers.(multihopServiceServer).GenerateServerCertificates" + switch m.direct { + case true: + return tls.GenerateServerCertificates(ctx, m.storage, req, m.options...) + + default: + client := m.client.Load() + if client == nil { + return nil, fmt.Errorf("%s: error fetching multihop connection, client is nil", op) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return nil, temperror.New(errors.New("client could not be understood as a multihop service client")) + } + return multihopClient.GenerateServerCertificates(ctx, req) + } +} + +// RotateNodeCredentials implements the MultihopServiceServer interface. If it's +// direct (e.g. running on a controller) it handles the request directly, +// otherwise sends it to its next hop. +func (m *multihopServiceServer) RotateNodeCredentials(ctx context.Context, req *types.RotateNodeCredentialsRequest) (*types.RotateNodeCredentialsResponse, error) { + const op = "cluster.handlers.(multihopServiceServer).RotateNodeCredentials" + switch m.direct { + case true: + return rotation.RotateNodeCredentials(ctx, m.storage, req, m.options...) + + default: + client := m.client.Load() + if client == nil { + return nil, fmt.Errorf("%s: error fetching multihop connection, client is nil", op) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return nil, temperror.New(errors.New("client could not be understood as a multihop service client")) + } + return multihopClient.RotateNodeCredentials(ctx, req) + } +} diff --git a/internal/daemon/cluster/handlers/worker_service.go b/internal/daemon/cluster/handlers/worker_service.go new file mode 100644 index 000000000..581182341 --- /dev/null +++ b/internal/daemon/cluster/handlers/worker_service.go @@ -0,0 +1,507 @@ +package handlers + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/hashicorp/boundary/internal/daemon/controller/common" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/observability/event" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" + "github.com/hashicorp/go-bexpr" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +type workerServiceServer struct { + pbs.UnimplementedServerCoordinationServiceServer + pbs.UnimplementedSessionServiceServer + + serversRepoFn common.ServersRepoFactory + sessionRepoFn common.SessionRepoFactory + connectionRepoFn common.ConnectionRepoFactory + updateTimes *sync.Map + kms *kms.Kms +} + +func NewWorkerServiceServer( + serversRepoFn common.ServersRepoFactory, + sessionRepoFn common.SessionRepoFactory, + connectionRepoFn common.ConnectionRepoFactory, + updateTimes *sync.Map, + kms *kms.Kms, +) *workerServiceServer { + return &workerServiceServer{ + serversRepoFn: serversRepoFn, + sessionRepoFn: sessionRepoFn, + connectionRepoFn: connectionRepoFn, + updateTimes: updateTimes, + kms: kms, + } +} + +var ( + _ pbs.SessionServiceServer = &workerServiceServer{} + _ pbs.ServerCoordinationServiceServer = &workerServiceServer{} +) + +func (ws *workerServiceServer) Status(ctx context.Context, req *pbs.StatusRequest) (*pbs.StatusResponse, error) { + const op = "workers.(workerServiceServer).Status" + // TODO: on the worker, if we get errors back from this repeatedly, do we + // terminate all sessions since we can't know if they were canceled? + + wStat := req.GetWorkerStatus() + if wStat == nil { + return &pbs.StatusResponse{}, status.Error(codes.InvalidArgument, "Worker sent nil status.") + } + switch { + case wStat.GetName() == "" && wStat.GetKeyId() == "": + return &pbs.StatusResponse{}, status.Error(codes.InvalidArgument, "Name and keyId are not set in the request; one is required.") + case wStat.GetName() != "" && wStat.GetKeyId() != "": + return &pbs.StatusResponse{}, status.Error(codes.InvalidArgument, "Name and keyId are both set in the request; only one is allowed.") + case wStat.GetAddress() == "": + return &pbs.StatusResponse{}, status.Error(codes.InvalidArgument, "Address is not set but is required.") + } + // This Store call is currently only for testing purposes + ws.updateTimes.Store(wStat.GetName(), time.Now()) + + serverRepo, err := ws.serversRepoFn() + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error getting servers repo")) + return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to store worker status: %v", err) + } + + // Convert API tags to storage tags + wTags := wStat.GetTags() + workerTags := make([]*servers.Tag, 0, len(wTags)) + for _, v := range wTags { + workerTags = append(workerTags, &servers.Tag{ + Key: v.GetKey(), + Value: v.GetValue(), + }) + } + + wConf := servers.NewWorker(scope.Global.String(), + servers.WithName(wStat.GetName()), + servers.WithDescription(wStat.GetDescription()), + servers.WithAddress(wStat.GetAddress()), + servers.WithWorkerTags(workerTags...)) + opts := []servers.Option{servers.WithUpdateTags(req.GetUpdateTags())} + if wStat.GetPublicId() != "" { + opts = append(opts, servers.WithPublicId(wStat.GetPublicId())) + } + if wStat.GetKeyId() != "" { + opts = append(opts, servers.WithKeyId(wStat.GetKeyId())) + } + wrk, err := serverRepo.UpsertWorkerStatus(ctx, wConf, opts...) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error storing worker status")) + return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error storing worker status: %v", err) + } + controllers, err := serverRepo.ListControllers(ctx) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error getting current controllers")) + return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error getting current controllers: %v", err) + } + + responseControllers := []*pbs.UpstreamServer{} + for _, c := range controllers { + thisController := &pbs.UpstreamServer{ + Address: c.Address, + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + } + responseControllers = append(responseControllers, thisController) + } + ret := &pbs.StatusResponse{ + CalculatedUpstreams: responseControllers, + WorkerId: wrk.GetPublicId(), + } + + stateReport := make([]session.StateReport, 0, len(req.GetJobs())) + + for _, jobStatus := range req.GetJobs() { + switch jobStatus.Job.GetType() { + case pbs.JOBTYPE_JOBTYPE_SESSION: + si := jobStatus.GetJob().GetSessionInfo() + if si == nil { + return nil, status.Error(codes.Internal, "Error getting session info at status time") + } + + switch si.Status { + case pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING, + pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED: + // No need to see about canceling anything + continue + } + + sr := session.StateReport{ + SessionId: si.GetSessionId(), + ConnectionIds: make([]string, 0, len(si.GetConnections())), + } + for _, conn := range si.GetConnections() { + switch conn.Status { + case pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED, + pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED: + sr.ConnectionIds = append(sr.ConnectionIds, conn.GetConnectionId()) + } + } + stateReport = append(stateReport, sr) + } + } + + sessRepo, err := ws.sessionRepoFn() + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error getting sessions repo")) + return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err) + } + connectionRepo, err := ws.connectionRepoFn() + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error getting connection repo")) + return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err) + } + + notActive, err := session.WorkerStatusReport(ctx, sessRepo, connectionRepo, wrk.GetPublicId(), stateReport) + if err != nil { + return nil, status.Errorf(codes.Internal, + "Error comparing state of sessions for worker with public id %q: %v", + wrk.GetPublicId(), err) + } + for _, na := range notActive { + var connChanges []*pbs.Connection + for _, connId := range na.ConnectionIds { + connChanges = append(connChanges, &pbs.Connection{ + ConnectionId: connId, + Status: session.StatusClosed.ProtoVal(), + }) + } + ret.JobsRequests = append(ret.JobsRequests, &pbs.JobChangeRequest{ + Job: &pbs.Job{ + Type: pbs.JOBTYPE_JOBTYPE_SESSION, + JobInfo: &pbs.Job_SessionInfo{ + SessionInfo: &pbs.SessionJobInfo{ + SessionId: na.SessionId, + Status: na.Status.ProtoVal(), + Connections: connChanges, + }, + }, + }, + RequestType: pbs.CHANGETYPE_CHANGETYPE_UPDATE_STATE, + }) + } + + return ret, nil +} + +func (ws *workerServiceServer) LookupSession(ctx context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) { + const op = "workers.(workerServiceServer).LookupSession" + + sessRepo, err := ws.sessionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "Error getting session repo: %v", err) + } + + sessionInfo, authzSummary, err := sessRepo.LookupSession(ctx, req.GetSessionId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "Error looking up session: %v", err) + } + if sessionInfo == nil { + return nil, status.Error(codes.PermissionDenied, "Unknown session ID.") + } + if len(sessionInfo.States) == 0 { + return nil, status.Error(codes.Internal, "Empty session states during lookup.") + } + + if sessionInfo.WorkerFilter != "" { + if req.ServerId == "" { + event.WriteError(ctx, op, errors.New("worker filter enabled for session but got no server ID from worker")) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Did not receive server ID when looking up session but filtering is enabled: %v", err) + } + serversRepo, err := ws.serversRepoFn() + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error getting servers repo")) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error acquiring server repo when looking up session: %v", err) + } + var w *servers.Worker + if req.WorkerKeyId != "" { + workerId, err := serversRepo.LookupWorkerIdByKeyId(ctx, req.WorkerKeyId) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up worker by keyId", "keyId", req.WorkerKeyId)) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error looking up tags for server: %v", err) + } + w, err = serversRepo.LookupWorker(ctx, workerId) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up worker by workerId", "workerId", workerId)) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error looking up tags for server: %v", err) + } + } else { + w, err = serversRepo.LookupWorkerByName(ctx, req.GetServerId()) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up worker by name", "name", req.ServerId)) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error looking up tags for server: %v", err) + } + } + // Build the map for filtering. + tagMap := w.CanonicalTags() + + // Create the evaluator + eval, err := bexpr.CreateEvaluator(sessionInfo.WorkerFilter) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error creating worker filter evaluator", "server_id", req.ServerId)) + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error creating worker filter evaluator: %v", err) + } + filterInput := map[string]interface{}{ + "name": w.GetName(), + "tags": tagMap, + } + ok, err := eval.Evaluate(filterInput) + if err != nil { + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, + fmt.Sprintf("Worker filter expression evaluation resulted in error: %s", err)) + } + if !ok { + return nil, handlers.ApiErrorWithCodeAndMessage( + codes.FailedPrecondition, + "Worker filter expression precludes this worker from serving this session") + } + } + + creds, err := sessRepo.ListSessionCredentials(ctx, sessionInfo.ScopeId, sessionInfo.PublicId) + if err != nil { + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, + fmt.Sprintf("Error retrieving session credentials: %s", err)) + } + var workerCreds []*pbs.Credential + for _, c := range creds { + m := &pbs.Credential{} + err = proto.Unmarshal(c, m) + if err != nil { + return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, + fmt.Sprintf("Error unmarshaling credentials: %s", err)) + } + workerCreds = append(workerCreds, m) + } + + resp := &pbs.LookupSessionResponse{ + Authorization: &targets.SessionAuthorizationData{ + SessionId: sessionInfo.GetPublicId(), + Certificate: sessionInfo.Certificate, + }, + Status: sessionInfo.States[0].Status.ProtoVal(), + Version: sessionInfo.Version, + TofuToken: string(sessionInfo.TofuToken), + Endpoint: sessionInfo.Endpoint, + Expiration: sessionInfo.ExpirationTime.Timestamp, + ConnectionLimit: sessionInfo.ConnectionLimit, + ConnectionsLeft: authzSummary.ConnectionLimit, + HostId: sessionInfo.HostId, + HostSetId: sessionInfo.HostSetId, + TargetId: sessionInfo.TargetId, + UserId: sessionInfo.UserId, + Credentials: workerCreds, + } + if resp.ConnectionsLeft != -1 { + resp.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount) + } + + wrapper, err := ws.kms.GetWrapper(ctx, sessionInfo.ScopeId, 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 +} + +func (ws *workerServiceServer) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) { + const op = "workers.(workerServiceServer).CancelSession" + + sessRepo, err := ws.sessionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + ses, _, err := sessRepo.LookupSession(ctx, req.GetSessionId()) + if err != nil { + return nil, err + } + + ses, err = sessRepo.CancelSession(ctx, req.GetSessionId(), ses.Version) + if err != nil { + return nil, err + } + + return &pbs.CancelSessionResponse{ + Status: ses.States[0].Status.ProtoVal(), + }, nil +} + +func (ws *workerServiceServer) ActivateSession(ctx context.Context, req *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) { + const op = "workers.(workerServiceServer).ActivateSession" + + sessRepo, err := ws.sessionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + sessionInfo, sessionStates, err := sessRepo.ActivateSession(ctx, req.GetSessionId(), req.GetVersion(), []byte(req.GetTofuToken())) + if err != nil { + return nil, status.Errorf(codes.Internal, "error looking up session: %v", err) + } + if sessionInfo == nil { + return nil, status.Error(codes.PermissionDenied, "Unknown session ID.") + } + if len(sessionStates) == 0 { + return nil, status.Error(codes.Internal, "Invalid session state in activate response.") + } + + return &pbs.ActivateSessionResponse{ + Status: sessionStates[0].Status.ProtoVal(), + }, nil +} + +func (ws *workerServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) { + const op = "workers.(workerServiceServer).AuthorizeConnection" + connectionRepo, err := ws.connectionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + sessionRepo, err := ws.sessionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + serversRepo, err := ws.serversRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting servers repo: %v", err) + } + w, err := serversRepo.LookupWorkerByName(ctx, req.GetWorkerId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "error looking up worker: %v", err) + } + if w == nil { + return nil, status.Errorf(codes.NotFound, "worker not found with name %q", req.GetWorkerId()) + } + + connectionInfo, connStates, authzSummary, err := session.AuthorizeConnection(ctx, sessionRepo, connectionRepo, req.GetSessionId(), w.GetPublicId()) + if err != nil { + return nil, err + } + if connectionInfo == nil { + return nil, status.Error(codes.Internal, "Invalid authorize connection response.") + } + if len(connStates) == 0 { + return nil, status.Error(codes.Internal, "Invalid connection state in authorize response.") + } + + ret := &pbs.AuthorizeConnectionResponse{ + ConnectionId: connectionInfo.GetPublicId(), + Status: connStates[0].Status.ProtoVal(), + ConnectionsLeft: authzSummary.ConnectionLimit, + } + if ret.ConnectionsLeft != -1 { + ret.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount) + } + + return ret, nil +} + +func (ws *workerServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) { + const op = "workers.(workerServiceServer).ConnectConnection" + connRepo, err := ws.connectionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + connectionInfo, connStates, err := connRepo.ConnectConnection(ctx, session.ConnectWith{ + ConnectionId: req.GetConnectionId(), + ClientTcpAddress: req.GetClientTcpAddress(), + ClientTcpPort: req.GetClientTcpPort(), + EndpointTcpAddress: req.GetEndpointTcpAddress(), + EndpointTcpPort: req.GetEndpointTcpPort(), + UserClientIp: req.GetUserClientIp(), + }) + if err != nil { + return nil, err + } + if connectionInfo == nil { + return nil, status.Error(codes.Internal, "Invalid connect connection response.") + } + + return &pbs.ConnectConnectionResponse{ + Status: connStates[0].Status.ProtoVal(), + }, nil +} + +func (ws *workerServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) { + const op = "workers.(workerServiceServer).CloseConnection" + numCloses := len(req.GetCloseRequestData()) + if numCloses == 0 { + return &pbs.CloseConnectionResponse{}, nil + } + + closeWiths := make([]session.CloseWith, 0, numCloses) + closeIds := make([]string, 0, numCloses) + + for _, v := range req.GetCloseRequestData() { + closeIds = append(closeIds, v.GetConnectionId()) + closeWiths = append(closeWiths, session.CloseWith{ + ConnectionId: v.GetConnectionId(), + BytesUp: v.GetBytesUp(), + BytesDown: v.GetBytesDown(), + ClosedReason: session.ClosedReason(v.GetReason()), + }) + } + connRepo, err := ws.connectionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting connection repo: %v", err) + } + + sessRepo, err := ws.sessionRepoFn() + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) + } + + closeInfos, err := session.CloseConnections(ctx, sessRepo, connRepo, closeWiths) + if err != nil { + return nil, err + } + if closeInfos == nil { + return nil, status.Error(codes.Internal, "Invalid close connection response.") + } + + closeData := make([]*pbs.CloseConnectionResponseData, 0, numCloses) + for _, v := range closeInfos { + if v.Connection == nil { + return nil, status.Errorf(codes.Internal, "No connection found while closing one of the connection IDs: %v", closeIds) + } + if len(v.ConnectionStates) == 0 { + return nil, status.Errorf(codes.Internal, "No connection states found while closing one of the connection IDs: %v", closeIds) + } + closeData = append(closeData, &pbs.CloseConnectionResponseData{ + ConnectionId: v.Connection.GetPublicId(), + Status: v.ConnectionStates[0].Status.ProtoVal(), + }) + } + + ret := &pbs.CloseConnectionResponse{ + CloseResponseData: closeData, + } + + return ret, nil +} diff --git a/internal/daemon/controller/handlers/workers/worker_service_status_test.go b/internal/daemon/cluster/handlers/worker_service_status_test.go similarity index 57% rename from internal/daemon/controller/handlers/workers/worker_service_status_test.go rename to internal/daemon/cluster/handlers/worker_service_status_test.go index b871584d8..9a9c77d4f 100644 --- a/internal/daemon/controller/handlers/workers/worker_service_status_test.go +++ b/internal/daemon/cluster/handlers/worker_service_status_test.go @@ -1,4 +1,4 @@ -package workers_test +package handlers_test import ( "context" @@ -8,18 +8,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/internal/authtoken" - "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" + "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/store" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/registration" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func TestStatus(t *testing.T) { @@ -31,17 +39,10 @@ func TestStatus(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) serverRepo, _ := servers.NewRepository(rw, rw, kms) - serverRepo.UpsertServer(ctx, &servers.Server{ + serverRepo.UpsertController(ctx, &store.Controller{ PrivateId: "test_controller1", - Type: "controller", Address: "127.0.0.1", }) - serverRepo.UpsertServer(ctx, &servers.Server{ - PrivateId: "test_worker1", - Type: "worker", - Address: "127.0.0.1", - }) - serversRepoFn := func() (*servers.Repository, error) { return serverRepo, nil } @@ -70,7 +71,7 @@ func TestStatus(t *testing.T) { target.WithSessionConnectionLimit(-1), ) - worker1 := session.TestWorker(t, conn, wrapper) + worker1 := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -83,14 +84,14 @@ func TestStatus(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker1.PrivateId, worker1.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) require.NoError(t, err) - s := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) + s := handlers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) require.NotNil(t, s) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PublicId) require.NoError(t, err) cases := []struct { @@ -104,23 +105,31 @@ func TestStatus(t *testing.T) { name: "No Sessions", wantErr: false, req: &pbs.StatusRequest{ - Worker: worker1, + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Name: worker1.GetName(), + Address: worker1.GetAddress(), + }, }, want: &pbs.StatusResponse{ - Controllers: []*servers.Server{ + CalculatedUpstreams: []*pbs.UpstreamServer{ { - PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", }, }, + WorkerId: worker1.PublicId, }, }, { name: "Still Active", wantErr: false, req: &pbs.StatusRequest{ - Worker: worker1, + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Name: worker1.GetName(), + Address: worker1.GetAddress(), + }, Jobs: []*pbs.JobStatus{ { Job: &pbs.Job{ @@ -142,15 +151,37 @@ func TestStatus(t *testing.T) { }, }, want: &pbs.StatusResponse{ - Controllers: []*servers.Server{ + CalculatedUpstreams: []*pbs.UpstreamServer{ { - PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", }, }, + WorkerId: worker1.PublicId, }, }, + { + name: "No Name or keyId", + wantErr: true, + req: &pbs.StatusRequest{ + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Address: worker1.GetAddress(), + }, + }, + wantErrMsg: status.Error(codes.InvalidArgument, "Name and keyId are not set in the request; one is required.").Error(), + }, + { + name: "No Address", + wantErr: true, + req: &pbs.StatusRequest{ + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Name: worker1.GetName(), + }, + }, + wantErrMsg: status.Error(codes.InvalidArgument, "Address is not set but is required.").Error(), + }, } for _, tc := range cases { @@ -160,7 +191,7 @@ func TestStatus(t *testing.T) { got, err := s.Status(ctx, tc.req) if tc.wantErr { require.Error(err) - assert.Nil(got) + assert.Equal(got, &pbs.StatusResponse{}) assert.Equal(tc.wantErrMsg, err.Error()) return } @@ -170,14 +201,15 @@ func TestStatus(t *testing.T) { got, cmpopts.IgnoreUnexported( pbs.StatusResponse{}, - servers.Server{}, + servers.ServerWorkerStatus{}, + pbs.UpstreamServer{}, pbs.JobChangeRequest{}, pbs.Job{}, pbs.Job_SessionInfo{}, pbs.SessionJobInfo{}, pbs.Connection{}, ), - cmpopts.IgnoreFields(servers.Server{}, "CreateTime", "UpdateTime"), + cmpopts.IgnoreFields(servers.ServerWorkerStatus{}, "Tags"), ), ) }) @@ -193,17 +225,10 @@ func TestStatusSessionClosed(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) serverRepo, _ := servers.NewRepository(rw, rw, kms) - serverRepo.UpsertServer(ctx, &servers.Server{ + serverRepo.UpsertController(ctx, &store.Controller{ PrivateId: "test_controller1", - Type: "controller", Address: "127.0.0.1", }) - serverRepo.UpsertServer(ctx, &servers.Server{ - PrivateId: "test_worker1", - Type: "worker", - Address: "127.0.0.1", - }) - serversRepoFn := func() (*servers.Repository, error) { return serverRepo, nil } @@ -232,7 +257,7 @@ func TestStatusSessionClosed(t *testing.T) { target.WithSessionConnectionLimit(-1), ) - worker1 := session.TestWorker(t, conn, wrapper) + worker1 := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -245,7 +270,7 @@ func TestStatusSessionClosed(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker1.PrivateId, worker1.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) sess2 := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -258,13 +283,13 @@ func TestStatusSessionClosed(t *testing.T) { ConnectionLimit: 10, }) tofu2 := session.TestTofu(t) - sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, worker1.PrivateId, worker1.Type, tofu2) + sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, tofu2) require.NoError(t, err) - s := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) + s := handlers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) require.NotNil(t, s) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PublicId) require.NoError(t, err) cases := []struct { @@ -283,7 +308,11 @@ func TestStatusSessionClosed(t *testing.T) { require.NoError(t, err) }, req: &pbs.StatusRequest{ - Worker: worker1, + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Name: worker1.GetName(), + Address: worker1.GetAddress(), + }, Jobs: []*pbs.JobStatus{ { Job: &pbs.Job{ @@ -305,11 +334,10 @@ func TestStatusSessionClosed(t *testing.T) { }, }, want: &pbs.StatusResponse{ - Controllers: []*servers.Server{ + CalculatedUpstreams: []*pbs.UpstreamServer{ { - PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", }, }, JobsRequests: []*pbs.JobChangeRequest{ @@ -326,6 +354,7 @@ func TestStatusSessionClosed(t *testing.T) { RequestType: pbs.CHANGETYPE_CHANGETYPE_UPDATE_STATE, }, }, + WorkerId: worker1.PublicId, }, }, } @@ -350,14 +379,15 @@ func TestStatusSessionClosed(t *testing.T) { got, cmpopts.IgnoreUnexported( pbs.StatusResponse{}, - servers.Server{}, + servers.ServerWorkerStatus{}, + pbs.UpstreamServer{}, pbs.JobChangeRequest{}, pbs.Job{}, pbs.Job_SessionInfo{}, pbs.SessionJobInfo{}, pbs.Connection{}, ), - cmpopts.IgnoreFields(servers.Server{}, "CreateTime", "UpdateTime"), + cmpopts.IgnoreFields(servers.ServerWorkerStatus{}, "Tags"), ), ) }) @@ -373,17 +403,13 @@ func TestStatusDeadConnection(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) serverRepo, _ := servers.NewRepository(rw, rw, kms) - serverRepo.UpsertServer(ctx, &servers.Server{ + serverRepo.UpsertController(ctx, &store.Controller{ PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", - }) - serverRepo.UpsertServer(ctx, &servers.Server{ - PrivateId: "test_worker1", - Type: "worker", Address: "127.0.0.1", }) + worker1 := servers.TestKmsWorker(t, conn, wrapper) + serversRepoFn := func() (*servers.Repository, error) { return serverRepo, nil } @@ -412,8 +438,6 @@ func TestStatusDeadConnection(t *testing.T) { target.WithSessionConnectionLimit(-1), ) - worker1 := session.TestWorker(t, conn, wrapper) - sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -425,7 +449,7 @@ func TestStatusDeadConnection(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker1.PrivateId, worker1.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) sess2 := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -438,20 +462,24 @@ func TestStatusDeadConnection(t *testing.T) { ConnectionLimit: 10, }) tofu2 := session.TestTofu(t) - sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, worker1.PrivateId, worker1.Type, tofu2) + sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, tofu2) require.NoError(t, err) - s := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) + s := handlers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) require.NotNil(t, s) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PublicId) require.NoError(t, err) - deadConn, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker1.PrivateId) + deadConn, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker1.PublicId) require.NoError(t, err) require.NotEqual(t, deadConn.PublicId, connection.PublicId) req := &pbs.StatusRequest{ - Worker: worker1, + WorkerStatus: &servers.ServerWorkerStatus{ + PublicId: worker1.GetPublicId(), + Name: worker1.GetName(), + Address: worker1.GetAddress(), + }, Jobs: []*pbs.JobStatus{ { Job: &pbs.Job{ @@ -473,13 +501,13 @@ func TestStatusDeadConnection(t *testing.T) { }, } want := &pbs.StatusResponse{ - Controllers: []*servers.Server{ + CalculatedUpstreams: []*pbs.UpstreamServer{ { - PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", }, }, + WorkerId: worker1.PublicId, } got, err := s.Status(ctx, req) @@ -489,14 +517,15 @@ func TestStatusDeadConnection(t *testing.T) { got, cmpopts.IgnoreUnexported( pbs.StatusResponse{}, - servers.Server{}, + servers.ServerWorkerStatus{}, + pbs.UpstreamServer{}, pbs.JobChangeRequest{}, pbs.Job{}, pbs.Job_SessionInfo{}, pbs.SessionJobInfo{}, pbs.Connection{}, ), - cmpopts.IgnoreFields(servers.Server{}, "CreateTime", "UpdateTime"), + cmpopts.IgnoreFields(servers.ServerWorkerStatus{}, "Tags"), ), ) @@ -507,3 +536,189 @@ func TestStatusDeadConnection(t *testing.T) { assert.Nil(t, states[0].EndTime) assert.Equal(t, session.StatusClosed, states[0].Status) } + +func TestStatusWorkerWithKeyId(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + serverRepo, _ := servers.NewRepository(rw, rw, kms) + serverRepo.UpsertController(ctx, &store.Controller{ + PrivateId: "test_controller1", + Address: "127.0.0.1", + }) + serversRepoFn := func() (*servers.Repository, error) { + return serverRepo, nil + } + sessionRepoFn := func() (*session.Repository, error) { + return session.NewRepository(rw, rw, kms) + } + connRepoFn := func() (*session.ConnectionRepository, error) { + return session.NewConnectionRepository(ctx, rw, rw, kms) + } + + repo, err := sessionRepoFn() + require.NoError(t, err) + connRepo, err := connRepoFn() + require.NoError(t, err) + + at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget( + ctx, + t, conn, prj.GetPublicId(), "test", + target.WithHostSources([]string{hs.GetPublicId()}), + target.WithSessionConnectionLimit(-1), + ) + + worker1 := servers.TestPkiWorker(t, conn, wrapper) + + rootStorage, err := servers.NewRepositoryStorage(ctx, rw, rw, kms) + require.NoError(t, err) + + _, err = rotation.RotateRootCertificates(ctx, rootStorage) + require.NoError(t, err) + + // Create struct to pass in with workerId that will be passed along to storage + state, err := servers.AttachWorkerIdToState(ctx, worker1.PublicId) + require.NoError(t, err) + + // This happens on the worker + fileStorage, err := file.New(ctx) + require.NoError(t, err) + nodeCreds, err := types.NewNodeCredentials(ctx, fileStorage) + require.NoError(t, err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(ctx) + require.NoError(t, err) + + // The AuthorizeNode request will result in a WorkerAuth record being stored under the workerId + nodeInfo, err := registration.AuthorizeNode(ctx, rootStorage, fetchReq, nodeenrollment.WithState(state)) + require.NoError(t, err) + + sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + ConnectionLimit: 10, + }) + tofu := session.TestTofu(t) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) + require.NoError(t, err) + require.NoError(t, err) + + s := handlers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connRepoFn, new(sync.Map), kms) + require.NotNil(t, s) + + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker1.PublicId) + require.NoError(t, err) + + cases := []struct { + name string + wantErr bool + wantErrMsg string + req *pbs.StatusRequest + want *pbs.StatusResponse + }{ + { + name: "Identify workerID based on keyId in status", + wantErr: false, + req: &pbs.StatusRequest{ + WorkerStatus: &servers.ServerWorkerStatus{ + Address: "someaddress", + KeyId: nodeInfo.Id, + }, + }, + want: &pbs.StatusResponse{ + CalculatedUpstreams: []*pbs.UpstreamServer{ + { + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", + }, + }, + WorkerId: worker1.PublicId, + }, + }, + { + name: "Active keyId Worker", + wantErr: false, + req: &pbs.StatusRequest{ + WorkerStatus: &servers.ServerWorkerStatus{ + KeyId: nodeInfo.Id, + Address: "someaddress", + }, + Jobs: []*pbs.JobStatus{ + { + Job: &pbs.Job{ + Type: pbs.JOBTYPE_JOBTYPE_SESSION, + JobInfo: &pbs.Job_SessionInfo{ + SessionInfo: &pbs.SessionJobInfo{ + SessionId: sess.PublicId, + Status: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE, + Connections: []*pbs.Connection{ + { + ConnectionId: connection.PublicId, + Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED, + }, + }, + }, + }, + }, + }, + }, + }, + want: &pbs.StatusResponse{ + CalculatedUpstreams: []*pbs.UpstreamServer{ + { + Type: pbs.UpstreamServer_TYPE_CONTROLLER, + Address: "127.0.0.1", + }, + }, + WorkerId: worker1.PublicId, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, err := s.Status(ctx, tc.req) + if tc.wantErr { + require.Error(err) + assert.Equal(got, &pbs.StatusResponse{}) + assert.Equal(tc.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.Empty( + cmp.Diff( + tc.want, + got, + cmpopts.IgnoreUnexported( + pbs.StatusResponse{}, + servers.ServerWorkerStatus{}, + pbs.UpstreamServer{}, + pbs.JobChangeRequest{}, + pbs.Job{}, + pbs.Job_SessionInfo{}, + pbs.SessionJobInfo{}, + pbs.Connection{}, + ), + cmpopts.IgnoreFields(servers.ServerWorkerStatus{}, "Tags"), + ), + ) + }) + } +} diff --git a/internal/daemon/cluster/handlers/worker_service_test.go b/internal/daemon/cluster/handlers/worker_service_test.go new file mode 100644 index 000000000..c5357b716 --- /dev/null +++ b/internal/daemon/cluster/handlers/worker_service_test.go @@ -0,0 +1,176 @@ +package handlers_test + +import ( + "context" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestLookupSession(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + serversRepoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, kms) + } + sessionRepoFn := func() (*session.Repository, error) { + return session.NewRepository(rw, rw, kms) + } + connectionRepoFn := func() (*session.ConnectionRepository, error) { + return session.NewConnectionRepository(ctx, rw, rw, kms) + } + + at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) + uId := at.GetIamUserId() + hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] + hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + tar := tcp.TestTarget(ctx, t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + + sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + egressSess := session.TestSession(t, conn, wrapper, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: prj.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + repo, err := sessionRepoFn() + require.NoError(t, err) + + creds := []*pbs.Credential{ + { + Credential: &pbs.Credential_UserPassword{ + UserPassword: &pbs.UserPassword{ + Username: "username", + Password: "password", + }, + }, + }, + { + Credential: &pbs.Credential_UserPassword{ + UserPassword: &pbs.UserPassword{ + Username: "another-username", + Password: "a different password", + }, + }, + }, + } + + workerCreds := make([]session.Credential, 0, len(creds)) + for _, c := range creds { + data, err := proto.Marshal(c) + require.NoError(t, err) + workerCreds = append(workerCreds, data) + } + err = repo.AddSessionCredentials(ctx, egressSess.ScopeId, egressSess.GetPublicId(), workerCreds) + require.NoError(t, err) + + s := handlers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, new(sync.Map), kms) + require.NotNil(t, s) + + cases := []struct { + name string + wantErr bool + wantErrMsg string + want *pbs.LookupSessionResponse + sessionId string + }{ + { + name: "Invalid session id", + sessionId: "s_fakesession", + wantErr: true, + wantErrMsg: "rpc error: code = PermissionDenied desc = Unknown session ID.", + }, + { + name: "Valid", + sessionId: sess.PublicId, + want: &pbs.LookupSessionResponse{ + ConnectionLimit: 1, + ConnectionsLeft: 1, + Version: 1, + Endpoint: sess.Endpoint, + HostId: sess.HostId, + HostSetId: sess.HostSetId, + TargetId: sess.TargetId, + UserId: sess.UserId, + Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING, + }, + }, + { + name: "Valid-with-egress-creds", + sessionId: egressSess.PublicId, + want: &pbs.LookupSessionResponse{ + ConnectionLimit: 1, + ConnectionsLeft: 1, + Version: 1, + Endpoint: egressSess.Endpoint, + HostId: egressSess.HostId, + HostSetId: egressSess.HostSetId, + TargetId: egressSess.TargetId, + UserId: egressSess.UserId, + Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING, + Credentials: creds, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + req := &pbs.LookupSessionRequest{ + SessionId: tc.sessionId, + } + + got, err := s.LookupSession(ctx, req) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + assert.Equal(tc.wantErrMsg, err.Error()) + return + } + assert.Empty( + cmp.Diff( + tc.want, + got, + cmpopts.IgnoreUnexported(pbs.LookupSessionResponse{}, pbs.Credential{}, pbs.UserPassword{}), + cmpopts.IgnoreFields(pbs.LookupSessionResponse{}, "Expiration", "Authorization"), + ), + ) + }) + } +} diff --git a/internal/daemon/controller/common/common.go b/internal/daemon/controller/common/common.go index 4499ec908..f5e7e86f5 100644 --- a/internal/daemon/controller/common/common.go +++ b/internal/daemon/controller/common/common.go @@ -15,17 +15,18 @@ import ( ) type ( - AuthTokenRepoFactory = oidc.AuthTokenRepoFactory - VaultCredentialRepoFactory = func() (*vault.Repository, error) - StaticCredentialRepoFactory = func() (*credstatic.Repository, error) - IamRepoFactory func() (*iam.Repository, error) - OidcAuthRepoFactory = oidc.OidcRepoFactory - PasswordAuthRepoFactory func() (*password.Repository, error) - ServersRepoFactory func() (*servers.Repository, error) - StaticRepoFactory func() (*static.Repository, error) - PluginHostRepoFactory func() (*pluginhost.Repository, error) - HostPluginRepoFactory func() (*hostplugin.Repository, error) - SessionRepoFactory func() (*session.Repository, error) - ConnectionRepoFactory func() (*session.ConnectionRepository, error) - TargetRepoFactory func() (*target.Repository, error) + AuthTokenRepoFactory = oidc.AuthTokenRepoFactory + VaultCredentialRepoFactory = func() (*vault.Repository, error) + StaticCredentialRepoFactory = func() (*credstatic.Repository, error) + IamRepoFactory func() (*iam.Repository, error) + OidcAuthRepoFactory = oidc.OidcRepoFactory + PasswordAuthRepoFactory func() (*password.Repository, error) + ServersRepoFactory func() (*servers.Repository, error) + StaticRepoFactory func() (*static.Repository, error) + PluginHostRepoFactory func() (*pluginhost.Repository, error) + HostPluginRepoFactory func() (*hostplugin.Repository, error) + SessionRepoFactory func() (*session.Repository, error) + ConnectionRepoFactory func() (*session.ConnectionRepository, error) + TargetRepoFactory func() (*target.Repository, error) + WorkerAuthRepoStorageFactory func() (*servers.WorkerAuthRepositoryStorage, error) ) diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 1284c11f1..f4f7ad351 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -28,6 +28,7 @@ import ( "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/scheduler/job" "github.com/hashicorp/boundary/internal/servers" + serversjob "github.com/hashicorp/boundary/internal/servers/job" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/types/scope" @@ -65,19 +66,20 @@ type Controller struct { apiGrpcGatewayTicket string // Repo factory methods - AuthTokenRepoFn common.AuthTokenRepoFactory - VaultCredentialRepoFn common.VaultCredentialRepoFactory - StaticCredentialRepoFn common.StaticCredentialRepoFactory - IamRepoFn common.IamRepoFactory - OidcRepoFn common.OidcAuthRepoFactory - PasswordAuthRepoFn common.PasswordAuthRepoFactory - ServersRepoFn common.ServersRepoFactory - SessionRepoFn common.SessionRepoFactory - ConnectionRepoFn common.ConnectionRepoFactory - StaticHostRepoFn common.StaticRepoFactory - PluginHostRepoFn common.PluginHostRepoFactory - HostPluginRepoFn common.HostPluginRepoFactory - TargetRepoFn common.TargetRepoFactory + AuthTokenRepoFn common.AuthTokenRepoFactory + VaultCredentialRepoFn common.VaultCredentialRepoFactory + StaticCredentialRepoFn common.StaticCredentialRepoFactory + IamRepoFn common.IamRepoFactory + OidcRepoFn common.OidcAuthRepoFactory + PasswordAuthRepoFn common.PasswordAuthRepoFactory + ServersRepoFn common.ServersRepoFactory + SessionRepoFn common.SessionRepoFactory + ConnectionRepoFn common.ConnectionRepoFactory + StaticHostRepoFn common.StaticRepoFactory + PluginHostRepoFn common.PluginHostRepoFactory + HostPluginRepoFn common.HostPluginRepoFactory + TargetRepoFn common.TargetRepoFactory + WorkerAuthRepoStorageFn common.WorkerAuthRepoStorageFactory scheduler *scheduler.Scheduler @@ -299,6 +301,10 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { c.ConnectionRepoFn = func() (*session.ConnectionRepository, error) { return session.NewConnectionRepository(ctx, dbase, dbase, c.kms) } + c.WorkerAuthRepoStorageFn = func() (*servers.WorkerAuthRepositoryStorage, error) { + return servers.NewRepositoryStorage(ctx, dbase, dbase, c.kms) + } + return c, nil } @@ -308,7 +314,9 @@ func (c *Controller) Start() error { event.WriteSysEvent(context.TODO(), op, "already started, skipping") return nil } + c.baseContext, c.baseCancel = context.WithCancel(context.Background()) + if err := c.registerJobs(); err != nil { return fmt.Errorf("error registering jobs: %w", err) } @@ -316,9 +324,9 @@ func (c *Controller) Start() error { return fmt.Errorf("error starting controller listeners: %w", err) } - // Upsert server before starting tickers and scheduler to ensure the server exists - if err := c.upsertServer(c.baseContext); err != nil { - return fmt.Errorf("error upserting server: %w", err) + // Upsert controller before starting tickers and scheduler to ensure the controller exists + if err := c.upsertController(c.baseContext); err != nil { + return fmt.Errorf("error upserting controller: %w", err) } if err := c.scheduler.Start(c.baseContext, c.schedulerWg); err != nil { return fmt.Errorf("error starting scheduler: %w", err) @@ -360,6 +368,9 @@ func (c *Controller) registerJobs() error { if err := session.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms, c.conf.StatusGracePeriodDuration); err != nil { return err } + if err := serversjob.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms); err != nil { + return err + } return nil } diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 54494928b..7f8eed9eb 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -35,6 +35,7 @@ import ( "github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" "github.com/hashicorp/boundary/internal/daemon/controller/internal/metric" "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" @@ -224,6 +225,13 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { } services.RegisterCredentialLibraryServiceServer(s, cl) } + if _, ok := currentServices[services.WorkerService_ServiceDesc.ServiceName]; !ok { + ws, err := workers.NewService(c.baseContext, c.ServersRepoFn, c.IamRepoFn) + if err != nil { + return fmt.Errorf("failed to create worker handler service: %w", err) + } + services.RegisterWorkerServiceServer(s, ws) + } if _, ok := currentServices[services.CredentialService_ServiceDesc.ServiceName]; !ok { c, err := credentials.NewService(c.StaticCredentialRepoFn, c.IamRepoFn) if err != nil { @@ -288,6 +296,9 @@ func registerGrpcGatewayEndpoints(ctx context.Context, gwMux *runtime.ServeMux, if err := services.RegisterCredentialLibraryServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil { return fmt.Errorf("failed to register credential library service handler: %w", err) } + if err := services.RegisterWorkerServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil { + return fmt.Errorf("failed to register worker service handler: %w", err) + } if err := services.RegisterCredentialServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil { return fmt.Errorf("failed to register credential service handler: %w", err) } diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index 0c23f14c6..69a7148ba 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" @@ -63,6 +64,7 @@ var ( resource.Role: roles.CollectionActions, resource.Scope: CollectionActions, resource.User: users.CollectionActions, + resource.Worker: workers.CollectionActions, }, scope.Org.String(): { diff --git a/internal/daemon/controller/handlers/scopes/scope_service_test.go b/internal/daemon/controller/handlers/scopes/scope_service_test.go index 989f45e9d..470df83a6 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service_test.go +++ b/internal/daemon/controller/handlers/scopes/scope_service_test.go @@ -95,6 +95,12 @@ var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ structpb.NewStringValue("list"), }, }, + "workers": { + Values: []*structpb.Value{ + structpb.NewStringValue("create:worker-led"), + structpb.NewStringValue("list"), + }, + }, } var orgAuthorizedCollectionActions = map[string]*structpb.ListValue{ diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 8f7aba70e..7480ece18 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -799,77 +799,27 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession // worker IDs below is used to contain their IDs in the same order. This is // used to fetch tags for filtering. But we avoid allocation unless we // actually need it. - var workers []*pb.WorkerInfo - var workerIds []string - hasWorkerFilter := len(t.GetWorkerFilter()) > 0 - servers, err := serversRepo.ListServers(ctx, servers.ServerTypeWorker) + selectedWorkers, err := serversRepo.ListWorkers(ctx, []string{scope.Global.String()}) if err != nil { return nil, err } - for _, v := range servers { - if hasWorkerFilter { - workerIds = append(workerIds, v.GetPrivateId()) - } - workers = append(workers, &pb.WorkerInfo{Address: v.Address}) - } - if hasWorkerFilter && len(workerIds) > 0 { - finalWorkers := make([]*pb.WorkerInfo, 0, len(workers)) - // Fetch the tags for the given worker IDs - tags, err := serversRepo.ListTagsForServers(ctx, workerIds) - if err != nil { - return nil, err - } - // Build the map for filtering. This is similar to the filter map we - // built from the worker config, but with one extra level: a map of the - // worker's ID to its filter map. - tagMap := make(map[string]map[string][]string) - for _, tag := range tags { - currWorkerMap := tagMap[tag.ServerId] - if currWorkerMap == nil { - currWorkerMap = make(map[string][]string) - tagMap[tag.ServerId] = currWorkerMap - } - currWorkerMap[tag.Key] = append(currWorkerMap[tag.Key], tag.Value) - // We don't need to reinsert after the fact because maps are - // reference types, so we don't need to re-insert into tagMap - } - - // Create the evaluator + if len(t.GetWorkerFilter()) > 0 && len(selectedWorkers) > 0 { eval, err := bexpr.CreateEvaluator(t.GetWorkerFilter()) if err != nil { return nil, err } - - // Iterate through the known worker IDs, and evaluate. If evaluation - // returns true, add to the final worker slice, which is assigned back - // to workers after this. - for i, worker := range workerIds { - filterInput := map[string]interface{}{ - "name": worker, - "tags": tagMap[worker], - } - ok, err := eval.Evaluate(filterInput) - if err != nil && !stderrors.Is(err, pointerstructure.ErrNotFound) { - return nil, handlers.ApiErrorWithCodeAndMessage( - codes.FailedPrecondition, - fmt.Sprintf("Worker filter expression evaluation resulted in error: %s", err)) - } - if ok { - finalWorkers = append(finalWorkers, workers[i]) - } + selectedWorkers, err = workerList(selectedWorkers).filtered(eval) + if err != nil { + return nil, err } - workers = finalWorkers } - if len(workers) == 0 { + + if len(selectedWorkers) == 0 { return nil, handlers.ApiErrorWithCodeAndMessage( codes.FailedPrecondition, "No workers are available to handle this session, or all have been filtered.") } - workerAddresses := make([]string, 0, len(workers)) - for _, worker := range workers { - workerAddresses = append(workerAddresses, worker.GetAddress()) - } requestedId := req.GetHostId() staticHostRepo, err := s.staticHostRepoFn() @@ -992,7 +942,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, workerAddresses) + sess, privKey, err := sessionRepo.CreateSession(ctx, wrapper, sess, workerList(selectedWorkers).addresses()) if err != nil { return nil, err } @@ -1091,7 +1041,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession PrivateKey: privKey, HostId: chosenEndpoint.HostId, Endpoint: endpointUrl.String(), - WorkerInfo: workers, + WorkerInfo: workerList(selectedWorkers).workerInfos(), ConnectionLimit: t.GetSessionConnectionLimit(), } marshaledSad, err := proto.Marshal(sad) @@ -1966,3 +1916,46 @@ func validateAuthorizeSessionRequest(req *pbs.AuthorizeSessionRequest) error { } return nil } + +// workerList is a helper type to make the selection of workers clearer and more declarative. +type workerList []*servers.Worker + +// addresses converts the slice of workers to a slice of their addresses +func (w workerList) addresses() []string { + ret := make([]string, 0, len(w)) + for _, worker := range w { + ret = append(ret, worker.GetAddress()) + } + return ret +} + +// workerInfos converts the slice of workers to a slice of their workerInfo protos +func (w workerList) workerInfos() []*pb.WorkerInfo { + ret := make([]*pb.WorkerInfo, 0, len(w)) + for _, worker := range w { + ret = append(ret, &pb.WorkerInfo{Address: worker.GetAddress()}) + } + return ret +} + +// filtered returns a new workerList where all elements contained in it are the +// ones which from the original workerList that pass the evaluator's evaluation. +func (w workerList) filtered(eval *bexpr.Evaluator) (workerList, error) { + var ret []*servers.Worker + for _, worker := range w { + filterInput := map[string]interface{}{ + "name": worker.GetName(), + "tags": worker.CanonicalTags(), + } + ok, err := eval.Evaluate(filterInput) + if err != nil && !stderrors.Is(err, pointerstructure.ErrNotFound) { + return nil, handlers.ApiErrorWithCodeAndMessage( + codes.FailedPrecondition, + fmt.Sprintf("Worker filter expression evaluation resulted in error: %s", err)) + } + if ok { + ret = append(ret, worker) + } + } + return ret, nil +} diff --git a/internal/daemon/controller/handlers/targets/target_service_test.go b/internal/daemon/controller/handlers/targets/target_service_test.go new file mode 100644 index 000000000..e7f635326 --- /dev/null +++ b/internal/daemon/controller/handlers/targets/target_service_test.go @@ -0,0 +1,79 @@ +package targets + +import ( + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/types/scope" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" + "github.com/hashicorp/go-bexpr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorkerList_Addresses(t *testing.T) { + addresses := []string{ + "test1", + "test2", + "test3", + "test4", + } + var workerInfos []*pb.WorkerInfo + var tested workerList + for _, a := range addresses { + workerInfos = append(workerInfos, &pb.WorkerInfo{Address: a}) + tested = append(tested, servers.NewWorker(scope.Global.String(), + servers.WithName(a), + servers.WithAddress(a))) + } + assert.Equal(t, addresses, tested.addresses()) + assert.Equal(t, workerInfos, tested.workerInfos()) +} + +func TestWorkerList_Filter(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + var workers []*servers.Worker + for i := 0; i < 5; i++ { + switch { + case i%2 == 0: + workers = append(workers, servers.TestKmsWorker(t, conn, wrapper, + servers.WithName(fmt.Sprintf("test_worker_%d", i)), + servers.WithWorkerTags(&servers.Tag{ + Key: fmt.Sprintf("key%d", i), + Value: fmt.Sprintf("value%d", i), + }))) + default: + workers = append(workers, servers.TestPkiWorker(t, conn, wrapper, + servers.WithName(fmt.Sprintf("test_worker_%d", i)), + servers.WithWorkerTags(&servers.Tag{ + Key: "key", + Value: "configvalue", + }))) + } + } + + { + ev, err := bexpr.CreateEvaluator(`"/name" matches "test_worker_[13]" and "configvalue2" in "/tags/key"`) + require.NoError(t, err) + got, err := workerList(workers).filtered(ev) + require.NoError(t, err) + assert.Len(t, got, 0) + } + { + ev, err := bexpr.CreateEvaluator(`"/name" matches "test_worker_[12]" and "configvalue" in "/tags/key"`) + require.NoError(t, err) + got, err := workerList(workers).filtered(ev) + require.NoError(t, err) + assert.Len(t, got, 1) + } + { + ev, err := bexpr.CreateEvaluator(`"configvalue" in "/tags/key"`) + require.NoError(t, err) + got, err := workerList(workers).filtered(ev) + require.NoError(t, err) + assert.Len(t, got, 2) + } +} diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index cdd60632a..d2a8185ab 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -16,12 +16,12 @@ import ( "github.com/hashicorp/boundary/internal/credential" credstatic "github.com/hashicorp/boundary/internal/credential/static" "github.com/hashicorp/boundary/internal/credential/vault" + cluster "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets" - "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" @@ -2371,9 +2371,6 @@ func TestAuthorizeSession(t *testing.T) { sessionRepoFn := func() (*session.Repository, error) { return session.NewRepository(rw, rw, kms) } - connectionRepoFn := func() (*session.ConnectionRepository, error) { - return session.NewConnectionRepository(ctx, rw, rw, kms) - } staticRepo, err := static.NewRepository(rw, rw, kms) require.NoError(t, err) staticHostRepoFn := func() (*static.Repository, error) { @@ -2532,14 +2529,7 @@ func TestAuthorizeSession(t *testing.T) { require.NoError(t, err) // Tell our DB that there is a worker ready to serve the data - workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, &sync.Map{}, kms) - _, err = workerService.Status(ctx, &spbs.StatusRequest{ - Worker: &spb.Server{ - PrivateId: "testworker", - Address: "localhost:8457", - }, - }) - require.NoError(t, err) + servers.TestKmsWorker(t, conn, wrapper) asRes1, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ Id: tar.GetPublicId(), @@ -2641,9 +2631,6 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { sessionRepoFn := func() (*session.Repository, error) { return session.NewRepository(rw, rw, kms) } - connectionRepoFn := func() (*session.ConnectionRepository, error) { - return session.NewConnectionRepository(ctx, rw, rw, kms) - } staticHostRepoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } @@ -2887,14 +2874,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { require.NoError(t, err) // Tell our DB that there is a worker ready to serve the data - workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, &sync.Map{}, kms) - _, err = workerService.Status(ctx, &spbs.StatusRequest{ - Worker: &spb.Server{ - PrivateId: "testworker", - Address: "localhost:8457", - }, - }) - require.NoError(t, err) + servers.TestKmsWorker(t, conn, wrapper) asRes, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ Id: tar.GetPublicId(), @@ -3002,11 +2982,12 @@ func TestAuthorizeSession_Errors(t *testing.T) { store := vault.TestCredentialStore(t, conn, wrapper, proj.GetPublicId(), v.Addr, tok, sec.Auth.Accessor) workerExists := func(tar target.Target) (version uint32) { - workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, &sync.Map{}, kms) + workerService := cluster.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, &sync.Map{}, kms) _, err := workerService.Status(context.Background(), &spbs.StatusRequest{ - Worker: &spb.Server{ - PrivateId: "testworker", - Address: "localhost:123", + WorkerStatus: &spb.ServerWorkerStatus{ + PublicId: "w_1234567890", + Name: "w_1234567890", + Address: "localhost:123", }, }) require.NoError(t, err) diff --git a/internal/daemon/controller/handlers/workers/worker_service.go b/internal/daemon/controller/handlers/workers/worker_service.go index bd44def7a..f2c7726b7 100644 --- a/internal/daemon/controller/handlers/workers/worker_service.go +++ b/internal/daemon/controller/handlers/workers/worker_service.go @@ -2,432 +2,616 @@ package workers import ( "context" - "errors" "fmt" - "sync" - "time" + "strings" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/common" + "github.com/hashicorp/boundary/internal/daemon/controller/common/scopeids" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" - pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" - "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/observability/event" + "github.com/hashicorp/boundary/internal/errors" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers" - "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" - "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets" - "github.com/hashicorp/go-bexpr" + "github.com/hashicorp/boundary/internal/types/scope" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mr-tron/base58" "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/wrapperspb" ) -type workerServiceServer struct { - pbs.UnimplementedServerCoordinationServiceServer - pbs.UnimplementedSessionServiceServer - - serversRepoFn common.ServersRepoFactory - sessionRepoFn common.SessionRepoFactory - connectionRepoFn common.ConnectionRepoFactory - updateTimes *sync.Map - kms *kms.Kms -} - -func NewWorkerServiceServer( - serversRepoFn common.ServersRepoFactory, - sessionRepoFn common.SessionRepoFactory, - connectionRepoFn common.ConnectionRepoFactory, - updateTimes *sync.Map, - kms *kms.Kms, -) *workerServiceServer { - return &workerServiceServer{ - serversRepoFn: serversRepoFn, - sessionRepoFn: sessionRepoFn, - connectionRepoFn: connectionRepoFn, - updateTimes: updateTimes, - kms: kms, - } -} +const ( + PkiWorkerType = "pki" + KmsWorkerType = "kms" +) var ( - _ pbs.SessionServiceServer = &workerServiceServer{} - _ pbs.ServerCoordinationServiceServer = &workerServiceServer{} + maskManager handlers.MaskManager + + // IdActions contains the set of actions that can be performed on + // individual resources + IdActions = action.ActionSet{ + action.NoOp, + action.Read, + action.Update, + action.Delete, + } + + // CollectionActions contains the set of actions that can be performed on + // this collection + CollectionActions = action.ActionSet{ + action.CreateWorkerLed, + action.List, + } ) -func (ws *workerServiceServer) Status(ctx context.Context, req *pbs.StatusRequest) (*pbs.StatusResponse, error) { - const op = "workers.(workerServiceServer).Status" - // TODO: on the worker, if we get errors back from this repeatedly, do we - // terminate all sessions since we can't know if they were canceled? - ws.updateTimes.Store(req.Worker.PrivateId, time.Now()) - serverRepo, err := ws.serversRepoFn() +func init() { + var err error + if maskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.Worker{}}, handlers.MaskSource{&pb.Worker{}}); err != nil { + panic(err) + } +} + +// Service handles request as described by the pbs.WorkerServiceServer interface. +type Service struct { + pbs.UnimplementedWorkerServiceServer + + repoFn common.ServersRepoFactory + iamRepoFn common.IamRepoFactory +} + +// NewService returns a worker service which handles worker related requests to boundary. +func NewService(ctx context.Context, repo common.ServersRepoFactory, iamRepoFn common.IamRepoFactory) (Service, error) { + const op = "workers.NewService" + if repo == nil { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing servers repository") + } + if iamRepoFn == nil { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") + } + return Service{repoFn: repo, iamRepoFn: iamRepoFn}, nil +} + +var _ pbs.WorkerServiceServer = Service{} + +// ListWorkers implements the interface pbs.WorkerServiceServer. +func (s Service) ListWorkers(ctx context.Context, req *pbs.ListWorkersRequest) (*pbs.ListWorkersResponse, error) { + if err := validateListRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetScopeId(), action.List) + 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 + // may have authorization on downstream scopes. Or, if they've not + // authenticated, still process in case u_anon has permissions. + if (authResults.Error == handlers.ForbiddenError() || authResults.Error == handlers.UnauthenticatedError()) && + req.GetRecursive() && + authResults.AuthenticationFinished { + } else { + return nil, authResults.Error + } + } + + scopeIds, scopeInfoMap, err := scopeids.GetListingScopeIds( + ctx, s.iamRepoFn, authResults, req.GetScopeId(), resource.Worker, req.GetRecursive()) if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error getting servers repo")) - return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to store worker status: %v", err) + return nil, err } - sessRepo, err := ws.sessionRepoFn() - connectionRepo, err := ws.connectionRepoFn() + // If no scopes match, return an empty response + if len(scopeIds) == 0 { + return &pbs.ListWorkersResponse{}, nil + } + + ul, err := s.listFromRepo(ctx, scopeIds) if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error getting sessions repo")) - return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error acquiring repo to query session status: %v", err) + return nil, err } - req.Worker.Type = resource.Worker.String() - controllers, _, err := serverRepo.UpsertServer(ctx, req.Worker, servers.WithUpdateTags(req.GetUpdateTags())) + if len(ul) == 0 { + return &pbs.ListWorkersResponse{}, nil + } + + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error storing worker status")) - return &pbs.StatusResponse{}, status.Errorf(codes.Internal, "Error storing worker status: %v", err) + return nil, err } - ret := &pbs.StatusResponse{ - Controllers: controllers, + finalItems := make([]*pb.Worker, 0, len(ul)) + res := perms.Resource{ + Type: resource.Worker, + } + for _, item := range ul { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { + continue + } + + outputFields := authResults.FetchOutputFields(res, action.List).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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + item, err := toProto(ctx, item, outputOpts...) + if err != nil { + return nil, err + } + + if filter.Match(item) { + finalItems = append(finalItems, item) + } + } + return &pbs.ListWorkersResponse{Items: finalItems}, nil +} + +// GetWorker implements the interface pbs.WorkerServiceServer. +func (s Service) GetWorker(ctx context.Context, req *pbs.GetWorkerRequest) (*pbs.GetWorkerResponse, error) { + const op = "workers.(Service).GetWorker" + + if err := validateGetRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetId(), action.Read) + if authResults.Error != nil { + return nil, authResults.Error + } + w, err := s.getFromRepo(ctx, req.GetId()) + if err != nil { + return nil, err } - stateReport := make([]session.StateReport, 0, len(req.GetJobs())) + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "no request context found") + } - for _, jobStatus := range req.GetJobs() { - switch jobStatus.Job.GetType() { - case pbs.JOBTYPE_JOBTYPE_SESSION: - si := jobStatus.GetJob().GetSessionInfo() - if si == nil { - return nil, status.Error(codes.Internal, "Error getting session info at status time") - } + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, w.GetPublicId(), IdActions).Strings())) + } - switch si.Status { - case pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING, - pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED: - // No need to see about canceling anything - continue - } + item, err := toProto(ctx, w, outputOpts...) + if err != nil { + return nil, err + } - sr := session.StateReport{ - SessionId: si.GetSessionId(), - ConnectionIds: make([]string, 0, len(si.GetConnections())), - } - for _, conn := range si.GetConnections() { - switch conn.Status { - case pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_AUTHORIZED, - pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED: - sr.ConnectionIds = append(sr.ConnectionIds, conn.GetConnectionId()) + return &pbs.GetWorkerResponse{Item: item}, nil +} + +// CreateWorker implements the interface pbs.WorkerServiceServer. +func (s Service) CreateWorkerLed(ctx context.Context, req *pbs.CreateWorkerLedRequest) (*pbs.CreateWorkerLedResponse, error) { + const op = "workers.(Service).CreateWorkerLed" + + if err := validateCreateRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.CreateWorkerLed) + if authResults.Error != nil { + return nil, authResults.Error + } + reqBytes, err := base58.FastBase58Decoding(req.GetItem().WorkerGeneratedAuthToken.GetValue()) + if err != nil { + return nil, fmt.Errorf("%s: error decoding node_credentials_token: %w", op, err) + } + // Decode the proto into the request + creds := new(types.FetchNodeCredentialsRequest) + if err := proto.Unmarshal(reqBytes, creds); err != nil { + return nil, fmt.Errorf("%s: error unmarshaling node_credentials_token: %w", op, err) + } + created, err := s.createInRepo(ctx, req.GetItem(), creds) + if err != nil { + return nil, fmt.Errorf("%s: error creating worker: %w", op, err) + } + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "no request context found") + } + + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, created.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, created, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.CreateWorkerLedResponse{Item: item}, nil +} + +// DeleteWorker implements the interface pbs.WorkerServiceServer. +func (s Service) DeleteWorker(ctx context.Context, req *pbs.DeleteWorkerRequest) (*pbs.DeleteWorkerResponse, error) { + if err := validateDeleteRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetId(), action.Delete) + if authResults.Error != nil { + return nil, authResults.Error + } + _, err := s.deleteFromRepo(ctx, req.GetId()) + if err != nil { + return nil, err + } + return nil, nil +} + +// UpdateWorker implements the interface pbs.WorkerServiceServer. +func (s Service) UpdateWorker(ctx context.Context, req *pbs.UpdateWorkerRequest) (*pbs.UpdateWorkerResponse, error) { + const op = "workers.(Service).UpdateWorker" + + if err := validateUpdateRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetId(), action.Update) + if authResults.Error != nil { + return nil, authResults.Error + } + w, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, err + } + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "no request context found") + } + + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, w.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, w, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateWorkerResponse{Item: item}, nil +} + +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*servers.Worker, error) { + repo, err := s.repoFn() + if err != nil { + return nil, err + } + wl, err := repo.ListWorkers(ctx, scopeIds, servers.WithLiveness(-1)) + if err != nil { + return nil, err + } + return wl, nil +} + +func (s Service) getFromRepo(ctx context.Context, id string) (*servers.Worker, error) { + repo, err := s.repoFn() + if err != nil { + return nil, err + } + w, err := repo.LookupWorker(ctx, id) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, handlers.NotFoundErrorf("Worker %q doesn't exist.", id) + } + return nil, err + } + if w == nil { + return nil, handlers.NotFoundErrorf("Worker %q doesn't exist.", id) + } + return w, nil +} + +func (s Service) createInRepo(ctx context.Context, worker *pb.Worker, creds *types.FetchNodeCredentialsRequest) (*servers.Worker, error) { + const op = "workers.(Service).createInRepo" + repo, err := s.repoFn() + if err != nil { + return nil, err + } + newWorker := servers.NewWorker( + worker.GetScopeId(), + servers.WithName(worker.GetName().GetValue()), + servers.WithDescription(worker.GetDescription().GetValue()), + ) + retWorker, err := repo.CreateWorker(ctx, newWorker, servers.WithFetchNodeCredentialsRequest(creds)) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker")) + } + return retWorker, nil +} + +func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { + const op = "workers.(Service).deleteFromRepo" + repo, err := s.repoFn() + if err != nil { + return false, err + } + rows, err := repo.DeleteWorker(ctx, id) + if err != nil { + if errors.IsNotFoundError(err) { + return false, nil + } + return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete worker")) + } + return rows > 0, nil +} + +func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Worker) (*servers.Worker, error) { + const op = "workers.(Service).updateInRepo" + var opts []servers.Option + if desc := item.GetDescription(); desc != nil { + opts = append(opts, servers.WithDescription(desc.GetValue())) + } + if name := item.GetName(); name != nil { + opts = append(opts, servers.WithName(name.GetValue())) + } + w := servers.NewWorker(scopeId, opts...) + w.PublicId = id + dbMask := maskManager.Translate(mask) + if len(dbMask) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + repo, err := s.repoFn() + if err != nil { + return nil, err + } + out, rowsUpdated, err := repo.UpdateWorker(ctx, w, item.GetVersion(), dbMask) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update worker")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Worker %q doesn't exist or incorrect version provided.", id) + } + return out, nil +} + +func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { + res := auth.VerifyResults{} + repo, err := s.repoFn() + if err != nil { + res.Error = err + return res + } + + var parentId string + opts := []auth.Option{auth.WithType(resource.Worker), auth.WithAction(a)} + switch a { + case action.List, action.CreateWorkerLed: + parentId = id + default: + w, err := repo.LookupWorker(ctx, id) + if err != nil { + res.Error = err + return res + } + if w == nil { + res.Error = handlers.NotFoundError() + return res + } + parentId = w.GetScopeId() + opts = append(opts, auth.WithId(id)) + } + opts = append(opts, auth.WithScopeId(parentId)) + return auth.Verify(ctx, opts...) +} + +func toProto(ctx context.Context, in *servers.Worker, opt ...handlers.Option) (*pb.Worker, error) { + const op = "workers.toProto" + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building worker proto") + } + outputFields := *opts.WithOutputFields + + out := pb.Worker{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() + } + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() + } + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { + out.Description = wrapperspb.String(in.GetDescription()) + } + if outputFields.Has(globals.NameField) && in.GetName() != "" { + out.Name = wrapperspb.String(in.GetName()) + } + if outputFields.Has(globals.CreatedTimeField) { + out.CreatedTime = in.GetCreateTime().GetTimestamp() + } + if outputFields.Has(globals.UpdatedTimeField) { + out.UpdatedTime = in.GetUpdateTime().GetTimestamp() + } + if outputFields.Has(globals.VersionField) { + out.Version = in.GetVersion() + } + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope + } + if outputFields.Has(globals.AuthorizedActionsField) { + out.AuthorizedActions = opts.WithAuthorizedActions + if in.Type == KmsWorkerType && out.AuthorizedActions != nil { + // KMS workers cannot be updated through the API + allActions := out.AuthorizedActions + out.AuthorizedActions = make([]string, 0, len(allActions)) + for _, act := range allActions { + if act != action.Update.String() { + out.AuthorizedActions = append(out.AuthorizedActions, act) } } - stateReport = append(stateReport, sr) + } + } + if outputFields.Has(globals.AddressField) && in.GetAddress() != "" { + out.Address = in.GetAddress() + } + if outputFields.Has(globals.TypeField) && in.GetType() != "" { + out.Type = in.GetType() + } + if outputFields.Has(globals.LastStatusTimeField) { + out.LastStatusTime = in.GetLastStatusTime().GetTimestamp() + } + if outputFields.Has(globals.ActiveConnectionCountField) { + out.ActiveConnectionCount = &wrapperspb.UInt32Value{Value: in.ActiveConnectionCount()} + } + if outputFields.Has(globals.ConfigTagsField) && len(in.GetConfigTags()) > 0 { + var err error + out.ConfigTags, err = tagsToMapProto(in.GetConfigTags()) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error preparing config tags proto")) + } + } + if outputFields.Has(globals.CanonicalTagsField) && len(in.CanonicalTags()) > 0 { + var err error + out.CanonicalTags, err = tagsToMapProto(in.CanonicalTags()) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error preparing canonical tags proto")) } } - notActive, err := session.WorkerStatusReport(ctx, sessRepo, connectionRepo, req.Worker.PrivateId, stateReport) - if err != nil { - return nil, status.Errorf(codes.Internal, "Error comparing state of sessions for worker: %s: %v", req.Worker.PrivateId, err) - } - for _, na := range notActive { - var connChanges []*pbs.Connection - for _, connId := range na.ConnectionIds { - connChanges = append(connChanges, &pbs.Connection{ - ConnectionId: connId, - Status: session.StatusClosed.ProtoVal(), - }) - } - ret.JobsRequests = append(ret.JobsRequests, &pbs.JobChangeRequest{ - Job: &pbs.Job{ - Type: pbs.JOBTYPE_JOBTYPE_SESSION, - JobInfo: &pbs.Job_SessionInfo{ - SessionInfo: &pbs.SessionJobInfo{ - SessionId: na.SessionId, - Status: na.Status.ProtoVal(), - Connections: connChanges, - }, - }, - }, - RequestType: pbs.CHANGETYPE_CHANGETYPE_UPDATE_STATE, - }) - } + return &out, nil +} +func tagsToMapProto(in map[string][]string) (map[string]*structpb.ListValue, error) { + b := make(map[string][]interface{}) + for k, v := range in { + result := make([]interface{}, 0, len(v)) + for _, t := range v { + result = append(result, t) + } + b[k] = result + } + ret := make(map[string]*structpb.ListValue) + var err error + for k, v := range b { + ret[k], err = structpb.NewList(v) + if err != nil { + return nil, err + } + } return ret, nil } -func (ws *workerServiceServer) LookupSession(ctx context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) { - const op = "workers.(workerServiceServer).LookupSession" - - sessRepo, err := ws.sessionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "Error getting session repo: %v", err) - } - - sessionInfo, authzSummary, err := sessRepo.LookupSession(ctx, req.GetSessionId()) - if err != nil { - return nil, status.Errorf(codes.Internal, "Error looking up session: %v", err) - } - if sessionInfo == nil { - return nil, status.Error(codes.PermissionDenied, "Unknown session ID.") - } - if len(sessionInfo.States) == 0 { - return nil, status.Error(codes.Internal, "Empty session states during lookup.") - } - - if sessionInfo.WorkerFilter != "" { - if req.ServerId == "" { - event.WriteError(ctx, op, errors.New("worker filter enabled for session but got no server ID from worker")) - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Did not receive server ID when looking up session but filtering is enabled: %v", err) - } - serversRepo, err := ws.serversRepoFn() - if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error getting servers repo")) - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error acquiring server repo when looking up session: %v", err) - } - tags, err := serversRepo.ListTagsForServers(ctx, []string{req.ServerId}) - if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error looking up tags for server", "server_id", req.ServerId)) - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error looking up tags for server: %v", err) - } - // Build the map for filtering. - tagMap := make(map[string][]string) - for _, tag := range tags { - tagMap[tag.Key] = append(tagMap[tag.Key], tag.Value) - // We don't need to reinsert after the fact because maps are - // reference types, so we don't need to re-insert into tagMap - } - - // Create the evaluator - eval, err := bexpr.CreateEvaluator(sessionInfo.WorkerFilter) - if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error creating worker filter evaluator", "server_id", req.ServerId)) - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, "Error creating worker filter evaluator: %v", err) - } - filterInput := map[string]interface{}{ - "name": req.ServerId, - "tags": tagMap, - } - ok, err := eval.Evaluate(filterInput) - if err != nil { - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, - fmt.Sprintf("Worker filter expression evaluation resulted in error: %s", err)) - } - if !ok { - return nil, handlers.ApiErrorWithCodeAndMessage( - codes.FailedPrecondition, - "Worker filter expression precludes this worker from serving this session") - } - } - - creds, err := sessRepo.ListSessionCredentials(ctx, sessionInfo.ScopeId, sessionInfo.PublicId) - if err != nil { - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, - fmt.Sprintf("Error retrieving session credentials: %s", err)) - } - var workerCreds []*pbs.Credential - for _, c := range creds { - m := &pbs.Credential{} - err = proto.Unmarshal(c, m) - if err != nil { - return &pbs.LookupSessionResponse{}, status.Errorf(codes.Internal, - fmt.Sprintf("Error unmarshaling credentials: %s", err)) - } - workerCreds = append(workerCreds, m) - } - - resp := &pbs.LookupSessionResponse{ - Authorization: &targets.SessionAuthorizationData{ - SessionId: sessionInfo.GetPublicId(), - Certificate: sessionInfo.Certificate, - }, - Status: sessionInfo.States[0].Status.ProtoVal(), - Version: sessionInfo.Version, - TofuToken: string(sessionInfo.TofuToken), - Endpoint: sessionInfo.Endpoint, - Expiration: sessionInfo.ExpirationTime.Timestamp, - ConnectionLimit: sessionInfo.ConnectionLimit, - ConnectionsLeft: authzSummary.ConnectionLimit, - HostId: sessionInfo.HostId, - HostSetId: sessionInfo.HostSetId, - TargetId: sessionInfo.TargetId, - UserId: sessionInfo.UserId, - Credentials: workerCreds, - } - if resp.ConnectionsLeft != -1 { - resp.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount) - } - - wrapper, err := ws.kms.GetWrapper(ctx, sessionInfo.ScopeId, 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 +// 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 +// * All required parameters are set +// * There are no conflicting parameters provided +func validateGetRequest(req *pbs.GetWorkerRequest) error { + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, servers.WorkerPrefix) } -func (ws *workerServiceServer) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) { - const op = "workers.(workerServiceServer).CancelSession" - - sessRepo, err := ws.sessionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) +func validateListRequest(req *pbs.ListWorkersRequest) error { + badFields := map[string]string{} + if req.GetScopeId() != scope.Global.String() { + badFields["scope_id"] = "Must be 'global' when listing." } - - ses, _, err := sessRepo.LookupSession(ctx, req.GetSessionId()) - if err != nil { - return nil, err + if _, err := handlers.NewFilter(req.GetFilter()); err != nil { + badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err) } - - ses, err = sessRepo.CancelSession(ctx, req.GetSessionId(), ses.Version) - if err != nil { - return nil, err + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) } - - return &pbs.CancelSessionResponse{ - Status: ses.States[0].Status.ProtoVal(), - }, nil + return nil } -func (ws *workerServiceServer) ActivateSession(ctx context.Context, req *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) { - const op = "workers.(workerServiceServer).ActivateSession" - - sessRepo, err := ws.sessionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) - } - - sessionInfo, sessionStates, err := sessRepo.ActivateSession( - ctx, - req.GetSessionId(), - req.GetVersion(), - req.GetWorkerId(), - resource.Worker.String(), - []byte(req.GetTofuToken())) - if err != nil { - return nil, status.Errorf(codes.Internal, "error looking up session: %v", err) - } - if sessionInfo == nil { - return nil, status.Error(codes.PermissionDenied, "Unknown session ID.") - } - if len(sessionStates) == 0 { - return nil, status.Error(codes.Internal, "Invalid session state in activate response.") - } - - return &pbs.ActivateSessionResponse{ - Status: sessionStates[0].Status.ProtoVal(), - }, nil +func validateDeleteRequest(req *pbs.DeleteWorkerRequest) error { + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, servers.WorkerPrefix) } -func (ws *workerServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) { - const op = "workers.(workerServiceServer).AuthorizeConnection" - connectionRepo, err := ws.connectionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) - } - - sessionRepo, err := ws.sessionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) - } - - connectionInfo, connStates, authzSummary, err := session.AuthorizeConnection(ctx, sessionRepo, connectionRepo, req.GetSessionId(), req.GetWorkerId()) - if err != nil { - return nil, err - } - if connectionInfo == nil { - return nil, status.Error(codes.Internal, "Invalid authorize connection response.") - } - if len(connStates) == 0 { - return nil, status.Error(codes.Internal, "Invalid connection state in authorize response.") - } - - ret := &pbs.AuthorizeConnectionResponse{ - ConnectionId: connectionInfo.GetPublicId(), - Status: connStates[0].Status.ProtoVal(), - ConnectionsLeft: authzSummary.ConnectionLimit, - } - if ret.ConnectionsLeft != -1 { - ret.ConnectionsLeft -= int32(authzSummary.CurrentConnectionCount) - } - - return ret, nil +func validateUpdateRequest(req *pbs.UpdateWorkerRequest) error { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { + badFields := map[string]string{} + if req.GetItem().GetAddress() != "" { + badFields[globals.AddressField] = "This is a read only field." + } + if req.GetItem().GetCanonicalTags() != nil { + badFields[globals.CanonicalAddressField] = "This is a read only field." + } + if req.GetItem().GetConfigTags() != nil { + badFields[globals.TagsField] = "This is a read only field." + } + nameString := req.GetItem().GetName().String() + if !strutil.Printable(nameString) { + badFields[globals.NameField] = "Contains non-printable characters" + } + if strings.ToLower(nameString) != nameString { + badFields[globals.NameField] = "Must be all lowercase." + } + descriptionString := req.GetItem().GetDescription().String() + if !strutil.Printable(descriptionString) { + badFields[globals.DescriptionField] = "Contains non-printable characters." + } + return badFields + }, servers.WorkerPrefix) } -func (ws *workerServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) { - const op = "workers.(workerServiceServer).ConnectConnection" - connRepo, err := ws.connectionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) - } - - connectionInfo, connStates, err := connRepo.ConnectConnection(ctx, session.ConnectWith{ - ConnectionId: req.GetConnectionId(), - ClientTcpAddress: req.GetClientTcpAddress(), - ClientTcpPort: req.GetClientTcpPort(), - EndpointTcpAddress: req.GetEndpointTcpAddress(), - EndpointTcpPort: req.GetEndpointTcpPort(), - UserClientIp: req.GetUserClientIp(), +func validateCreateRequest(req *pbs.CreateWorkerLedRequest) error { + return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { + const ( + mustBeGlobalMsg = "Must be 'global'" + cannotBeEmptyMsg = "Cannot be empty." + readOnlyFieldMsg = "This is a read only field." + ) + badFields := map[string]string{} + if scope.Global.String() != req.GetItem().GetScopeId() { + badFields[globals.ScopeIdField] = mustBeGlobalMsg + } + // FIXME: in the future, we won't require this token since we'll support + // the server led flow where a token is returned when a worker is created. + if req.GetItem().WorkerGeneratedAuthToken == nil { + badFields[globals.WorkerGeneratedAuthTokenField] = cannotBeEmptyMsg + } + if req.GetItem().Address != "" { + badFields[globals.CanonicalAddressField] = readOnlyFieldMsg + } + if req.GetItem().CanonicalTags != nil { + badFields[globals.CanonicalTagsField] = readOnlyFieldMsg + } + if req.GetItem().ConfigTags != nil { + badFields[globals.ConfigTagsField] = readOnlyFieldMsg + } + if req.GetItem().LastStatusTime != nil { + badFields[globals.LastStatusTimeField] = readOnlyFieldMsg + } + if req.GetItem().AuthorizedActions != nil { + badFields[globals.AuthorizedActionsField] = readOnlyFieldMsg + } + nameString := req.GetItem().GetName().String() + if !strutil.Printable(nameString) { + badFields[globals.NameField] = "Name contains non-printable characters." + } + if strings.ToLower(nameString) != nameString { + badFields[globals.NameField] = "Name must be all lowercase." + } + descriptionString := req.GetItem().GetDescription().String() + if !strutil.Printable(descriptionString) { + badFields[globals.DescriptionField] = "Description contains non-printable characters." + } + return badFields }) - if err != nil { - return nil, err - } - if connectionInfo == nil { - return nil, status.Error(codes.Internal, "Invalid connect connection response.") - } - - return &pbs.ConnectConnectionResponse{ - Status: connStates[0].Status.ProtoVal(), - }, nil -} - -func (ws *workerServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) { - const op = "workers.(workerServiceServer).CloseConnection" - numCloses := len(req.GetCloseRequestData()) - if numCloses == 0 { - return &pbs.CloseConnectionResponse{}, nil - } - - closeWiths := make([]session.CloseWith, 0, numCloses) - closeIds := make([]string, 0, numCloses) - - for _, v := range req.GetCloseRequestData() { - closeIds = append(closeIds, v.GetConnectionId()) - closeWiths = append(closeWiths, session.CloseWith{ - ConnectionId: v.GetConnectionId(), - BytesUp: v.GetBytesUp(), - BytesDown: v.GetBytesDown(), - ClosedReason: session.ClosedReason(v.GetReason()), - }) - } - connRepo, err := ws.connectionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting connection repo: %v", err) - } - - sessRepo, err := ws.sessionRepoFn() - if err != nil { - return nil, status.Errorf(codes.Internal, "error getting session repo: %v", err) - } - - closeInfos, err := session.CloseConnections(ctx, sessRepo, connRepo, closeWiths) - if err != nil { - return nil, err - } - if closeInfos == nil { - return nil, status.Error(codes.Internal, "Invalid close connection response.") - } - - closeData := make([]*pbs.CloseConnectionResponseData, 0, numCloses) - for _, v := range closeInfos { - if v.Connection == nil { - return nil, status.Errorf(codes.Internal, "No connection found while closing one of the connection IDs: %v", closeIds) - } - if len(v.ConnectionStates) == 0 { - return nil, status.Errorf(codes.Internal, "No connection states found while closing one of the connection IDs: %v", closeIds) - } - closeData = append(closeData, &pbs.CloseConnectionResponseData{ - ConnectionId: v.Connection.GetPublicId(), - Status: v.ConnectionStates[0].Status.ProtoVal(), - }) - } - - ret := &pbs.CloseConnectionResponse{ - CloseResponseData: closeData, - } - - return ret, nil } diff --git a/internal/daemon/controller/handlers/workers/worker_service_test.go b/internal/daemon/controller/handlers/workers/worker_service_test.go index 8bd637002..fccdef7c5 100644 --- a/internal/daemon/controller/handlers/workers/worker_service_test.go +++ b/internal/daemon/controller/handlers/workers/worker_service_test.go @@ -1,176 +1,1296 @@ -package workers_test +package workers import ( "context" - "sync" + "fmt" + "sort" "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/boundary/internal/authtoken" - "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/db" - pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" - "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/errors" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" - "github.com/hashicorp/boundary/internal/session" - "github.com/hashicorp/boundary/internal/target" - "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" ) -func TestLookupSession(t *testing.T) { - ctx := context.Background() +var testAuthorizedActions = []string{"no-op", "read", "update", "delete"} + +func structListValue(t *testing.T, ss ...string) *structpb.ListValue { + t.Helper() + var val []interface{} + for _, s := range ss { + val = append(val, s) + } + lv, err := structpb.NewList(val) + require.NoError(t, err) + return lv +} + +func TestGet(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } rw := db.New(conn) - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - - serversRepoFn := func() (*servers.Repository, error) { - return servers.NewRepository(rw, rw, kms) - } - sessionRepoFn := func() (*session.Repository, error) { - return session.NewRepository(rw, rw, kms) - } - connectionRepoFn := func() (*session.ConnectionRepository, error) { - return session.NewConnectionRepository(ctx, rw, rw, kms) + kms := kms.TestKms(t, conn, wrap) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + repoFn := func() (*servers.Repository, error) { + return repo, nil } - at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) - uId := at.GetIamUserId() - hc := static.TestCatalogs(t, conn, prj.GetPublicId(), 1)[0] - hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] - h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] - static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := tcp.TestTarget(ctx, t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + kmsWorker := servers.TestKmsWorker(t, conn, wrap, + servers.WithName("test kms worker names"), + servers.WithDescription("test kms worker description"), + servers.WithAddress("test kms worker address"), + servers.WithWorkerTags(&servers.Tag{Key: "key", Value: "val"})) - sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ - UserId: uId, - HostId: h.GetPublicId(), - TargetId: tar.GetPublicId(), - HostSetId: hs.GetPublicId(), - AuthTokenId: at.GetPublicId(), - ScopeId: prj.GetPublicId(), - Endpoint: "tcp://127.0.0.1:22", - }) + kmsAuthzActions := make([]string, len(testAuthorizedActions)) + copy(kmsAuthzActions, testAuthorizedActions) - egressSess := session.TestSession(t, conn, wrapper, session.ComposedOf{ - UserId: uId, - HostId: h.GetPublicId(), - TargetId: tar.GetPublicId(), - HostSetId: hs.GetPublicId(), - AuthTokenId: at.GetPublicId(), - ScopeId: prj.GetPublicId(), - Endpoint: "tcp://127.0.0.1:22", - }) + wantKmsWorker := &pb.Worker{ + Id: kmsWorker.GetPublicId(), + ScopeId: kmsWorker.GetScopeId(), + Scope: &scopes.ScopeInfo{Id: kmsWorker.GetScopeId(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + CreatedTime: kmsWorker.CreateTime.GetTimestamp(), + UpdatedTime: kmsWorker.UpdateTime.GetTimestamp(), + Version: kmsWorker.GetVersion(), + Name: wrapperspb.String(kmsWorker.GetName()), + Description: wrapperspb.String(kmsWorker.GetDescription()), + Address: kmsWorker.GetAddress(), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + AuthorizedActions: strutil.StrListDelete(kmsAuthzActions, action.Update.String()), + LastStatusTime: kmsWorker.GetLastStatusTime().GetTimestamp(), + CanonicalTags: map[string]*structpb.ListValue{ + "key": structListValue(t, "val"), + }, + ConfigTags: map[string]*structpb.ListValue{ + "key": structListValue(t, "val"), + }, + Type: KmsWorkerType, + } - repo, err := sessionRepoFn() + var pkiWorkerKeyId string + pkiWorker := servers.TestPkiWorker(t, conn, wrap, + servers.WithName("test pki worker names"), + servers.WithDescription("test pki worker description"), + servers.WithTestPkiWorkerAuthorizedKeyId(&pkiWorkerKeyId)) + // Add config tags to the created worker + pkiWorker, err = repo.UpsertWorkerStatus(context.Background(), + servers.NewWorker(pkiWorker.GetScopeId(), + servers.WithAddress("test kms worker address"), + servers.WithWorkerTags(&servers.Tag{ + Key: "config", + Value: "test", + })), + servers.WithUpdateTags(true), + servers.WithPublicId(pkiWorker.GetPublicId()), + servers.WithKeyId(pkiWorkerKeyId)) require.NoError(t, err) - creds := []*pbs.Credential{ - { - Credential: &pbs.Credential_UserPassword{ - UserPassword: &pbs.UserPassword{ - Username: "username", - Password: "password", - }, - }, + wantPkiWorker := &pb.Worker{ + Id: pkiWorker.GetPublicId(), + ScopeId: pkiWorker.GetScopeId(), + Scope: &scopes.ScopeInfo{Id: pkiWorker.GetScopeId(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + CreatedTime: pkiWorker.CreateTime.GetTimestamp(), + UpdatedTime: pkiWorker.UpdateTime.GetTimestamp(), + Version: pkiWorker.GetVersion(), + Name: wrapperspb.String(pkiWorker.GetName()), + Description: wrapperspb.String(pkiWorker.GetDescription()), + Address: pkiWorker.GetAddress(), + AuthorizedActions: testAuthorizedActions, + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + LastStatusTime: pkiWorker.GetLastStatusTime().GetTimestamp(), + CanonicalTags: map[string]*structpb.ListValue{ + "config": structListValue(t, "test"), }, - { - Credential: &pbs.Credential_UserPassword{ - UserPassword: &pbs.UserPassword{ - Username: "another-username", - Password: "a different password", - }, - }, + ConfigTags: map[string]*structpb.ListValue{ + "config": structListValue(t, "test"), }, + Type: PkiWorkerType, } - workerCreds := make([]session.Credential, 0, len(creds)) - for _, c := range creds { - data, err := proto.Marshal(c) - require.NoError(t, err) - workerCreds = append(workerCreds, data) - } - err = repo.AddSessionCredentials(ctx, egressSess.ScopeId, egressSess.GetPublicId(), workerCreds) - require.NoError(t, err) - - s := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, connectionRepoFn, new(sync.Map), kms) - require.NotNil(t, s) - cases := []struct { - name string - wantErr bool - wantErrMsg string - want *pbs.LookupSessionResponse - sessionId string + name string + scopeId string + req *pbs.GetWorkerRequest + res *pbs.GetWorkerResponse + err error }{ { - name: "Invalid session id", - sessionId: "s_fakesession", - wantErr: true, - wantErrMsg: "rpc error: code = PermissionDenied desc = Unknown session ID.", + name: "Get an Existing KMS Worker", + scopeId: kmsWorker.GetScopeId(), + req: &pbs.GetWorkerRequest{Id: kmsWorker.GetPublicId()}, + res: &pbs.GetWorkerResponse{Item: wantKmsWorker}, }, { - name: "Valid", - sessionId: sess.PublicId, - want: &pbs.LookupSessionResponse{ - ConnectionLimit: 1, - ConnectionsLeft: 1, - Version: 1, - Endpoint: sess.Endpoint, - HostId: sess.HostId, - HostSetId: sess.HostSetId, - TargetId: sess.TargetId, - UserId: sess.UserId, - Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING, - }, + name: "Get an Existing PKI Worker", + scopeId: pkiWorker.GetScopeId(), + req: &pbs.GetWorkerRequest{Id: pkiWorker.GetPublicId()}, + res: &pbs.GetWorkerResponse{Item: wantPkiWorker}, }, { - name: "Valid-with-egress-creds", - sessionId: egressSess.PublicId, - want: &pbs.LookupSessionResponse{ - ConnectionLimit: 1, - ConnectionsLeft: 1, - Version: 1, - Endpoint: egressSess.Endpoint, - HostId: egressSess.HostId, - HostSetId: egressSess.HostSetId, - TargetId: egressSess.TargetId, - UserId: egressSess.UserId, - Status: pbs.SESSIONSTATUS_SESSIONSTATUS_PENDING, - Credentials: creds, - }, + name: "Get a non-existent Worker", + req: &pbs.GetWorkerRequest{Id: servers.WorkerPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Wrong id prefix", + req: &pbs.GetWorkerRequest{Id: "j_1234567890"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Space in id", + req: &pbs.GetWorkerRequest{Id: servers.WorkerPrefix + "_1 23456789"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } - for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - req := &pbs.LookupSessionRequest{ - SessionId: tc.sessionId, - } + s, err := NewService(context.Background(), repoFn, iamRepoFn) + require.NoError(t, err, "Couldn't create new worker service.") - got, err := s.LookupSession(ctx, req) - if tc.wantErr { - require.Error(err) - assert.Nil(got) - assert.Equal(tc.wantErrMsg, err.Error()) - return + got, err := s.GetWorker(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) + if tc.err != nil { + require.Error(t, err) + assert.True(t, errors.Is(err, tc.err), "GetWorker(%+v) got error %v, wanted %v", tc.req, err, tc.err) + } else { + require.NoError(t, err) } - assert.Empty( - cmp.Diff( - tc.want, - got, - cmpopts.IgnoreUnexported(pbs.LookupSessionResponse{}, pbs.Credential{}, pbs.UserPassword{}), - cmpopts.IgnoreFields(pbs.LookupSessionResponse{}, "Expiration", "Authorization"), - ), - ) + assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform()), "GetWorker(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) + }) + } +} + +func TestList(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + rw := db.New(conn) + kms := kms.TestKms(t, conn, wrap) + repoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, kms) + } + + var wantKmsWorkers []*pb.Worker + for i := 0; i < 10; i++ { + w := servers.TestKmsWorker(t, conn, wrap, servers.WithName(fmt.Sprintf("kms-worker%d", i))) + kmsAuthzActions := make([]string, len(testAuthorizedActions)) + copy(kmsAuthzActions, testAuthorizedActions) + wantKmsWorkers = append(wantKmsWorkers, &pb.Worker{ + Id: w.GetPublicId(), + ScopeId: w.GetScopeId(), + Scope: &scopes.ScopeInfo{Id: w.GetScopeId(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + CreatedTime: w.CreateTime.GetTimestamp(), + UpdatedTime: w.UpdateTime.GetTimestamp(), + Version: w.GetVersion(), + Name: wrapperspb.String(w.GetName()), + AuthorizedActions: strutil.StrListDelete(kmsAuthzActions, action.Update.String()), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + Address: w.GetAddress(), + Type: KmsWorkerType, + LastStatusTime: w.GetLastStatusTime().GetTimestamp(), + }) + } + + var wantPkiWorkers []*pb.Worker + for i := 0; i < 10; i++ { + w := servers.TestPkiWorker(t, conn, wrap, servers.WithName(fmt.Sprintf("pki-worker%d", i))) + wantPkiWorkers = append(wantPkiWorkers, &pb.Worker{ + Id: w.GetPublicId(), + ScopeId: w.GetScopeId(), + Scope: &scopes.ScopeInfo{Id: w.GetScopeId(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + CreatedTime: w.CreateTime.GetTimestamp(), + UpdatedTime: w.UpdateTime.GetTimestamp(), + Version: w.GetVersion(), + Name: wrapperspb.String(w.GetName()), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + AuthorizedActions: testAuthorizedActions, + Address: w.GetAddress(), + Type: PkiWorkerType, + LastStatusTime: w.GetLastStatusTime().GetTimestamp(), + }) + } + + cases := []struct { + name string + req *pbs.ListWorkersRequest + res *pbs.ListWorkersResponse + err error + }{ + { + name: "List All Workers", + req: &pbs.ListWorkersRequest{ScopeId: scope.Global.String()}, + res: &pbs.ListWorkersResponse{Items: append(wantKmsWorkers, wantPkiWorkers...)}, + }, + { + name: "List global workers recursively", + req: &pbs.ListWorkersRequest{ScopeId: "global", Recursive: true}, + res: &pbs.ListWorkersResponse{ + Items: append(wantKmsWorkers, wantPkiWorkers...), + }, + }, + { + name: "Filter to a single worker of each type", + req: &pbs.ListWorkersRequest{ScopeId: "global", Recursive: true, Filter: `"/item/name"=="kms-worker2" or "/item/name"=="pki-worker2"`}, + res: &pbs.ListWorkersResponse{ + Items: []*pb.Worker{wantKmsWorkers[2], wantPkiWorkers[2]}, + }, + }, + { + name: "Filter to 2 workers of each type", + req: &pbs.ListWorkersRequest{ScopeId: "global", Recursive: true, Filter: `"/item/name" matches "kms-worker[23]" or "/item/name" matches "pki-worker[23]"`}, + res: &pbs.ListWorkersResponse{ + Items: []*pb.Worker{wantKmsWorkers[2], wantKmsWorkers[3], wantPkiWorkers[2], wantPkiWorkers[3]}, + }, + }, + { + name: "Filter to no workers", + req: &pbs.ListWorkersRequest{ScopeId: "global", Recursive: true, Filter: `"/item/id"=="doesntmatch"`}, + res: &pbs.ListWorkersResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListWorkersRequest{ScopeId: "global", Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := NewService(context.Background(), repoFn, iamRepoFn) + require.NoError(err, "Couldn't create new worker service.") + + // Test with a non-anon user + got, gErr := s.ListWorkers(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListWorkers(%q) got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) + return + } + require.NoError(gErr) + sort.Slice(got.Items, func(i, j int) bool { + return got.Items[i].GetName().GetValue() < got.Items[j].GetName().GetValue() + }) + require.NoError(gErr) + sort.Slice(tc.res.Items, func(i, j int) bool { + return tc.res.Items[i].GetName().GetValue() < tc.res.Items[j].GetName().GetValue() + }) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListWorkers(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + + // Test the anon case + got, gErr = s.ListWorkers(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } + }) + } +} + +func TestDelete(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + rw := db.New(conn) + kms := kms.TestKms(t, conn, wrap) + repoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, kms) + } + ctx := context.Background() + + s, err := NewService(ctx, repoFn, iamRepoFn) + require.NoError(t, err, "Error when getting new worker service.") + + w := servers.TestKmsWorker(t, conn, wrap) + + cases := []struct { + name string + scopeId string + req *pbs.DeleteWorkerRequest + res *pbs.DeleteWorkerResponse + err error + }{ + { + name: "Delete an Existing Worker", + scopeId: w.GetScopeId(), + req: &pbs.DeleteWorkerRequest{ + Id: w.GetPublicId(), + }, + }, + { + name: "Delete bad worker id", + scopeId: w.GetScopeId(), + req: &pbs.DeleteWorkerRequest{ + Id: servers.WorkerPrefix + "_doesntexis", + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Bad Worker Id formatting", + scopeId: w.GetScopeId(), + req: &pbs.DeleteWorkerRequest{ + Id: "bad_format", + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.DeleteWorker(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "DeleteWorker(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + assert.EqualValuesf(tc.res, got, "DeleteWorker(%+v) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdate(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + repoFn := func() (*servers.Repository, error) { + return repo, nil + } + + wkr := servers.TestPkiWorker(t, conn, wrapper, + servers.WithName("default"), + servers.WithDescription("default")) + + version := wkr.GetVersion() + + resetWorker := func() { + version++ + _, _, err = repo.UpdateWorker(context.Background(), wkr, version, []string{"Name", "Description"}) + require.NoError(t, err, "Failed to reset worker.") + version++ + } + + wCreated := wkr.GetCreateTime().GetTimestamp().AsTime() + toMerge := &pbs.UpdateWorkerRequest{ + Id: wkr.GetPublicId(), + } + workerService, err := NewService(ctx, repoFn, iamRepoFn) + require.NoError(t, err) + expectedScope := &scopes.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"} + + cases := []struct { + name string + req *pbs.UpdateWorkerRequest + res *pbs.UpdateWorkerResponse + err error + }{ + { + name: "Update an Existing Worker", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("name"), + Description: wrapperspb.String("desc"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Name: wrapperspb.String("name"), + Description: wrapperspb.String("desc"), + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description,type"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("name"), + Description: wrapperspb.String("desc"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Name: wrapperspb.String("name"), + Description: wrapperspb.String("desc"), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "cant update address", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"address"}, + }, + Item: &pb.Worker{ + Address: "updated", + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "No Update Mask", + req: &pbs.UpdateWorkerRequest{ + Item: &pb.Worker{ + Name: wrapperspb.String("updated name"), + Description: wrapperspb.String("updated desc"), + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Empty Path", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.Worker{ + Name: wrapperspb.String("updated name"), + Description: wrapperspb.String("updated desc"), + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Only non-existant paths in Mask", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.Worker{ + Name: wrapperspb.String("updated name"), + Description: wrapperspb.String("updated desc"), + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Name", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Worker{ + Description: wrapperspb.String("ignored"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Description: wrapperspb.String("default"), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "Unset Description", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("ignored"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Name: wrapperspb.String("default"), + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("updated"), + Description: wrapperspb.String("ignored"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Name: wrapperspb.String("updated"), + Description: wrapperspb.String("default"), + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("ignored"), + Description: wrapperspb.String("notignored"), + }, + }, + res: &pbs.UpdateWorkerResponse{ + Item: &pb.Worker{ + Id: wkr.GetPublicId(), + ScopeId: wkr.GetScopeId(), + Scope: expectedScope, + Name: wrapperspb.String("default"), + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + Description: wrapperspb.String("notignored"), + CreatedTime: wkr.GetCreateTime().GetTimestamp(), + LastStatusTime: wkr.GetLastStatusTime().GetTimestamp(), + AuthorizedActions: testAuthorizedActions, + Type: PkiWorkerType, + }, + }, + }, + { + name: "Update a Non Existing Worker", + req: &pbs.UpdateWorkerRequest{ + Id: servers.WorkerPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Worker{ + Description: wrapperspb.String("desc"), + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Cant change Id", + req: &pbs.UpdateWorkerRequest{ + Id: wkr.GetPublicId(), + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.Worker{ + Id: "w_somethinge", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.Worker{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.Worker{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify tags", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"tags"}, + }, + Item: &pb.Worker{ + ConfigTags: map[string]*structpb.ListValue{ + "foo": structListValue(t, "bar"), + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify canonical tags", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"canonical_tags"}, + }, + Item: &pb.Worker{ + CanonicalTags: map[string]*structpb.ListValue{ + "foo": structListValue(t, "bar"), + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify address", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.AddressField}, + }, + Item: &pb.Worker{ + Address: "should_fail", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid name- uppercase", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description", "address"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("BADNAME"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid name- non-printable", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description", "address"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("\x00"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid description- nonprintable", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description", "address"}, + }, + Item: &pb.Worker{ + Description: wrapperspb.String("\x00"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.req.Item.Version = version + + req := proto.Clone(toMerge).(*pbs.UpdateWorkerRequest) + proto.Merge(req, tc.req) + + got, gErr := workerService.UpdateWorker(auth.DisabledAuthTestContext(iamRepoFn, scope.Global.String()), req) + if tc.err != nil { + require.Error(t, gErr) + assert.True(t, errors.Is(gErr, tc.err), "UpdateWorker(%+v) got error %v, wanted %v", req, gErr, tc.err) + } + + if tc.err == nil { + defer resetWorker() + } + + if got != nil { + assert.NotNilf(t, tc.res, "Expected UpdateWorker response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() + // Verify updated set after it was created + assert.True(t, gotUpdateTime.After(wCreated), "Updated resource should have been updated after its creation. Was updated %v, which is after %v", gotUpdateTime, wCreated) + + // Clear all values which are hard to compare against. + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + } + if tc.res != nil { + tc.res.Item.Version = version + 1 + } + assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateWorker(%q) got response %q, wanted %q", req, got, tc.res) + }) + } +} + +func TestUpdate_KMS(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + repoFn := func() (*servers.Repository, error) { + return repo, nil + } + + wkr := servers.TestKmsWorker(t, conn, wrapper, + servers.WithName("default"), + servers.WithDescription("default")) + + toMerge := &pbs.UpdateWorkerRequest{ + Id: wkr.GetPublicId(), + } + workerService, err := NewService(ctx, repoFn, iamRepoFn) + require.NoError(t, err) + + cases := []struct { + name string + req *pbs.UpdateWorkerRequest + res *pbs.UpdateWorkerResponse + err error + }{ + { + name: "Cant set name", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Worker{ + Name: wrapperspb.String("name"), + }, + }, + }, + { + name: "Cant set description", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Worker{ + Description: wrapperspb.String("description"), + }, + }, + }, + { + name: "Cant set address", + req: &pbs.UpdateWorkerRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"address"}, + }, + Item: &pb.Worker{ + Address: "address", + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := proto.Clone(toMerge).(*pbs.UpdateWorkerRequest) + proto.Merge(req, tc.req) + got, gErr := workerService.UpdateWorker(auth.DisabledAuthTestContext(iamRepoFn, scope.Global.String()), req) + assert.Error(t, gErr) + assert.Nil(t, got) + }) + } +} + +func TestUpdate_BadVersion(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + _, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err, "Couldn't create new worker repo.") + repoFn := func() (*servers.Repository, error) { + return repo, nil + } + ctx := context.Background() + + workerService, err := NewService(ctx, repoFn, iamRepoFn) + require.NoError(t, err, "Failed to create a new host set service.") + + wkr := servers.TestKmsWorker(t, conn, wrapper) + + upTar, err := workerService.UpdateWorker(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), &pbs.UpdateWorkerRequest{ + Id: wkr.GetPublicId(), + Item: &pb.Worker{ + Description: wrapperspb.String("updated"), + Version: 72, + }, + UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}}, + }) + assert.Nil(t, upTar) + assert.Error(t, err) + assert.True(t, errors.Is(err, handlers.NotFoundError()), "Got %v, wanted not found error.", err) +} + +func TestCreateWorkerLed(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + testRootWrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, testRootWrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + rw := db.New(conn) + testKms := kms.TestKms(t, conn, testRootWrapper) + repoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, testKms) + } + testCtx := context.Background() + + testSrv, err := NewService(testCtx, repoFn, iamRepoFn) + require.NoError(t, err, "Error when getting new worker service.") + + // Get an initial set of authorized node credentials + rootStorage, err := servers.NewRepositoryStorage(testCtx, rw, rw, testKms) + require.NoError(t, err) + _, err = rotation.RotateRootCertificates(testCtx, rootStorage) + require.NoError(t, err) + + fetchReqFn := func() string { + // This happens on the worker + fileStorage, err := file.New(testCtx) + require.NoError(t, err) + defer fileStorage.Cleanup() + + nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage) + require.NoError(t, err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx) + require.NoError(t, err) + + fetchEncoded, err := proto.Marshal(fetchReq) + require.NoError(t, err) + + return base58.Encode(fetchEncoded) + } + + tests := []struct { + name string + scopeId string + service Service + req *pbs.CreateWorkerLedRequest + res *pbs.CreateWorkerLedResponse + wantErr bool + wantErrIs error + wantErrContains string + }{ + { + name: "invalid-scope", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: "invalid-scope", + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: "Must be 'global'", + }, + { + name: "missing-node-credentials-token", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.WorkerGeneratedAuthTokenField, + }, + { + name: "invalid-id", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + Id: "invalid-id", + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.IdField, + }, + { + name: "invalid-address", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + Address: "invalid-address", + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.AddressField, + }, + { + name: "invalid-config-tags", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + ConfigTags: map[string]*structpb.ListValue{ + "invalid": {Values: []*structpb.Value{ + structpb.NewStringValue("invalid-tags"), + }}, + }, + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.ConfigTagsField, + }, + { + name: "invalid-canonical-tags", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + CanonicalTags: map[string]*structpb.ListValue{ + "invalid": {Values: []*structpb.Value{ + structpb.NewStringValue("invalid-canonical-tags"), + }}, + }, + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.CanonicalTagsField, + }, + { + name: "invalid-last-status-time", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + LastStatusTime: timestamppb.Now(), + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.LastStatusTimeField, + }, + { + name: "invalid-authorized-actions", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + AuthorizedActions: []string{"invalid-authorized-actions"}, + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.AuthorizedActionsField, + }, + { + name: "invalid-create-time", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + CreatedTime: timestamppb.Now(), + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.CreatedTimeField, + }, + { + name: "invalid-update-time", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + UpdatedTime: timestamppb.Now(), + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.UpdatedTimeField, + }, + { + name: "invalid-version", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + Version: 1, + }, + }, + wantErr: true, + wantErrIs: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: globals.VersionField, + }, + { + name: "invalid-auth", + service: testSrv, + scopeId: "splat-auth-scope", + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + }, + }, + wantErr: true, + wantErrContains: "non-existent scope \"splat-auth-scope\"", + }, + { + name: "invalid-base58-encoding", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: "invalid;semicolon"}, + }, + }, + wantErr: true, + wantErrContains: "error decoding node_credentials_token", + }, + { + name: "invalid-marshal", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: "notNodeCreds"}, + }, + }, + wantErr: true, + wantErrContains: "error unmarshaling node_credentials_token", + }, + { + name: "create-error", + service: func() Service { + repoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, &db.Db{}, testKms) + } + testSrv, err := NewService(testCtx, repoFn, iamRepoFn) + require.NoError(t, err, "Error when getting new worker service.") + return testSrv + }(), + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + Name: &wrapperspb.StringValue{Value: "success"}, + Description: &wrapperspb.StringValue{Value: "success-description"}, + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + }, + }, + wantErr: true, + wantErrContains: "error creating worker", + }, + { + name: "bad-repo-function-in-create", + service: func() Service { + // var cnt gives us a way for the repoFn to fail the 2nd time + // it's called so we can get past the AuthRequest check in + // CreateWorker(...) and test the createInRepo(...) failure + // path if the repoFn returns and err. + cnt := 0 + repoFn := func() (*servers.Repository, error) { + cnt = cnt + 1 + switch { + case cnt > 1: + return nil, errors.New(testCtx, errors.Internal, "bad-repo-function", "error creating repo") + default: + return servers.NewRepository(rw, rw, testKms) + } + } + testSrv, err := NewService(testCtx, repoFn, iamRepoFn) + require.NoError(t, err, "Error when getting new worker service.") + return testSrv + }(), + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + Name: &wrapperspb.StringValue{Value: "success"}, + Description: &wrapperspb.StringValue{Value: "success-description"}, + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + }, + }, + wantErr: true, + wantErrContains: "error creating worker: bad-repo-function: error creating repo", + }, + { + name: "success", + service: testSrv, + scopeId: scope.Global.String(), + req: &pbs.CreateWorkerLedRequest{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + Name: &wrapperspb.StringValue{Value: "success"}, + Description: &wrapperspb.StringValue{Value: "success-description"}, + WorkerGeneratedAuthToken: &wrapperspb.StringValue{Value: fetchReqFn()}, + }, + }, + res: &pbs.CreateWorkerLedResponse{ + Item: &workers.Worker{ + ScopeId: scope.Global.String(), + Name: &wrapperspb.StringValue{Value: "success"}, + Description: &wrapperspb.StringValue{Value: "success-description"}, + ActiveConnectionCount: &wrapperspb.UInt32Value{Value: 0}, + Version: 1, + Type: PkiWorkerType, + }, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.service.CreateWorkerLed(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrIs != nil { + assert.ErrorIs(err, tc.wantErrIs) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotNil(got) + { + // we only need to check that these "read-only" fields were set + // outbound. Other portions of the domain is responsible for + // testing that these values are correct and there's no reason + // to repeat those tests here. + assert.NotEmpty(got.GetItem().GetId()) + assert.NotEmpty(got.GetItem().CreatedTime) + assert.NotEmpty(got.GetItem().UpdatedTime) + assert.NotEmpty(got.GetItem().Scope) + assert.NotEmpty(got.GetItem().AuthorizedActions) + { + tc.res.Item.Id = got.GetItem().GetId() + tc.res.Item.CreatedTime = got.GetItem().GetCreatedTime() + tc.res.Item.UpdatedTime = got.GetItem().GetUpdatedTime() + tc.res.Item.Scope = got.GetItem().Scope + tc.res.Item.AuthorizedActions = got.GetItem().GetAuthorizedActions() + } + } + assert.Equal(tc.res, got) }) } } diff --git a/internal/daemon/controller/intercepting_listener.go b/internal/daemon/controller/intercepting_listener.go index 52f5c6463..b427fdd4d 100644 --- a/internal/daemon/controller/intercepting_listener.go +++ b/internal/daemon/controller/intercepting_listener.go @@ -1,12 +1,14 @@ package controller import ( - "context" + "crypto/tls" "errors" "fmt" "net" + "strings" "github.com/hashicorp/boundary/internal/observability/event" + nodee "github.com/hashicorp/nodeenrollment" ) // tempError is an error that satisfies the temporary error interface that is @@ -62,42 +64,61 @@ func newInterceptingListener(c *Controller, baseLn net.Listener) *interceptingLi func (m *interceptingListener) Accept() (net.Conn, error) { const op = "controller.(interceptingListener).Accept" - ctx := context.TODO() + ctx := m.c.baseContext conn, err := m.baseLn.Accept() if err != nil { + // We may already be closed but can't be sure, so just try anyways if conn != nil { if err := conn.Close(); err != nil { - event.WriteError(context.TODO(), op, err, event.WithInfoMsg("error closing worker connection")) + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) } } return nil, newTempError(err) } - nonce := make([]byte, 20) - read, err := conn.Read(nonce) - if err != nil { - if err := conn.Close(); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + tlsConn := conn.(*tls.Conn) + switch { + case nodee.ContainsKnownAlpnProto(tlsConn.ConnectionState().NegotiatedProtocol): + keyId, err := nodee.KeyIdFromPkix(tlsConn.ConnectionState().PeerCertificates[0].SubjectKeyId) + if err != nil { + if err := conn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + } + return nil, newTempError(fmt.Errorf("error deriving key id from newly-authenticated node: %w", err)) } - return nil, newTempError(fmt.Errorf("error reading nonce from connection: %w", err)) - } - if read != len(nonce) { - if err := conn.Close(); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + event.WriteSysEvent(ctx, op, "worker successfully authenticated", "key_id", keyId) + return conn, nil + + case strings.HasPrefix(tlsConn.ConnectionState().NegotiatedProtocol, "v1workerauth"): + nonce := make([]byte, 20) + read, err := conn.Read(nonce) + if err != nil { + if err := conn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + } + return nil, newTempError(fmt.Errorf("error reading nonce from connection: %w", err)) } - return nil, newTempError(fmt.Errorf("error reading nonce from worker, expected %d bytes, got %d", 20, read)) - } - workerInfoRaw, found := m.c.workerAuthCache.Load(string(nonce)) - if !found { - if err := conn.Close(); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + if read != len(nonce) { + if err := conn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + } + return nil, newTempError(fmt.Errorf("error reading nonce from worker, expected %d bytes, got %d", 20, read)) } - return nil, newTempError(errors.New("did not find valid nonce for incoming worker")) + workerInfoRaw, found := m.c.workerAuthCache.Load(string(nonce)) + if !found { + if err := conn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection")) + } + return nil, newTempError(errors.New("did not find valid nonce for incoming worker")) + } + workerInfo := workerInfoRaw.(*workerAuthEntry) + workerInfo.conn = tlsConn + event.WriteSysEvent(ctx, op, "worker successfully authed", "name", workerInfo.Name, "description", workerInfo.Description, "proxy_address", workerInfo.ProxyAddress) + return tlsConn, nil + + default: + return nil, newTempError(errors.New("unable to authenticate incoming connection")) } - workerInfo := workerInfoRaw.(*workerAuthEntry) - workerInfo.conn = conn - event.WriteSysEvent(ctx, op, "worker successfully authed", "name", workerInfo.Name) - return conn, nil } func (m *interceptingListener) Close() error { diff --git a/internal/daemon/controller/listeners.go b/internal/daemon/controller/listeners.go index b0429cb62..3475819b2 100644 --- a/internal/daemon/controller/listeners.go +++ b/internal/daemon/controller/listeners.go @@ -13,10 +13,12 @@ import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/hashicorp/boundary/internal/cmd/base" - "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" + "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/internal/metric" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/nodeenrollment/multihop" + "github.com/hashicorp/nodeenrollment/protocol" "google.golang.org/grpc" ) @@ -103,9 +105,25 @@ func (c *Controller) configureForApi(ln *base.ServerListener) ([]func(), error) } func (c *Controller) configureForCluster(ln *base.ServerListener) (func(), error) { - l := tls.NewListener(ln.ClusterListener, &tls.Config{ - GetConfigForClient: c.validateWorkerTls, - }) + const op = "controller.configureForCluster" + + workerAuthStorage, err := c.WorkerAuthRepoStorageFn() + if err != nil { + return nil, fmt.Errorf("error fetching worker auth storage: %w", err) + } + + l, err := protocol.NewInterceptingListener( + &protocol.InterceptingListenerConfiguration{ + Context: c.baseContext, + Storage: workerAuthStorage, + BaseListener: ln.ClusterListener, + BaseTlsConfiguration: &tls.Config{ + GetConfigForClient: c.validateWorkerTls, + }, + }) + if err != nil { + return nil, fmt.Errorf("error instantiating node auth listener: %w", err) + } workerReqInterceptor, err := workerRequestInfoInterceptor(c.baseContext, c.conf.Eventer) if err != nil { @@ -125,10 +143,21 @@ func (c *Controller) configureForCluster(ln *base.ServerListener) (func(), error ), ) - workerService := workers.NewWorkerServiceServer(c.ServersRepoFn, c.SessionRepoFn, c.ConnectionRepoFn, + workerService := handlers.NewWorkerServiceServer(c.ServersRepoFn, c.SessionRepoFn, c.ConnectionRepoFn, c.workerStatusUpdateTimes, c.kms) pbs.RegisterServerCoordinationServiceServer(workerServer, workerService) pbs.RegisterSessionServiceServer(workerServer, workerService) + + multihopService, err := handlers.NewMultihopServiceServer( + workerAuthStorage, + true, + nil, + ) + if err != nil { + return nil, fmt.Errorf("%s: error creating multihop service handler: %w", op, err) + } + multihop.RegisterMultihopServiceServer(workerServer, multihopService) + metric.InitializeClusterCollectors(c.conf.PrometheusRegisterer, workerServer) ln.GrpcServer = workerServer diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index e02d4dd2f..2111407d8 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -735,10 +735,10 @@ func (tc *TestController) AddClusterControllerMember(t testing.TB, opts *TestCon // WaitForNextWorkerStatusUpdate waits for the next status check from a worker to // come in. If it does not come in within the default status grace // period, this function returns an error. -func (tc *TestController) WaitForNextWorkerStatusUpdate(workerId string) error { +func (tc *TestController) WaitForNextWorkerStatusUpdate(workerStatusName string) error { const op = "controller.(TestController).WaitForNextWorkerStatusUpdate" ctx := context.TODO() - event.WriteSysEvent(ctx, op, "waiting for next status report from worker", "worker", workerId) + event.WriteSysEvent(ctx, op, "waiting for next status report from worker", "worker", workerStatusName) waitStatusStart := time.Now() ctx, cancel := context.WithTimeout(tc.ctx, tc.b.StatusGracePeriodDuration) defer cancel() @@ -777,7 +777,7 @@ func (tc *TestController) WaitForNextWorkerStatusUpdate(workerId string) error { return false } - if workerStatusUpdateId == workerId { + if workerStatusUpdateId == workerStatusName { waitStatusCurrent = workerStatusUpdateTime return false } @@ -795,10 +795,10 @@ func (tc *TestController) WaitForNextWorkerStatusUpdate(workerId string) error { } if err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error waiting for next status report from worker", "worker", workerId)) + event.WriteError(ctx, op, err, event.WithInfoMsg("error waiting for next status report from worker", "worker", workerStatusName)) return err } - event.WriteSysEvent(ctx, op, "waiting for next status report from worker received successfully", "worker", workerId) + event.WriteSysEvent(ctx, op, "waiting for next status report from worker received successfully", "worker", workerStatusName) return nil } diff --git a/internal/daemon/controller/tickers.go b/internal/daemon/controller/tickers.go index 749195f50..3de6f52fd 100644 --- a/internal/daemon/controller/tickers.go +++ b/internal/daemon/controller/tickers.go @@ -5,10 +5,10 @@ import ( "math/rand" "time" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/observability/event" - "github.com/hashicorp/boundary/internal/servers" - "github.com/hashicorp/boundary/internal/types/resource" ) // In the future we could make this configurable @@ -30,7 +30,7 @@ func (c *Controller) startStatusTicking(cancelCtx context.Context) { return case <-timer.C: - if err := c.upsertServer(cancelCtx); err != nil { + if err := c.upsertController(cancelCtx); err != nil { event.WriteError(cancelCtx, op, err, event.WithInfoMsg("error fetching repository for status update")) } timer.Reset(statusInterval) @@ -38,21 +38,18 @@ func (c *Controller) startStatusTicking(cancelCtx context.Context) { } } -func (c *Controller) upsertServer(ctx context.Context) error { - const op = "controller.(Controller).upsertServer" - server := &servers.Server{ - PrivateId: c.conf.RawConfig.Controller.Name, - Name: c.conf.RawConfig.Controller.Name, - Type: resource.Controller.String(), - Description: c.conf.RawConfig.Controller.Description, - Address: c.conf.RawConfig.Controller.PublicClusterAddr, +func (c *Controller) upsertController(ctx context.Context) error { + const op = "controller.(Controller).upsertController" + controller := &store.Controller{ + PrivateId: c.conf.RawConfig.Controller.Name, + Address: c.conf.RawConfig.Controller.PublicClusterAddr, } repo, err := c.ServersRepoFn() if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("error fetching repository for status update")) } - _, _, err = repo.UpsertServer(ctx, server) + _, err = repo.UpsertController(ctx, controller) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("error performing status update")) } diff --git a/internal/daemon/controller/worker_tls_config.go b/internal/daemon/controller/worker_tls_config.go index 29e20e12b..703e75f60 100644 --- a/internal/daemon/controller/worker_tls_config.go +++ b/internal/daemon/controller/worker_tls_config.go @@ -27,6 +27,7 @@ type workerAuthEntry struct { // been replayed, then stores connection information into the auth cache, which // is used by the intercepting listener to log information. func (c Controller) validateWorkerTls(hello *tls.ClientHelloInfo) (*tls.Config, error) { + const op = "controller.(Controller).validateWorkerTls" for _, p := range hello.SupportedProtos { switch { case strings.HasPrefix(p, "v1workerauth-"): @@ -50,7 +51,7 @@ func (c Controller) validateWorkerTls(hello *tls.ClientHelloInfo) (*tls.Config, return tlsConf, err } } - return nil, nil + return nil, errors.New("no supported protos found in hello") } // v1WorkerAuthConfig: diff --git a/internal/daemon/worker/auth_rotation.go b/internal/daemon/worker/auth_rotation.go new file mode 100644 index 000000000..cdf3f38b7 --- /dev/null +++ b/internal/daemon/worker/auth_rotation.go @@ -0,0 +1,202 @@ +package worker + +import ( + "context" + "errors" + "time" + + berrors "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/observability/event" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/multihop" + "github.com/hashicorp/nodeenrollment/types" +) + +func (w *Worker) startAuthRotationTicking(cancelCtx context.Context) { + const op = "worker.(Worker).startAuthRotationTicking" + if w.conf.WorkerAuthKms != nil { + // Nothing to do here, just immediately exit + // FIXME: Write a system event + return + } + + // The default time to use when we encounter an error or some other reason + // we can't get a better reset time + const defaultResetDuration = time.Hour + timer := time.NewTimer(defaultResetDuration) + // You're not supposed to call reset on timers that haven't been stopped or + // expired, so we stop it here so it resets in the loop below. That way if + // we want to adjust time, e.g. for tests, we can set the resetDuration + // value from within the loop + timer.Stop() + var resetDuration time.Duration // will start at 0, so we run immediately + for { + if w.TestOverrideAuthRotationPeriod != 0 { + resetDuration = w.TestOverrideAuthRotationPeriod + } + timer.Reset(resetDuration) + + select { + case <-cancelCtx.Done(): + event.WriteSysEvent(cancelCtx, op, "auth rotation ticking shutting down") + return + + case <-timer.C: + resetDuration = defaultResetDuration + + // Check if it's time to rotate and if not don't do anything + currentNodeCreds, err := types.LoadNodeCredentials(cancelCtx, w.WorkerAuthStorage, nodeenrollment.CurrentId, nodeenrollment.WithWrapper(w.conf.WorkerAuthStorageKms)) + if err != nil { + if errors.Is(err, nodeenrollment.ErrNotFound) { + // Be silent + continue + } + event.WriteError(cancelCtx, op, err) + continue + } + + if currentNodeCreds == nil { + event.WriteSysEvent(cancelCtx, op, "no error loading worker auth creds but nil creds, skipping rotation") + continue + } + + // Check the certificates to see if it's time + if len(currentNodeCreds.CertificateBundles) != 2 { + // Likely this means we haven't been authorized yet, and + // unclear what to do in any other situation + continue + } + + var earliestValid, latestValid time.Time + // Go through the bundles and find the full range of time that we + // have valid certificates + for _, bundle := range currentNodeCreds.CertificateBundles { + if curr := bundle.CertificateNotBefore.AsTime(); earliestValid.IsZero() || curr.Before(earliestValid) { + earliestValid = curr + } + if curr := bundle.CertificateNotAfter.AsTime(); latestValid.IsZero() || curr.After(latestValid) { + latestValid = curr + } + } + // Ensure that our time periods aren't somehow quite messed up + now := time.Now() + if earliestValid.After(now) || latestValid.Before(now) || earliestValid.After(latestValid) { + // We basically have no valid creds so we can't rotate. + // TODO (maybe): Have this trigger a new set of creds and request? + event.WriteSysEvent(cancelCtx, op, "not within node creds validity period, unsure what to do, not rotating") + continue + } + + // Figure out the midpoint; if we're after it, try to rotate + var shouldRotate bool + switch { + case w.TestOverrideAuthRotationPeriod != 0: + shouldRotate = true + default: + delta := latestValid.Sub(earliestValid) + shouldRotate = now.Before(earliestValid.Add(delta / 2)) + } + + if !shouldRotate { + continue + } + + if err := rotateWorkerAuth(cancelCtx, w, currentNodeCreds); err != nil { + event.WriteError(cancelCtx, op, err) + continue + } + + // TODO (maybe): Calculate new delta and set a custom retry time on the timer? + } + } +} + +// rotateWorkerAuth is a one-stop shop to perform a rotation, given a valid set +// of current node credentials. Note that it doesn't really sanity check inputs; +// it is meant to be called from other functions that have validated these +// inputs and is separated out purely for (a) cleanliness/readability of the +// ticking function and (b) possible future re-use. +func rotateWorkerAuth(ctx context.Context, w *Worker, currentNodeCreds *types.NodeCredentials) error { + const op = "worker.rotateWorkerAuth" + + randReaderOpt := nodeenrollment.WithRandomReader(w.conf.SecureRandomReader) + + // Ensure we can get some needed values prior to actually doing generation + currentKeyId, err := nodeenrollment.KeyIdFromPkix(currentNodeCreds.CertificatePublicKeyPkix) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + client := w.controllerMultihopConn.Load() + if client == nil { + return berrors.Wrap(ctx, err, op) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return berrors.Wrap(ctx, err, op) + } + + // Generate a new set of credentials but don't persist them yet + newNodeCreds, err := types.NewNodeCredentials( + ctx, + w.WorkerAuthStorage, + randReaderOpt, + nodeenrollment.WithWrapper(w.conf.WorkerAuthKms), + nodeenrollment.WithSkipStorage(true), + ) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + + // Get a signed request from the new credentials + fetchReq, err := newNodeCreds.CreateFetchNodeCredentialsRequest(ctx, randReaderOpt) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + + // Encrypt the values to the server + encFetchReq, err := nodeenrollment.EncryptMessage(ctx, currentKeyId, fetchReq, currentNodeCreds, randReaderOpt) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + + resp, err := multihopClient.RotateNodeCredentials(ctx, &types.RotateNodeCredentialsRequest{ + CertificatePublicKeyPkix: currentNodeCreds.CertificatePublicKeyPkix, + EncryptedFetchNodeCredentialsRequest: encFetchReq, + }) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + + // Decrypt the response using current credentials + fetchResp := new(types.FetchNodeCredentialsResponse) + if err := nodeenrollment.DecryptMessage( + ctx, + currentKeyId, + resp.EncryptedFetchNodeCredentialsResponse, + currentNodeCreds, + fetchResp, + ); err != nil { + return berrors.Wrap(ctx, err, op) + } + + // Call handle on the inner response, which will be encrypted to the + // new node credentials. This will also store the result, + // overwriting the current credentials. + newNodeCreds, err = newNodeCreds.HandleFetchNodeCredentialsResponse( + ctx, + w.WorkerAuthStorage, + fetchResp, + nodeenrollment.WithWrapper(w.conf.WorkerAuthKms), + ) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + + newKeyId, err := nodeenrollment.KeyIdFromPkix(newNodeCreds.CertificatePublicKeyPkix) + if err != nil { + return berrors.Wrap(ctx, err, op) + } + w.WorkerAuthCurrentKeyId.Store(newKeyId) + + return nil +} diff --git a/internal/daemon/worker/auth_rotation_test.go b/internal/daemon/worker/auth_rotation_test.go new file mode 100644 index 000000000..4e306d66c --- /dev/null +++ b/internal/daemon/worker/auth_rotation_test.go @@ -0,0 +1,118 @@ +package worker_test + +import ( + "testing" + "time" + + "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/daemon/controller" + "github.com/hashicorp/boundary/internal/daemon/worker" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/resolver" + "google.golang.org/protobuf/proto" +) + +// TestRotationTicking ensures that we see new credential information for a +// worker on both the controller side and worker side in a periodic fashion +func TestRotationTicking(t *testing.T) { + t.Parallel() + + require, assert := require.New(t), assert.New(t) + logger := hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + }) + + conf, err := config.DevController() + require.NoError(err) + + c := controller.NewTestController(t, &controller.TestControllerOpts{ + Config: conf, + Logger: logger.Named("controller"), + }) + t.Cleanup(c.Shutdown) + + const rotationPeriod = 20 * time.Second + + w := worker.NewTestWorker(t, &worker.TestWorkerOpts{ + InitialUpstreams: c.ClusterAddrs(), + Logger: logger.Named("worker"), + AuthRotationPeriod: rotationPeriod, + }) + t.Cleanup(w.Shutdown) + + // Get a servers repo and worker auth repo + serversRepo, err := c.Controller().ServersRepoFn() + require.NoError(err) + workerAuthRepo, err := c.Controller().WorkerAuthRepoStorageFn() + require.NoError(err) + + // Give time for the job to ensure that roots are created and offset us from + // the rotation period + time.Sleep(rotationPeriod / 2) + + // Verify that there are no nodes authorized yet + auths, err := workerAuthRepo.List(c.Context(), (*types.NodeInformation)(nil)) + require.NoError(err) + assert.Len(auths, 0) + + // Perform initial authentication of worker to controller + reqBytes, err := base58.FastBase58Decoding(w.Worker().WorkerAuthRegistrationRequest) + require.NoError(err) + + // Decode the proto into the request and create the worker + req := new(types.FetchNodeCredentialsRequest) + require.NoError(proto.Unmarshal(reqBytes, req)) + _, err = serversRepo.CreateWorker(c.Context(), &servers.Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + }, + }, servers.WithFetchNodeCredentialsRequest(req)) + require.NoError(err) + + // Verify we see one authorized set of credentials now + auths, err = workerAuthRepo.List(c.Context(), (*types.NodeInformation)(nil)) + require.NoError(err) + assert.Len(auths, 1) + // Fetch creds and store current key + currNodeCreds, err := types.LoadNodeCredentials(w.Context(), w.Worker().WorkerAuthStorage, nodeenrollment.CurrentId) + require.NoError(err) + currKey := currNodeCreds.CertificatePublicKeyPkix + + // Now we wait and check that we see new values in the DB and different + // creds on the worker after each rotation period + for i := 2; i < 5; i++ { + time.Sleep(rotationPeriod) + + // Verify we see the expected number, since we aren't expiring any it + // should be equal to the number of times we did rotations + auths, err = workerAuthRepo.List(c.Context(), (*types.NodeInformation)(nil)) + require.NoError(err) + assert.Len(auths, i) + + // Fetch creds and compare current key + currNodeCreds, err := types.LoadNodeCredentials(w.Context(), w.Worker().WorkerAuthStorage, nodeenrollment.CurrentId) + require.NoError(err) + assert.NotEqual(currKey, currNodeCreds.CertificatePublicKeyPkix) + currKey = currNodeCreds.CertificatePublicKeyPkix + currKeyId, err := nodeenrollment.KeyIdFromPkix(currNodeCreds.CertificatePublicKeyPkix) + require.NoError(err) + assert.Equal(currKeyId, w.Worker().WorkerAuthCurrentKeyId.Load()) + + // Stop and start the client connections to ensure the new credentials + // are valid; if not, we won't establish a new connection and rotation + // will fail + w.Worker().Resolver().UpdateState(resolver.State{Addresses: []resolver.Address{}}) + cc, err := w.Worker().GrpcClientConn() + require.NoError(err) + require.NoError(cc.Close()) + require.NoError(w.Worker().StartControllerConnections()) + } +} diff --git a/internal/daemon/worker/controller_connection.go b/internal/daemon/worker/controller_connection.go index cb07dee20..29eaa3bec 100644 --- a/internal/daemon/worker/controller_connection.go +++ b/internal/daemon/worker/controller_connection.go @@ -14,6 +14,8 @@ import ( "math/big" mathrand "math/rand" "net" + "os" + "path/filepath" "strings" "time" @@ -23,15 +25,24 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-secure-stdlib/base62" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/multihop" + "github.com/hashicorp/nodeenrollment/protocol" "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/resolver" "google.golang.org/protobuf/proto" ) -func (w *Worker) startControllerConnections() error { - const op = "worker.(Worker).startControllerConnections" - initialAddrs := make([]resolver.Address, 0, len(w.conf.RawConfig.Worker.Controllers)) - for _, addr := range w.conf.RawConfig.Worker.Controllers { +const hcpbUrlSuffix = ".proxy.boundary.hashicorp.cloud:9202" + +// StartControllerConnections starts up the resolver and initiates controller +// connection client creation +func (w *Worker) StartControllerConnections() error { + const op = "worker.(Worker).StartControllerConnections" + initialAddrs := make([]resolver.Address, 0, len(w.conf.RawConfig.Worker.InitialUpstreams)) + for _, addr := range w.conf.RawConfig.Worker.InitialUpstreams { switch { case strings.HasPrefix(addr, "/"): initialAddrs = append(initialAddrs, resolver.Address{Addr: addr}) @@ -41,21 +52,27 @@ func (w *Worker) startControllerConnections() error { host, port, err = net.SplitHostPort(net.JoinHostPort(addr, "9201")) } if err != nil { - return fmt.Errorf("error parsing controller address: %w", err) + return fmt.Errorf("error parsing upstream address: %w", err) } initialAddrs = append(initialAddrs, resolver.Address{Addr: net.JoinHostPort(host, port)}) } } if len(initialAddrs) == 0 { - return errors.New("no initial controller addresses found") + if w.conf.RawConfig.HCPBClusterId != "" { + clusterAddress := fmt.Sprintf("%s%s", w.conf.RawConfig.HCPBClusterId, hcpbUrlSuffix) + initialAddrs = append(initialAddrs, resolver.Address{Addr: clusterAddress}) + event.WriteSysEvent(w.baseContext, op, fmt.Sprintf("Setting HCPB Cluster address %s as upstream address", clusterAddress)) + } else { + return errors.New("no initial upstream addresses found") + } } w.Resolver().InitialState(resolver.State{ Addresses: initialAddrs, }) if err := w.createClientConn(initialAddrs[0].Addr); err != nil { - return fmt.Errorf("error making client connection to controller: %w", err) + return fmt.Errorf("error making client connection to upstream address: %w", err) } return nil @@ -64,39 +81,79 @@ func (w *Worker) startControllerConnections() error { func (w *Worker) controllerDialerFunc() func(context.Context, string) (net.Conn, error) { const op = "worker.(Worker).controllerDialerFunc" return func(ctx context.Context, addr string) (net.Conn, error) { - tlsConf, authInfo, err := w.workerAuthTLSConfig() - if err != nil { - return nil, fmt.Errorf("error creating tls config for worker auth: %w", err) - } - dialer := &net.Dialer{} - var nonTlsConn net.Conn + var conn net.Conn + var err error switch { - case strings.HasPrefix(addr, "/"): - nonTlsConn, err = dialer.DialContext(ctx, "unix", addr) + case w.conf.WorkerAuthKms != nil: + conn, err = w.v1KmsAuthDialFn(ctx, addr) default: - nonTlsConn, err = dialer.DialContext(ctx, "tcp", addr) - } - if err != nil { - return nil, fmt.Errorf("unable to dial to controller: %w", err) - } - tlsConn := tls.Client(nonTlsConn, tlsConf) - written, err := tlsConn.Write([]byte(authInfo.ConnectionNonce)) - if err != nil { - if err := nonTlsConn.Close(); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error closing connection after writing failure")) + conn, err = protocol.Dial(ctx, w.WorkerAuthStorage, addr, nodeenrollment.WithWrapper(w.conf.WorkerAuthStorageKms)) + // No error and a valid connection means the WorkerAuthRegistrationRequest was populated + // We can remove the stored workerAuthRequest file + if err == nil && conn != nil { + if w.WorkerAuthStorage.BaseDir() != "" { + workerAuthReqFilePath := filepath.Join(w.WorkerAuthStorage.BaseDir(), base.WorkerAuthReqFile) + // Intentionally ignoring any error removing this file + _ = os.Remove(workerAuthReqFilePath) + } } - return nil, fmt.Errorf("unable to write connection nonce: %w", err) } - if written != len(authInfo.ConnectionNonce) { - if err := nonTlsConn.Close(); err != nil { - event.WriteError(ctx, op, err, event.WithInfoMsg("error closing connection after writing failure")) + switch err { + case nil: + case nodeenrollment.ErrNotAuthorized: + // We don't event in this case, because the function retries often + // and will spam the logs. The status function will event indicating + // that it can't send status because it's not authorized, so that + // will be a fine hint to the user as to the issue. + default: + event.WriteError(ctx, op, err) + } + + if err == nil && conn != nil { + if !w.everAuthenticated.Load() { + w.everAuthenticated.Store(true) } - return nil, fmt.Errorf("expected to write %d bytes of connection nonce, wrote %d", len(authInfo.ConnectionNonce), written) + event.WriteSysEvent(ctx, op, "worker has successfully authenticated") } - return tlsConn, nil + + return conn, err } } +func (w *Worker) v1KmsAuthDialFn(ctx context.Context, addr string) (net.Conn, error) { + const op = "worker.(Worker).v1KmsAuthDialFn" + tlsConf, authInfo, err := w.workerAuthTLSConfig() + if err != nil { + return nil, fmt.Errorf("error creating tls config for worker auth: %w", err) + } + dialer := &net.Dialer{} + var nonTlsConn net.Conn + switch { + case strings.HasPrefix(addr, "/"): + nonTlsConn, err = dialer.DialContext(ctx, "unix", addr) + default: + nonTlsConn, err = dialer.DialContext(ctx, "tcp", addr) + } + if err != nil { + return nil, fmt.Errorf("unable to dial to upstream: %w", err) + } + tlsConn := tls.Client(nonTlsConn, tlsConf) + written, err := tlsConn.Write([]byte(authInfo.ConnectionNonce)) + if err != nil { + if err := nonTlsConn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing connection after writing failure")) + } + return nil, fmt.Errorf("unable to write connection nonce: %w", err) + } + if written != len(authInfo.ConnectionNonce) { + if err := nonTlsConn.Close(); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error closing connection after writing failure")) + } + return nil, fmt.Errorf("expected to write %d bytes of connection nonce, wrote %d", len(authInfo.ConnectionNonce), written) + } + return tlsConn, nil +} + func (w *Worker) createClientConn(addr string) error { const op = "worker.(Worker).createClientConn" defaultTimeout := (time.Second + time.Nanosecond).String() @@ -113,35 +170,47 @@ func (w *Worker) createClientConn(addr string) error { } `, defaultTimeout) res := w.Resolver() - cc, err := grpc.DialContext(w.baseContext, - fmt.Sprintf("%s:///%s", res.Scheme(), addr), + dialOpts := []grpc.DialOption{ grpc.WithResolvers(res), grpc.WithUnaryInterceptor(metric.InstrumentClusterClient()), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)), grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(math.MaxInt32)), grpc.WithContextDialer(w.controllerDialerFunc()), - grpc.WithInsecure(), + grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(defServiceConfig), // Don't have the resolver reach out for a service config from the // resolver, use the one specified as default grpc.WithDisableServiceConfig(), + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: time.Second, + Multiplier: 1.2, + Jitter: 0.2, + MaxDelay: 3 * time.Second, + }, + }), + } + cc, err := grpc.DialContext(w.baseContext, + fmt.Sprintf("%s:///%s", res.Scheme(), addr), + dialOpts..., ) if err != nil { return fmt.Errorf("error dialing controller for worker auth: %w", err) } + w.grpcClientConn.Store(cc) w.controllerStatusConn.Store(pbs.NewServerCoordinationServiceClient(cc)) w.controllerSessionConn.Store(pbs.NewSessionServiceClient(cc)) - - event.WriteSysEvent(w.baseContext, op, "connected to controller", "address", addr) + w.controllerMultihopConn.Store(multihop.NewMultihopServiceClient(cc)) return nil } func (w *Worker) workerAuthTLSConfig() (*tls.Config, *base.WorkerAuthInfo, error) { var err error info := &base.WorkerAuthInfo{ - Name: w.conf.RawConfig.Worker.Name, - Description: w.conf.RawConfig.Worker.Description, + Name: w.conf.RawConfig.Worker.Name, + Description: w.conf.RawConfig.Worker.Description, + ProxyAddress: w.conf.RawConfig.Worker.PublicAddr, } info.ConnectionNonce, err = w.nonceFn(20) diff --git a/internal/daemon/worker/listeners.go b/internal/daemon/worker/listeners.go index ce259a700..54ca63ea6 100644 --- a/internal/daemon/worker/listeners.go +++ b/internal/daemon/worker/listeners.go @@ -6,14 +6,24 @@ import ( "errors" "fmt" "log" + "math" "net" "net/http" "os" "time" "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/daemon/cluster/handlers" + pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-multierror" + nodee "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/multihop" + nodeenet "github.com/hashicorp/nodeenrollment/net" + "github.com/hashicorp/nodeenrollment/protocol" + "github.com/hashicorp/nodeenrollment/types" + "github.com/hashicorp/nodeenrollment/util/temperror" + "google.golang.org/grpc" ) func (w *Worker) startListeners() error { @@ -27,32 +37,30 @@ func (w *Worker) startListeners() error { if err != nil { return fmt.Errorf("%s: unable to initialize std logger: %w", op, err) } - - servers := make([]func(), 0, len(w.listeners)) - for i := range w.listeners { - ln := w.listeners[i] - workerServer, err := w.configureForWorker(ln, logger) - if err != nil { - return fmt.Errorf("%s: failed to configure for worker: %w", op, err) - } - servers = append(servers, workerServer) + if w.proxyListener == nil { + return fmt.Errorf("%s: nil proxy listener", op) } - for _, s := range servers { - s() + workerServer, err := w.configureForWorker(w.proxyListener, logger) + if err != nil { + return fmt.Errorf("%s: failed to configure for worker: %w", op, err) } + workerServer() + return nil } func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger) (func(), error) { + const op = "worker.configureForWorker" + handler, err := w.handler(HandlerProperties{ListenerConfig: ln.Config}) if err != nil { return nil, err } cancelCtx := w.baseContext - server := &http.Server{ + httpServer := &http.Server{ Handler: handler, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, @@ -61,80 +69,187 @@ func (w *Worker) configureForWorker(ln *base.ServerListener, logger *log.Logger) return cancelCtx }, } - ln.HTTPServer = server + ln.HTTPServer = httpServer if ln.Config.HTTPReadHeaderTimeout > 0 { - server.ReadHeaderTimeout = ln.Config.HTTPReadHeaderTimeout + httpServer.ReadHeaderTimeout = ln.Config.HTTPReadHeaderTimeout } if ln.Config.HTTPReadTimeout > 0 { - server.ReadTimeout = ln.Config.HTTPReadTimeout + httpServer.ReadTimeout = ln.Config.HTTPReadTimeout } if ln.Config.HTTPWriteTimeout > 0 { - server.WriteTimeout = ln.Config.HTTPWriteTimeout + httpServer.WriteTimeout = ln.Config.HTTPWriteTimeout } if ln.Config.HTTPIdleTimeout > 0 { - server.IdleTimeout = ln.Config.HTTPIdleTimeout + httpServer.IdleTimeout = ln.Config.HTTPIdleTimeout } - l := tls.NewListener(ln.ProxyListener, &tls.Config{ - GetConfigForClient: w.getSessionTls, - }) + fetchCredsFn := func( + ctx context.Context, + _ nodee.Storage, + req *types.FetchNodeCredentialsRequest, + _ ...nodee.Option, + ) (*types.FetchNodeCredentialsResponse, error) { + client := w.controllerMultihopConn.Load() + if client == nil { + return nil, temperror.New(errors.New("error fetching controller connection, client is nil")) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return nil, temperror.New(errors.New("client could not be understood as a multihop service client")) + } + // log.Println("proxying fetch node credentials request") + return multihopClient.FetchNodeCredentials(ctx, req) + } - return func() { go server.Serve(l) }, nil + generateServerCertificatesFn := func( + ctx context.Context, + _ nodee.Storage, + req *types.GenerateServerCertificatesRequest, + _ ...nodee.Option, + ) (*types.GenerateServerCertificatesResponse, error) { + client := w.controllerMultihopConn.Load() + if client == nil { + return nil, temperror.New(errors.New("error fetching controller connection, client is nil")) + } + multihopClient, ok := client.(multihop.MultihopServiceClient) + if !ok { + return nil, temperror.New(errors.New("client could not be understood as a multihop service client")) + } + // log.Println("proxying generate server cert request") + return multihopClient.GenerateServerCertificates(ctx, req) + } + + interceptingListener, err := protocol.NewInterceptingListener( + &protocol.InterceptingListenerConfiguration{ + Context: w.baseContext, + Storage: w.WorkerAuthStorage, + BaseListener: ln.ProxyListener, + BaseTlsConfiguration: &tls.Config{ + GetConfigForClient: w.getSessionTls, + }, + FetchCredsFunc: fetchCredsFn, + GenerateServerCertificatesFunc: generateServerCertificatesFn, + }) + if err != nil { + return nil, fmt.Errorf("error instantiating node auth listener: %w", err) + } + + w.workerAuthSplitListener = nodeenet.NewSplitListener(interceptingListener) + + downstreamServer := grpc.NewServer( + grpc.MaxRecvMsgSize(math.MaxInt32), + grpc.MaxSendMsgSize(math.MaxInt32), + ) + multihopService, err := handlers.NewMultihopServiceServer( + w.WorkerAuthStorage, + false, + w.controllerMultihopConn, + ) + if err != nil { + return nil, fmt.Errorf("%s: error creating multihop service handler: %w", op, err) + } + multihop.RegisterMultihopServiceServer(downstreamServer, multihopService) + statusSessionService := NewWorkerProxyServiceServer(w.controllerStatusConn, w.controllerSessionConn) + pbs.RegisterServerCoordinationServiceServer(downstreamServer, statusSessionService) + pbs.RegisterSessionServiceServer(downstreamServer, statusSessionService) + + ln.GrpcServer = downstreamServer + + return func() { + go w.workerAuthSplitListener.Start() + go httpServer.Serve(w.workerAuthSplitListener.OtherListener()) + go ln.GrpcServer.Serve( + &eventingListener{ + ctx: cancelCtx, + baseLn: w.workerAuthSplitListener.NodeEnrollmentListener(), + }, + ) + }, nil } func (w *Worker) stopServersAndListeners() error { - var closeErrors *multierror.Error - err := w.stopHttpServersAndListeners() - if err != nil { - closeErrors = multierror.Append(closeErrors, err) + var mg multierror.Group + mg.Go(w.stopHttpServer) + mg.Go(w.stopClusterGrpcServer) + + // FIXME (jeff): For some reason, unlike the controller, the grpc server + // really likes to hang on closing. Maybe because it's never served a + // connection? This is a workaround to force it until I can dig in. + var cancel context.CancelFunc + if w.workerAuthSplitListener != nil { + var ctx context.Context + ctx, cancel = context.WithTimeout(w.baseContext, 2*time.Second) + go func() { + <-ctx.Done() + w.workerAuthSplitListener.Stop() + cancel() + }() } - err = w.stopAnyListeners() - if err != nil { - closeErrors = multierror.Append(closeErrors, err) + stopErrors := mg.Wait() + + if w.workerAuthSplitListener != nil { + cancel() + err := w.workerAuthSplitListener.Stop() + if err != nil { + stopErrors = multierror.Append(stopErrors, err) + } } - return closeErrors.ErrorOrNil() + err := w.stopAnyListeners() + if err != nil { + stopErrors = multierror.Append(stopErrors, err) + } + + return stopErrors.ErrorOrNil() } -func (w *Worker) stopHttpServersAndListeners() error { - var closeErrors *multierror.Error - for i := range w.listeners { - ln := w.listeners[i] - if ln.HTTPServer == nil { - continue - } - - ctx, cancel := context.WithTimeout(w.baseContext, ln.Config.MaxRequestDuration) - ln.HTTPServer.Shutdown(ctx) - cancel() - - err := ln.ProxyListener.Close() - err = listenerCloseErrorCheck(ln.Config.Type, err) - if err != nil { - closeErrors = multierror.Append(closeErrors, err) - } +func (w *Worker) stopHttpServer() error { + if w.proxyListener == nil { + return nil } - return closeErrors.ErrorOrNil() + if w.proxyListener.HTTPServer == nil { + return nil + } + + ctx, cancel := context.WithTimeout(w.baseContext, w.proxyListener.Config.MaxRequestDuration) + w.proxyListener.HTTPServer.Shutdown(ctx) + cancel() + + return nil +} + +func (w *Worker) stopClusterGrpcServer() error { + if w.proxyListener == nil { + return nil + } + if w.proxyListener.GrpcServer == nil { + return nil + } + + w.proxyListener.GrpcServer.GracefulStop() + return nil } // stopAnyListeners does a final once over the known // listeners to make sure we didn't miss any; // expected to run at the end of stopServersAndListeners. func (w *Worker) stopAnyListeners() error { + if w.proxyListener == nil { + return nil + } var closeErrors *multierror.Error - for _, ln := range w.listeners { - if ln == nil || ln.ProxyListener == nil { - continue - } - - err := ln.ProxyListener.Close() - err = listenerCloseErrorCheck(ln.Config.Type, err) - if err != nil { - closeErrors = multierror.Append(closeErrors, err) - } + var err error + if w.workerAuthSplitListener != nil { + err = w.workerAuthSplitListener.Stop() + } else if w.proxyListener.ProxyListener != nil { + err = w.proxyListener.ProxyListener.Close() + } + err = listenerCloseErrorCheck("proxy", err) + if err != nil { + closeErrors = multierror.Append(closeErrors, err) } return closeErrors.ErrorOrNil() @@ -160,3 +275,36 @@ func listenerCloseErrorCheck(lnType string, err error) error { return err } + +type eventingListener struct { + ctx context.Context + baseLn net.Listener +} + +func (e *eventingListener) Accept() (net.Conn, error) { + const op = "worker.(eventingListener).Accept" + conn, err := e.baseLn.Accept() + if err != nil || conn == nil { + return conn, err + } + + // This is all best-effort; anything going wrong here shouldn't disrupt the + // connection, so on error simply stop trying to get to an event + tlsConn, ok := conn.(*tls.Conn) + if ok && len(tlsConn.ConnectionState().PeerCertificates) > 0 { + keyId, err := nodee.KeyIdFromPkix(tlsConn.ConnectionState().PeerCertificates[0].SubjectKeyId) + if err == nil { + event.WriteSysEvent(e.ctx, op, "worker successfully authenticated", "key_id", keyId) + } + } + + return conn, err +} + +func (e *eventingListener) Close() error { + return e.baseLn.Close() +} + +func (e *eventingListener) Addr() net.Addr { + return e.baseLn.Addr() +} diff --git a/internal/daemon/worker/listeners_test.go b/internal/daemon/worker/listeners_test.go index f8b6e2974..d48cb6827 100644 --- a/internal/daemon/worker/listeners_test.go +++ b/internal/daemon/worker/listeners_test.go @@ -3,13 +3,10 @@ package worker import ( "context" "fmt" - "io" "net" "net/http" "os" - "strconv" "testing" - "time" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/go-secure-stdlib/listenerutil" @@ -18,30 +15,19 @@ import ( func TestStartListeners(t *testing.T) { testNonTlsRejected := func(t *testing.T, resp *http.Response, err error) { - require.NoError(t, err) - require.NotNil(t, resp) - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, "Client sent an HTTP request to an HTTPS server.\n", string(body)) + require.Error(t, err) } tests := []struct { - name string - listeners []*listenerutil.ListenerConfig - expErr bool - expErrMsg string - assertions func(t *testing.T, w *Worker, addrs []string) + name string + proxyListeners []*listenerutil.ListenerConfig + expErr bool + expErrMsg string + assertions func(t *testing.T, w *Worker, addr string) }{ { - name: "one tcp listener, one unix listener", - listeners: []*listenerutil.ListenerConfig{ - { - Type: "tcp", - Purpose: []string{"proxy"}, - Address: "127.0.0.1:0", - TLSDisable: true, - }, + name: "unix listener", + proxyListeners: []*listenerutil.ListenerConfig{ { Type: "unix", Purpose: []string{"proxy"}, @@ -50,68 +36,30 @@ func TestStartListeners(t *testing.T) { }, }, expErr: false, - assertions: func(t *testing.T, w *Worker, addrs []string) { - require.Len(t, addrs, 2) - - resp, err := http.Get("http://" + addrs[0] + "/v1/proxy/") - testNonTlsRejected(t, resp, err) - + assertions: func(t *testing.T, w *Worker, addr string) { cl := http.Client{ Transport: &http.Transport{ - Dial: func(network, addr string) (net.Conn, error) { return net.Dial("unix", addrs[1]) }, + Dial: func(network, addr string) (net.Conn, error) { return net.Dial("unix", addr) }, }, } - resp, err = cl.Get("http://anything.domain/v1/proxy/") + resp, err := cl.Get("http://anything.domain/v1/proxy/") testNonTlsRejected(t, resp, err) }, }, { - name: "multiple tcp listeners, multiple unix listeners", - listeners: []*listenerutil.ListenerConfig{ + name: "tcp listener", + proxyListeners: []*listenerutil.ListenerConfig{ { Type: "tcp", Purpose: []string{"proxy"}, Address: "127.0.0.1:0", TLSDisable: true, }, - { - Type: "tcp", - Purpose: []string{"proxy"}, - Address: "127.0.0.1:0", - TLSDisable: true, - }, - { - Type: "unix", - Purpose: []string{"proxy"}, - Address: "/tmp/boundary-worker-test-start-listeners0.sock", - TLSDisable: true, - }, - { - Type: "unix", - Purpose: []string{"proxy"}, - Address: "/tmp/boundary-worker-test-start-listeners1.sock", - TLSDisable: true, - }, }, expErr: false, - assertions: func(t *testing.T, w *Worker, addrs []string) { - require.Len(t, addrs, 4) - - resp, err := http.Get("http://" + addrs[0] + "/v1/proxy/") + assertions: func(t *testing.T, w *Worker, addr string) { + resp, err := http.Get("http://" + addr + "/v1/proxy/") testNonTlsRejected(t, resp, err) - - resp, err = http.Get("http://" + addrs[1] + "/v1/proxy/") - testNonTlsRejected(t, resp, err) - - for _, proxyAddr := range []string{addrs[2], addrs[3]} { - cl := http.Client{ - Transport: &http.Transport{ - Dial: func(network, addr string) (net.Conn, error) { return net.Dial("unix", proxyAddr) }, - }, - } - resp, err = cl.Get("http://anything.domain/v1/proxy") - testNonTlsRejected(t, resp, err) - } }, }, } @@ -119,7 +67,7 @@ func TestStartListeners(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conf := NewTestWorker(t, &TestWorkerOpts{DisableAutoStart: true}).w.conf - conf.RawConfig.SharedConfig.Listeners = tt.listeners + conf.RawConfig.SharedConfig.Listeners = tt.proxyListeners err := conf.SetupListeners(nil, conf.RawConfig.SharedConfig, []string{"proxy"}) require.NoError(t, err) @@ -136,12 +84,8 @@ func TestStartListeners(t *testing.T) { require.NoError(t, err) - addrs := make([]string, 0) - for _, l := range w.listeners { - addrs = append(addrs, l.ProxyListener.Addr().String()) - } if tt.assertions != nil { - tt.assertions(t, w, addrs) + tt.assertions(t, w, w.proxyListener.ProxyListener.Addr().String()) } }) } @@ -158,14 +102,14 @@ func TestStopHttpServersAndListeners(t *testing.T) { { name: "no listeners", workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: []*base.ServerListener{}} + return &Worker{proxyListener: &base.ServerListener{}} }, expErr: false, }, { name: "nil listeners slice", workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: nil} + return &Worker{proxyListener: nil} }, expErr: false, }, @@ -173,65 +117,42 @@ func TestStopHttpServersAndListeners(t *testing.T) { name: "listeners with nil http server", workerFn: func(t *testing.T) *Worker { return &Worker{ - listeners: []*base.ServerListener{ - {HTTPServer: nil}, - {HTTPServer: nil}, - {HTTPServer: nil}, + proxyListener: &base.ServerListener{ + HTTPServer: nil, }, } }, expErr: false, }, { - name: "multiple listeners", + name: "valid listener with http server", workerFn: func(t *testing.T) *Worker { l1, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - l2, err := net.Listen("unix", "/tmp/boundary-worker-TestStopHttpServersAndListeners-"+strconv.FormatInt(time.Now().UnixNano(), 10)) - require.NoError(t, err) s1 := &http.Server{} - s2 := &http.Server{} go s1.Serve(l1) - go s2.Serve(l2) // Make sure they're up _, err = http.Get("http://" + l1.Addr().String()) require.NoError(t, err) - c := http.Client{Transport: &http.Transport{ - Dial: func(network, addr string) (net.Conn, error) { - return net.Dial("unix", l2.Addr().String()) - }, - }} - _, err = c.Get("http://random.domain") - require.NoError(t, err) - return &Worker{ baseContext: context.Background(), - listeners: []*base.ServerListener{ - { - ProxyListener: l1, - HTTPServer: s1, - Config: &listenerutil.ListenerConfig{Type: "tcp"}, - }, - { - ProxyListener: l2, - HTTPServer: s2, - Config: &listenerutil.ListenerConfig{Type: "tcp"}, - }, + proxyListener: &base.ServerListener{ + ProxyListener: l1, + HTTPServer: s1, + Config: &listenerutil.ListenerConfig{Type: "tcp"}, }, } }, assertions: func(t *testing.T, w *Worker) { // Asserts the HTTP Servers are closed. - require.ErrorIs(t, w.listeners[0].HTTPServer.Serve(w.listeners[0].ProxyListener), http.ErrServerClosed) - require.ErrorIs(t, w.listeners[1].HTTPServer.Serve(w.listeners[1].ProxyListener), http.ErrServerClosed) + require.ErrorIs(t, w.proxyListener.HTTPServer.Serve(w.proxyListener.ProxyListener), http.ErrServerClosed) // Asserts the underlying listeners are closed. - require.ErrorIs(t, w.listeners[0].ProxyListener.Close(), net.ErrClosed) - require.ErrorIs(t, w.listeners[1].ProxyListener.Close(), net.ErrClosed) + require.ErrorIs(t, w.proxyListener.ProxyListener.Close(), net.ErrClosed) }, expErr: false, }, @@ -240,7 +161,7 @@ func TestStopHttpServersAndListeners(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := tt.workerFn(t) - err := w.stopHttpServersAndListeners() + err := w.stopServersAndListeners() if tt.expErr { require.EqualError(t, err, tt.expErrStr) return @@ -265,66 +186,14 @@ func TestStopAnyListeners(t *testing.T) { { name: "nil listeners", workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: nil} + return &Worker{proxyListener: nil} }, expErr: false, }, { name: "no listeners", workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: []*base.ServerListener{}} - }, - expErr: false, - }, - { - name: "listeners slice with nil server listeners", - workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: []*base.ServerListener{nil, nil, nil}} - }, - expErr: false, - }, - { - name: "non-empty but nil listeners", - workerFn: func(t *testing.T) *Worker { - return &Worker{listeners: []*base.ServerListener{ - {ProxyListener: nil}, {ProxyListener: nil}, {ProxyListener: nil}, - }} - }, - expErr: false, - }, - { - name: "multiple listeners, including a closed one", - workerFn: func(t *testing.T) *Worker { - l1, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - l2, err := net.Listen("unix", "/tmp/boundary-worker-TestStopAnyListeners-"+strconv.FormatInt(time.Now().UnixNano(), 10)) - require.NoError(t, err) - - l3, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - require.NoError(t, l3.Close()) - - return &Worker{listeners: []*base.ServerListener{ - { - Config: &listenerutil.ListenerConfig{Type: "tcp"}, - ProxyListener: l1, - }, - { - Config: &listenerutil.ListenerConfig{Type: "tcp"}, - ProxyListener: l2, - }, - { - Config: &listenerutil.ListenerConfig{Type: "tcp"}, - ProxyListener: l3, - }, - }} - }, - assertions: func(t *testing.T, w *Worker) { - for i := range w.listeners { - ln := w.listeners[i] - require.ErrorIs(t, ln.ProxyListener.Close(), net.ErrClosed) - } + return &Worker{proxyListener: &base.ServerListener{}} }, expErr: false, }, diff --git a/internal/daemon/worker/status.go b/internal/daemon/worker/status.go index a7b0173b7..9d312a6d7 100644 --- a/internal/daemon/worker/status.go +++ b/internal/daemon/worker/status.go @@ -3,6 +3,7 @@ package worker import ( "context" "errors" + "fmt" "math/rand" "time" @@ -11,13 +12,14 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/servers" - "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-secure-stdlib/strutil" "google.golang.org/grpc/resolver" ) type LastStatusInformation struct { *pbs.StatusResponse - StatusTime time.Time + StatusTime time.Time + LastCalculatedUpstreams []string } func (w *Worker) startStatusTicking(cancelCtx context.Context) { @@ -90,6 +92,15 @@ func (w *Worker) WaitForNextSuccessfulStatusUpdate() error { func (w *Worker) sendWorkerStatus(cancelCtx context.Context) { const op = "worker.(Worker).sendWorkerStatus" + + // If we've never managed to successfully authenticate then we won't have + // any session information anyways and this will produce a ton of noise in + // observability, so suppress it + if !w.everAuthenticated.Load() { + event.WriteSysEvent(cancelCtx, op, "worker is not authenticated to an upstream, not sending status") + return + } + // First send info as-is. We'll perform cleanup duties after we // get cancel/job change info back. var activeJobs []*pbs.JobStatus @@ -127,22 +138,30 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) { // Send status information client := w.controllerStatusConn.Load().(pbs.ServerCoordinationServiceClient) - var tags map[string]*servers.TagValues + var tags []*servers.TagPair // If we're not going to request a tag update, no reason to have these // marshaled on every status call. if w.updateTags.Load() { - tags = w.tags.Load().(map[string]*servers.TagValues) + tags = w.tags.Load().([]*servers.TagPair) } statusCtx, statusCancel := context.WithTimeout(cancelCtx, common.StatusTimeout) defer statusCancel() + + keyId := w.WorkerAuthCurrentKeyId.Load() + + if w.conf.RawConfig.Worker.Name == "" && keyId == "" { + event.WriteError(statusCtx, op, errors.New("worker name and keyId are both empty; one is needed to identify a worker"), + event.WithInfoMsg("error making status request to controller")) + } + result, err := client.Status(statusCtx, &pbs.StatusRequest{ Jobs: activeJobs, - Worker: &servers.Server{ - PrivateId: w.conf.RawConfig.Worker.Name, - Type: resource.Worker.String(), + WorkerStatus: &servers.ServerWorkerStatus{ + Name: w.conf.RawConfig.Worker.Name, Description: w.conf.RawConfig.Worker.Description, Address: w.conf.RawConfig.Worker.PublicAddr, Tags: tags, + KeyId: keyId, }, UpdateTags: w.updateTags.Load(), }) @@ -182,19 +201,27 @@ func (w *Worker) sendWorkerStatus(cancelCtx context.Context) { } } else { w.updateTags.Store(false) - addrs := make([]resolver.Address, 0, len(result.Controllers)) - strAddrs := make([]string, 0, len(result.Controllers)) - for _, v := range result.Controllers { - addrs = append(addrs, resolver.Address{Addr: v.Address}) - strAddrs = append(strAddrs, v.Address) + // This may be nil if we are in a multiple hop scenario + var addrs []resolver.Address + var newUpstreams []string + if len(result.CalculatedUpstreams) > 0 { + addrs = make([]resolver.Address, 0, len(result.CalculatedUpstreams)) + for _, v := range result.CalculatedUpstreams { + addrs = append(addrs, resolver.Address{Addr: v.Address}) + newUpstreams = append(newUpstreams, v.Address) + } + lastStatus := w.lastStatusSuccess.Load().(*LastStatusInformation) + // Compare upstreams; update resolver if there is a difference, and emit an event with old and new addresses + if lastStatus != nil && !strutil.EquivalentSlices(lastStatus.LastCalculatedUpstreams, newUpstreams) { + upstreamsMessage := fmt.Sprintf("Upstreams has changed; old upstreams were: %s, new upstreams are: %s", lastStatus.LastCalculatedUpstreams, newUpstreams) + event.WriteSysEvent(cancelCtx, op, upstreamsMessage) + w.Resolver().UpdateState(resolver.State{Addresses: addrs}) + } else if lastStatus == nil { + w.Resolver().UpdateState(resolver.State{Addresses: addrs}) + event.WriteSysEvent(cancelCtx, op, fmt.Sprintf("Upstreams after first status set to: %s", newUpstreams)) + } } - switch len(strAddrs) { - case 0: - event.WriteError(statusCtx, op, errors.New("got no controller addresses from controller; possibly prior to first status save, not persisting")) - default: - w.Resolver().UpdateState(resolver.State{Addresses: addrs}) - } - w.lastStatusSuccess.Store(&LastStatusInformation{StatusResponse: result, StatusTime: time.Now()}) + w.lastStatusSuccess.Store(&LastStatusInformation{StatusResponse: result, StatusTime: time.Now(), LastCalculatedUpstreams: newUpstreams}) for _, request := range result.GetJobsRequests() { switch request.GetRequestType() { diff --git a/internal/daemon/worker/testing.go b/internal/daemon/worker/testing.go index 9a8cf1c1c..865352b92 100644 --- a/internal/daemon/worker/testing.go +++ b/internal/daemon/worker/testing.go @@ -4,17 +4,18 @@ import ( "context" "fmt" "net" + "strings" "testing" "time" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/daemon/worker/session" + "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-hclog" wrapping "github.com/hashicorp/go-kms-wrapping/v2" - "github.com/hashicorp/go-secure-stdlib/base62" "github.com/prometheus/client_golang/prometheus" ) @@ -31,7 +32,7 @@ type TestWorker struct { name string } -// Worker returns the underlying controller +// Worker returns the underlying worker func (tw *TestWorker) Worker() *Worker { return tw.w } @@ -52,10 +53,10 @@ func (tw *TestWorker) Name() string { return tw.name } -func (tw *TestWorker) ControllerAddrs() []string { +func (tw *TestWorker) UpstreamAddrs() []string { var addrs []string lastStatus := tw.w.LastStatusSuccess() - for _, v := range lastStatus.GetControllers() { + for _, v := range lastStatus.GetCalculatedUpstreams() { addrs = append(addrs, v.Address) } @@ -164,8 +165,8 @@ type TestWorkerOpts struct { // Config; if not provided a dev one will be created Config *config.Config - // Sets initial controller addresses - InitialControllers []string + // Sets initial upstream addresses + InitialUpstreams []string // If true, the worker will not be started DisableAutoStart bool @@ -173,6 +174,12 @@ type TestWorkerOpts struct { // The worker auth KMS to use, or one will be created WorkerAuthKms wrapping.Wrapper + // The worker credential storage KMS to use, or one will be created + WorkerAuthStorageKms wrapping.Wrapper + + // The location of the worker's auth storage + WorkerAuthStoragePath string + // The name to use for the worker, otherwise one will be randomly // generated, unless provided in a non-nil Config Name string @@ -191,6 +198,9 @@ type TestWorkerOpts struct { // Overrides worker's nonceFn, for cases where we want to have control // over the nonce we send to the Controller NonceFn randFn + + // If set, override the normal auth rotation period + AuthRotationPeriod time.Duration } func NewTestWorker(t testing.TB, opts *TestWorkerOpts) *TestWorker { @@ -223,8 +233,8 @@ func NewTestWorker(t testing.TB, opts *TestWorkerOpts) *TestWorker { opts.Config.Worker.Name = opts.Name } - if len(opts.InitialControllers) > 0 { - opts.Config.Worker.Controllers = opts.InitialControllers + if len(opts.InitialUpstreams) > 0 { + opts.Config.Worker.InitialUpstreams = opts.InitialUpstreams } // Start a logger @@ -260,6 +270,8 @@ func NewTestWorker(t testing.TB, opts *TestWorkerOpts) *TestWorker { switch { case opts.WorkerAuthKms != nil: tw.b.WorkerAuthKms = opts.WorkerAuthKms + case opts.WorkerAuthStorageKms != nil: + tw.b.WorkerAuthStorageKms = opts.WorkerAuthStorageKms default: if err := tw.b.SetupKMSes(tw.b.Context, nil, opts.Config); err != nil { t.Fatal(err) @@ -288,6 +300,8 @@ func NewTestWorker(t testing.TB, opts *TestWorkerOpts) *TestWorker { t.Fatal(err) } + tw.w.TestOverrideAuthRotationPeriod = opts.AuthRotationPeriod + if opts.NonceFn != nil { tw.w.nonceFn = opts.NonceFn } @@ -309,17 +323,19 @@ func (tw *TestWorker) AddClusterWorkerMember(t testing.TB, opts *TestWorkerOpts) } nextOpts := &TestWorkerOpts{ WorkerAuthKms: tw.w.conf.WorkerAuthKms, + WorkerAuthStorageKms: tw.w.conf.WorkerAuthStorageKms, Name: opts.Name, - InitialControllers: tw.ControllerAddrs(), + InitialUpstreams: tw.UpstreamAddrs(), Logger: tw.w.conf.Logger, StatusGracePeriodDuration: opts.StatusGracePeriodDuration, } if nextOpts.Name == "" { var err error - nextOpts.Name, err = base62.Random(5) + nextOpts.Name, err = db.NewPublicId("w") if err != nil { t.Fatal(err) } + nextOpts.Name = strings.ToLower(nextOpts.Name) event.WriteSysEvent(context.TODO(), op, "worker name generated", "name", nextOpts.Name) } return NewTestWorker(t, nextOpts) diff --git a/internal/daemon/worker/testing_test.go b/internal/daemon/worker/testing_test.go index 0b9fe2536..b1fabe6c5 100644 --- a/internal/daemon/worker/testing_test.go +++ b/internal/daemon/worker/testing_test.go @@ -6,7 +6,9 @@ import ( "time" "github.com/hashicorp/boundary/internal/daemon/worker/session" + "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/require" ) @@ -60,3 +62,28 @@ func TestTestWorkerLookupSessionMissing(t *testing.T) { require.False(ok) require.Equal(TestSessionInfo{}, actual) } + +func TestTestWorker_WorkerAuthStorageKms(t *testing.T) { + tests := []struct { + name string + wrapper wrapping.Wrapper + }{ + { + name: "Nil Wrapper", + wrapper: nil, + }, + { + name: "Valid Wrapper", + wrapper: db.TestWrapper(t), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + tw := NewTestWorker(t, &TestWorkerOpts{ + WorkerAuthStorageKms: tt.wrapper, + }) + require.Equal(tt.wrapper, tw.Config().WorkerAuthStorageKms) + }) + } +} diff --git a/internal/daemon/worker/worker.go b/internal/daemon/worker/worker.go index afb517332..99af08590 100644 --- a/internal/daemon/worker/worker.go +++ b/internal/daemon/worker/worker.go @@ -14,6 +14,10 @@ import ( "sync/atomic" "time" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/nodeenrollment" + "github.com/mr-tron/base58" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" @@ -21,13 +25,18 @@ import ( "github.com/hashicorp/boundary/internal/daemon/worker/session" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/observability/event" - "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-secure-stdlib/mlock" + "github.com/hashicorp/nodeenrollment/multihop" + nodeenet "github.com/hashicorp/nodeenrollment/net" + nodeefile "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" ua "go.uber.org/atomic" + "google.golang.org/grpc" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" + "google.golang.org/protobuf/proto" ) type randFn func(length int) (string, error) @@ -42,7 +51,10 @@ type Worker struct { tickerWg sync.WaitGroup + grpcClientConn *atomic.Value + controllerStatusConn *atomic.Value + everAuthenticated *ua.Bool lastStatusSuccess *atomic.Value workerStartTime time.Time @@ -51,7 +63,9 @@ type Worker struct { controllerSessionConn *atomic.Value sessionInfoMap *sync.Map - listeners []*base.ServerListener + controllerMultihopConn *atomic.Value + + proxyListener *base.ServerListener // Used to generate a random nonce for Controller connections nonceFn randFn @@ -62,11 +76,18 @@ type Worker struct { // This stores whether or not to send updated tags on the next status // request. It can be set via startup in New below, or (eventually) via // SIGHUP. - updateTags ua.Bool + updateTags *ua.Bool - // Test-specific options + // The storage for node enrollment + WorkerAuthStorage *nodeefile.Storage + WorkerAuthCurrentKeyId *ua.String + WorkerAuthRegistrationRequest string + workerAuthSplitListener *nodeenet.SplitListener + + // Test-specific options (and possibly hidden dev-mode flags) TestOverrideX509VerifyDnsName string TestOverrideX509VerifyCertPool *x509.CertPool + TestOverrideAuthRotationPeriod time.Duration } func New(conf *Config) (*Worker, error) { @@ -75,16 +96,21 @@ func New(conf *Config) (*Worker, error) { metric.InitializeClusterClientCollectors(conf.PrometheusRegisterer) w := &Worker{ - conf: conf, - logger: conf.Logger.Named("worker"), - started: ua.NewBool(false), - controllerStatusConn: new(atomic.Value), - lastStatusSuccess: new(atomic.Value), - controllerResolver: new(atomic.Value), - controllerSessionConn: new(atomic.Value), - sessionInfoMap: new(sync.Map), - tags: new(atomic.Value), - nonceFn: base62.Random, + conf: conf, + logger: conf.Logger.Named("worker"), + started: ua.NewBool(false), + grpcClientConn: new(atomic.Value), + controllerStatusConn: new(atomic.Value), + everAuthenticated: ua.NewBool(false), + lastStatusSuccess: new(atomic.Value), + controllerResolver: new(atomic.Value), + controllerSessionConn: new(atomic.Value), + sessionInfoMap: new(sync.Map), + controllerMultihopConn: new(atomic.Value), + tags: new(atomic.Value), + updateTags: ua.NewBool(false), + nonceFn: base62.Random, + WorkerAuthCurrentKeyId: new(ua.String), } w.lastStatusSuccess.Store((*LastStatusInformation)(nil)) @@ -121,6 +147,7 @@ func New(conf *Config) (*Worker, error) { } } + var listenerCount int for i := range conf.Listeners { l := conf.Listeners[i] if l == nil || l.Config == nil || l.Config.Purpose == nil { @@ -131,11 +158,20 @@ func New(conf *Config) (*Worker, error) { } switch l.Config.Purpose[0] { case "proxy": - w.listeners = append(w.listeners, l) + if w.proxyListener == nil { + w.proxyListener = l + } + listenerCount++ } } - if len(w.listeners) == 0 { - return nil, fmt.Errorf("no proxy listeners found") + if listenerCount != 1 { + return nil, fmt.Errorf("exactly one proxy listener is required") + } + + w.WorkerAuthStorage, err = nodeefile.New(w.baseContext, + nodeefile.WithBaseDirectory(w.conf.RawConfig.Worker.AuthStoragePath)) + if err != nil { + return nil, err } return w, nil @@ -143,29 +179,144 @@ func New(conf *Config) (*Worker, error) { func (w *Worker) Start() error { const op = "worker.(Worker).Start" + + w.baseContext, w.baseCancel = context.WithCancel(context.Background()) + if w.started.Load() { event.WriteSysEvent(w.baseContext, op, "already started, skipping") return nil } - w.baseContext, w.baseCancel = context.WithCancel(context.Background()) - scheme := strconv.FormatInt(time.Now().UnixNano(), 36) controllerResolver := manual.NewBuilderWithScheme(scheme) w.controllerResolver.Store(controllerResolver) + if w.conf.WorkerAuthKms == nil { + // In this section, we look for existing worker credentials. The two + // variables below store whether to create new credentials and whether + // to create a fetch request so it can be displayed in the worker + // startup info. These may be different because if initial creds have + // been generated on the worker side but not yet authorized/fetched from + // the controller, we don't want to invalidate that request on restart + // by generating a new set of credentials. However it's safe to output a + // new fetch request so we do in fact do that. + var createNodeAuthCreds bool + var createFetchRequest bool + nodeCreds, err := types.LoadNodeCredentials(w.baseContext, w.WorkerAuthStorage, nodeenrollment.CurrentId, nodeenrollment.WithWrapper(w.conf.WorkerAuthStorageKms)) + switch { + case err == nil: + // It's unclear why this would ever happen -- it shouldn't -- so + // this is simply safety against panics if something goes + // catastrophically wrong + if nodeCreds == nil { + event.WriteSysEvent(w.baseContext, op, "no error loading worker auth creds but nil creds, creating new creds for registration") + createNodeAuthCreds = true + createFetchRequest = true + break + } + + // Check that we have valid creds, or that we have generated creds but + // simply are still waiting on authentication (in which case we don't + // want to invalidate what we've already sent) + var validCreds bool + switch len(nodeCreds.CertificateBundles) { + case 0: + // Still waiting on initial creds, so don't invalidate the request + // by creating new credentials. However, we will generate and + // display a new valid request in case the first was lost. + createFetchRequest = true + + default: + now := time.Now() + for _, bundle := range nodeCreds.CertificateBundles { + if bundle.CertificateNotBefore.AsTime().Before(now) && bundle.CertificateNotAfter.AsTime().After(now) { + // If we have a certificate in its validity period, + // everything is fine + validCreds = true + break + } + } + + // Certificates are both expired, so create new credentials and + // output a request based on those + createNodeAuthCreds = !validCreds + createFetchRequest = !validCreds + } + + case errors.Is(err, nodeenrollment.ErrNotFound): + // Nothing was found on disk, so create + createNodeAuthCreds = true + createFetchRequest = true + + default: + // Some other type of error happened, bail out + return fmt.Errorf("error loading worker auth creds: %w", err) + } + + // NOTE: this block _must_ be before the `if createFetchRequest` block + // or the fetch request may have no credentials to work with + if createNodeAuthCreds { + nodeCreds, err = types.NewNodeCredentials( + w.baseContext, + w.WorkerAuthStorage, + nodeenrollment.WithRandomReader(w.conf.SecureRandomReader), + nodeenrollment.WithWrapper(w.conf.WorkerAuthStorageKms), + ) + if err != nil { + return fmt.Errorf("error generating new worker auth creds: %w", err) + } + } + + if createFetchRequest { + if nodeCreds == nil { + return fmt.Errorf("need to create fetch request but worker auth creds are nil: %w", err) + } + req, err := nodeCreds.CreateFetchNodeCredentialsRequest(w.baseContext, nodeenrollment.WithRandomReader(w.conf.SecureRandomReader)) + if err != nil { + return fmt.Errorf("error creating worker auth fetch credentials request: %w", err) + } + reqBytes, err := proto.Marshal(req) + if err != nil { + return fmt.Errorf("error marshaling worker auth fetch credentials request: %w", err) + } + w.WorkerAuthRegistrationRequest = base58.FastBase58Encoding(reqBytes) + if err != nil { + return fmt.Errorf("error encoding worker auth registration request: %w", err) + } + currentKeyId, err := nodeenrollment.KeyIdFromPkix(nodeCreds.CertificatePublicKeyPkix) + if err != nil { + return fmt.Errorf("error deriving worker auth key id: %w", err) + } + w.WorkerAuthCurrentKeyId.Store(currentKeyId) + } + // Regardless, we want to load the currentKeyId + currentKeyId, err := nodeenrollment.KeyIdFromPkix(nodeCreds.CertificatePublicKeyPkix) + if err != nil { + return fmt.Errorf("error deriving worker auth key id: %w", err) + } + w.WorkerAuthCurrentKeyId.Store(currentKeyId) + } + if err := w.startListeners(); err != nil { return fmt.Errorf("error starting worker listeners: %w", err) } - if err := w.startControllerConnections(); err != nil { + if err := w.StartControllerConnections(); err != nil { return fmt.Errorf("error making controller connections: %w", err) } - w.tickerWg.Add(1) + // Rather than deal with some of the potential error conditions for Add on + // the waitgroup vs. Done (in case a function exits immediately), we will + // always start rotation and simply exit early if we're using KMS, and + // always add 2 here. + w.tickerWg.Add(2) go func() { defer w.tickerWg.Done() w.startStatusTicking(w.baseContext) }() + go func() { + defer w.tickerWg.Done() + w.startAuthRotationTicking(w.baseContext) + }() w.workerStartTime = time.Now() w.started.Store(true) @@ -241,19 +392,40 @@ func (w *Worker) Resolver() *manual.Resolver { func (w *Worker) ParseAndStoreTags(incoming map[string][]string) { if len(incoming) == 0 { - w.tags.Store(map[string]*servers.TagValues{}) + w.tags.Store([]*servers.TagPair{}) return } - tags := make(map[string]*servers.TagValues, len(incoming)) - for k, v := range incoming { - tags[k] = &servers.TagValues{ - Values: append(make([]string, 0, len(v)), v...), + tags := []*servers.TagPair{} + for k, vals := range incoming { + for _, v := range vals { + tags = append(tags, &servers.TagPair{ + Key: k, + Value: v, + }) } } w.tags.Store(tags) w.updateTags.Store(true) } +// GrpcClientConn returns the underlying client connection +func (w *Worker) GrpcClientConn() (*grpc.ClientConn, error) { + rawConn := w.grpcClientConn.Load() + if rawConn == nil { + return nil, errors.New("unable to load grpc client conn") + } + cc, ok := rawConn.(*grpc.ClientConn) + if !ok { + return nil, fmt.Errorf("invalid grpc client connection %T", rawConn) + } + if cc == nil { + return nil, fmt.Errorf("client connection is nil") + } + + return cc, nil +} + +// ControllerSessionConn returns the underlying session service client func (w *Worker) ControllerSessionConn() (pbs.SessionServiceClient, error) { rawConn := w.controllerSessionConn.Load() if rawConn == nil { @@ -270,6 +442,40 @@ func (w *Worker) ControllerSessionConn() (pbs.SessionServiceClient, error) { return sessClient, nil } +// ControllerServerCoordinationServiceConn returns the underlying server coordination service client +func (w *Worker) ControllerServerCoordinationServiceConn() (pbs.ServerCoordinationServiceClient, error) { + rawConn := w.controllerStatusConn.Load() + if rawConn == nil { + return nil, errors.New("unable to load controller service coordination service connection") + } + statusClient, ok := rawConn.(pbs.ServerCoordinationServiceClient) + if !ok { + return nil, fmt.Errorf("invalid service coordination service client %T", rawConn) + } + if statusClient == nil { + return nil, fmt.Errorf("service coordination service client is nil") + } + + return statusClient, nil +} + +// ControllerMultihopConn returns the underlying multihop service client +func (w *Worker) ControllerMultihopConn() (multihop.MultihopServiceClient, error) { + rawConn := w.controllerMultihopConn.Load() + if rawConn == nil { + return nil, errors.New("unable to load controller multihop service connection") + } + multihopClient, ok := rawConn.(multihop.MultihopServiceClient) + if !ok { + return nil, fmt.Errorf("invalid multihop client %T", rawConn) + } + if multihopClient == nil { + return nil, fmt.Errorf("session multihop is nil") + } + + return multihopClient, nil +} + func (w *Worker) getSessionTls(hello *tls.ClientHelloInfo) (*tls.Config, error) { const op = "worker.(Worker).getSessionTls" ctx := w.baseContext @@ -300,8 +506,9 @@ func (w *Worker) getSessionTls(hello *tls.ClientHelloInfo) (*tls.Config, error) defer cancel() resp, err := conn.LookupSession(timeoutContext, &pbs.LookupSessionRequest{ - ServerId: w.conf.RawConfig.Worker.Name, - SessionId: sessionId, + ServerId: w.conf.RawConfig.Worker.Name, + SessionId: sessionId, + WorkerKeyId: w.WorkerAuthCurrentKeyId.Load(), }) if err != nil { return nil, fmt.Errorf("error validating session: %w", err) diff --git a/internal/daemon/worker/worker_proxy_service.go b/internal/daemon/worker/worker_proxy_service.go new file mode 100644 index 000000000..1b4755658 --- /dev/null +++ b/internal/daemon/worker/worker_proxy_service.go @@ -0,0 +1,67 @@ +package worker + +import ( + "context" + "sync/atomic" + + pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" +) + +type workerProxyServiceServer struct { + pbs.UnimplementedServerCoordinationServiceServer + pbs.UnimplementedSessionServiceServer + + scsClient *atomic.Value + ssClient *atomic.Value +} + +func NewWorkerProxyServiceServer( + scsClient *atomic.Value, + ssClient *atomic.Value, +) *workerProxyServiceServer { + return &workerProxyServiceServer{ + scsClient: scsClient, + ssClient: ssClient, + } +} + +var ( + _ pbs.ServerCoordinationServiceServer = &workerProxyServiceServer{} + _ pbs.SessionServiceServer = &workerProxyServiceServer{} +) + +func (ws *workerProxyServiceServer) Status(ctx context.Context, req *pbs.StatusRequest) (*pbs.StatusResponse, error) { + resp, err := ws.scsClient.Load().(pbs.ServerCoordinationServiceClient).Status(ctx, req) + + if resp != nil { + // We don't currently support distributing new addreses to workers + // multiple hops away so ensure they're stripped out + resp.CalculatedUpstreams = nil + } + + return resp, err +} + +func (ws *workerProxyServiceServer) LookupSession(ctx context.Context, req *pbs.LookupSessionRequest) (*pbs.LookupSessionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).LookupSession(ctx, req) +} + +func (ws *workerProxyServiceServer) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).CancelSession(ctx, req) +} + +func (ws *workerProxyServiceServer) ActivateSession(ctx context.Context, req *pbs.ActivateSessionRequest) (*pbs.ActivateSessionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).ActivateSession(ctx, req) +} + +func (ws *workerProxyServiceServer) AuthorizeConnection(ctx context.Context, req *pbs.AuthorizeConnectionRequest) (*pbs.AuthorizeConnectionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).AuthorizeConnection(ctx, req) +} + +func (ws *workerProxyServiceServer) ConnectConnection(ctx context.Context, req *pbs.ConnectConnectionRequest) (*pbs.ConnectConnectionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).ConnectConnection(ctx, req) +} + +func (ws *workerProxyServiceServer) CloseConnection(ctx context.Context, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) { + return ws.ssClient.Load().(pbs.SessionServiceClient).CloseConnection(ctx, req) +} diff --git a/internal/daemon/worker/worker_test.go b/internal/daemon/worker/worker_test.go index 8ae62a97d..028c4e152 100644 --- a/internal/daemon/worker/worker_test.go +++ b/internal/daemon/worker/worker_test.go @@ -1,14 +1,27 @@ package worker import ( + "context" + "crypto/rand" + "os" "testing" + "time" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/configutil/v2" "github.com/hashicorp/go-secure-stdlib/listenerutil" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/registration" + "github.com/hashicorp/nodeenrollment/rotation" + nodeefile "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestWorkerNew(t *testing.T) { @@ -23,13 +36,13 @@ func TestWorkerNew(t *testing.T) { name: "nil listeners", in: &Config{Server: &base.Server{Listeners: nil}}, expErr: true, - expErrMsg: "no proxy listeners found", + expErrMsg: "exactly one proxy listener is required", }, { name: "zero listeners", in: &Config{Server: &base.Server{Listeners: []*base.ServerListener{}}}, expErr: true, - expErrMsg: "no proxy listeners found", + expErrMsg: "exactly one proxy listener is required", }, { name: "populated with nil values", @@ -43,7 +56,7 @@ func TestWorkerNew(t *testing.T) { }, }, expErr: true, - expErrMsg: "no proxy listeners found", + expErrMsg: "exactly one proxy listener is required", }, { name: "multiple purposes", @@ -60,7 +73,7 @@ func TestWorkerNew(t *testing.T) { expErrMsg: `found listener with multiple purposes "api,proxy"`, }, { - name: "valid listeners", + name: "too many proxy listeners", in: &Config{ Server: &base.Server{ Listeners: []*base.ServerListener{ @@ -71,10 +84,21 @@ func TestWorkerNew(t *testing.T) { }, }, }, - expErr: false, - assertions: func(t *testing.T, w *Worker) { - require.Len(t, w.listeners, 2) + expErr: true, + expErrMsg: "exactly one proxy listener is required", + }, + { + name: "valid listeners", + in: &Config{ + Server: &base.Server{ + Listeners: []*base.ServerListener{ + {Config: &listenerutil.ListenerConfig{Purpose: []string{"api"}}}, + {Config: &listenerutil.ListenerConfig{Purpose: []string{"proxy"}}}, + {Config: &listenerutil.ListenerConfig{Purpose: []string{"cluster"}}}, + }, + }, }, + expErr: false, }, { name: "worker nonce func is set", @@ -112,3 +136,139 @@ func TestWorkerNew(t *testing.T) { }) } } + +func TestSetupWorkerAuthStorage(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ts := db.TestWrapper(t) + keyId, err := ts.KeyId(ctx) + require.NoError(t, err) + + // First, just test the key ID is populated + tmpDir, err := os.MkdirTemp("", "") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, os.RemoveAll(tmpDir)) }) + tw := NewTestWorker(t, &TestWorkerOpts{ + WorkerAuthStorageKms: ts, + WorkerAuthStoragePath: tmpDir, + DisableAutoStart: true, + }) + t.Cleanup(tw.Shutdown) + err = tw.Worker().Start() + require.NoError(t, err) + + wKeyId, err := tw.Config().WorkerAuthStorageKms.KeyId(ctx) + require.NoError(t, err) + assert.Equal(t, keyId, wKeyId) + + // Create a fresh persistent dir for the following tests + tmpDir, err = os.MkdirTemp("", "") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, os.RemoveAll(tmpDir)) }) + + // Get an initial set of authorized node credentials + initStorage, err := nodeefile.New(ctx) + require.NoError(t, err) + t.Cleanup(initStorage.Cleanup) + _, err = rotation.RotateRootCertificates(ctx, initStorage) + require.NoError(t, err) + initNodeCreds, err := types.NewNodeCredentials(ctx, initStorage) + require.NoError(t, err) + req, err := initNodeCreds.CreateFetchNodeCredentialsRequest(ctx) + require.NoError(t, err) + _, err = registration.AuthorizeNode(ctx, initStorage, req) + require.NoError(t, err) + fetchResp, err := registration.FetchNodeCredentials(ctx, initStorage, req) + require.NoError(t, err) + initNodeCreds, err = initNodeCreds.HandleFetchNodeCredentialsResponse(ctx, initStorage, fetchResp) + require.NoError(t, err) + initKeyId, err := nodeenrollment.KeyIdFromPkix(initNodeCreds.CertificatePublicKeyPkix) + require.NoError(t, err) + + nonce := make([]byte, nodeenrollment.NonceSize) + _, err = rand.Reader.Read(nonce) + require.NoError(t, err) + + // What's going on here: in each test we are simulating a startup of a + // worker that has storage in various states. The input is a function to + // modify the current state of node credentials by using the worker's + // storage, but this happens before Start so we haven't done checking yet; + // the assertions check what the final result is. + tests := []struct { + name string + in func(t *testing.T, w *Worker) + expKeyId string // If set, the existing key ID to expect + expRegistrationRequest bool // Whether we should have seen a registration request generated + expError string // Some other error + }{ + { + name: "no creds", + in: func(t *testing.T, w *Worker) { + // Do nothing; in this case it will have already been cleared + }, + expRegistrationRequest: true, + }, + { + name: "valid creds", + in: func(t *testing.T, w *Worker) { + // Store the authorized creds + require.NoError(t, initNodeCreds.Store(ctx, w.WorkerAuthStorage)) + }, + expKeyId: initKeyId, + }, + { + name: "existing but not validated", + in: func(t *testing.T, w *Worker) { + creds := proto.Clone(initNodeCreds).(*types.NodeCredentials) + creds.CertificateBundles = nil + creds.RegistrationNonce = nonce + require.NoError(t, creds.Store(ctx, w.WorkerAuthStorage)) + }, + expKeyId: initKeyId, + expRegistrationRequest: true, + }, + { + name: "existing and outside cert times", // Note that cert from next CA will already not be valid + in: func(t *testing.T, w *Worker) { + creds := proto.Clone(initNodeCreds).(*types.NodeCredentials) + creds.CertificateBundles[0].CertificateNotBefore = timestamppb.New(time.Time{}) + creds.CertificateBundles[0].CertificateNotAfter = timestamppb.New(time.Time{}) + require.NoError(t, creds.Store(ctx, w.WorkerAuthStorage)) + }, + expRegistrationRequest: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tw := NewTestWorker(t, &TestWorkerOpts{ + WorkerAuthStoragePath: tmpDir, + DisableAutoStart: true, + }) + t.Cleanup(tw.Shutdown) + + // Always clear out storage that was there before, ignore errors + _ = tw.Worker().WorkerAuthStorage.Remove(ctx, &types.NodeCredentials{Id: string(nodeenrollment.CurrentId)}) + + // Run node credentials modification + tt.in(t, tw.Worker()) + + // Start up to run logic + require.NoError(t, tw.Worker().Start()) + + // Validate existing key was loaded or new key was created and loaded + if tt.expKeyId != "" { + assert.Equal(t, tt.expKeyId, tw.Worker().WorkerAuthCurrentKeyId.Load()) + } else { + assert.NotEmpty(t, tw.Worker().WorkerAuthCurrentKeyId.Load()) + } + if tt.expRegistrationRequest { + assert.NotEmpty(t, tw.Worker().WorkerAuthRegistrationRequest) + } else { + assert.Empty(t, tw.Worker().WorkerAuthRegistrationRequest) + } + }) + } +} diff --git a/internal/db/schema/migrations/oss/postgres/34/01_domain_types.up.sql b/internal/db/schema/migrations/oss/postgres/34/01_domain_types.up.sql new file mode 100644 index 000000000..504274797 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/34/01_domain_types.up.sql @@ -0,0 +1,15 @@ +begin; + +-- wt_network_address defines a type for a network address. +-- While wt_dns_names does exist, a wt_network_address can be either a dns name +-- or an ip address. +create domain wt_network_address as text + constraint wt_network_address_too_short + check (length(trim(value)) > 0) + constraint wt_network_address_too_long + check (length(trim(value)) < 256); +comment on domain wt_network_address is + 'standard column for a network address.'; + + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/34/02_worker_controller_tables.up.sql b/internal/db/schema/migrations/oss/postgres/34/02_worker_controller_tables.up.sql new file mode 100644 index 000000000..e67230adb --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/34/02_worker_controller_tables.up.sql @@ -0,0 +1,278 @@ +begin; + +-- Split the server table into two new tables: controller and worker + +create table server_controller ( + private_id text primary key, + description wt_description, + address wt_network_address not null, + create_time wt_timestamp, + update_time wt_timestamp +); +comment on table server_controller is + 'server_controller is a table where each row represents a Boundary controller.'; + +create trigger immutable_columns before update on server_controller + for each row execute procedure immutable_columns('private_id','create_time'); + +create trigger default_create_time_column before insert on server_controller + for each row execute procedure default_create_time(); + +create trigger controller_insert_time_column before insert on server_controller + for each row execute procedure update_time_column(); + +create trigger controller_update_time_column before update on server_controller + for each row execute procedure update_time_column(); + +-- Worker table takes the place of the server table. +-- instead of the private_id we use a wt_public_id field named public_id since +-- workers will now be exposed as resources in boundary. + +create table server_worker_type_enm ( + name text primary key + constraint only_predefined_types_allowed + check ( + name in ( + 'pki', + 'kms' + ) + ) +); +comment on table server_worker_type_enm is + 'server_worker_type_enm is an enumeration table for worker types. ' + 'It contains rows for representing the pki and kms types.'; + +insert into server_worker_type_enm (name) +values + ('pki'), + ('kms'); + +create table server_worker ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + description wt_description + constraint description_only_has_printable_characters + check (description is null or description !~ '[^[:print:]]'), + name wt_name -- server_worker_scope_id_name_uq defines an appropriate uniqueness constraint for name + constraint worker_name_must_be_set_by_status + check ( + type != 'kms' or name is not null + ) + constraint name_must_be_lowercase + check (name is null or lower(trim(name)) = name) + constraint name_only_has_printable_characters + check (name is null or name !~ '[^[:print:]]'), + address wt_network_address + constraint address_must_be_set_by_status + check ( + -- address can be null only no status update has been received yet. + (last_status_time is not null and address is not null) + or + (last_status_time is null and address is null) + ), + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + type text not null + constraint server_worker_type_enm_fkey + references server_worker_type_enm (name) + on delete restrict + on update cascade, + last_status_time timestamp with time zone + constraint last_status_time_not_before_create_time + check (last_status_time >= create_time) + constraint last_status_time_always_set_for_kms + check (type != 'kms' or last_status_time is not null), + constraint server_worker_scope_id_name_uq + unique(scope_id, name) +); +comment on table server_worker is + 'server_worker is a table where each row represents a Boundary worker.'; + +create trigger immutable_columns before update on server_worker + for each row execute procedure immutable_columns('public_id', 'scope_id', 'type', 'create_time'); + +create trigger default_create_time_column before insert on server_worker + for each row execute procedure default_create_time(); + +create trigger worker_insert_time_column before insert on server_worker + for each row execute procedure update_time_column(); + +create trigger worker_update_time_column before update on server_worker + for each row execute procedure update_time_column(); + +-- fixme: we should only update the version column when type = 'pki', but that +-- can be deferred +create trigger update_version_column after update of version, description, name on server_worker + for each row execute procedure update_version_column(); + +create function immutable_kms_name() + returns trigger +as $$ +begin + if old.type = 'kms' and new.name is distinct from old.name then + raise exception 'immutable column for kms worker: server_worker.name' using + errcode = '23601', + schema = tg_table_schema, + table = tg_table_name, + column = 'name'; + end if; + return new; +end; +$$ language plpgsql; +comment on function immutable_kms_name is + 'function used in before update triggers to make name column immutable for kms workers'; + +create trigger immutable_kms_name before update on server_worker + for each row execute procedure immutable_kms_name(); + +create function update_kms_server_worker_update_last_status_time_column() + returns trigger +as $$ +begin + if new.type = 'kms' then + new.last_status_time = now(); + end if; + return new; +end; +$$ language plpgsql; +comment on function update_kms_server_worker_update_last_status_time_column is + 'function used to update the last_status_time column in server_worker with type kms to now'; + +create trigger update_kms_server_worker_last_status_time_column before update of address, name, description on server_worker + for each row execute procedure update_kms_server_worker_update_last_status_time_column(); + +create function update_pki_server_worker_update_last_status_time_column() + returns trigger +as $$ +begin + if new.type = 'pki' then + new.last_status_time = now(); + end if; + return new; +end; +$$ language plpgsql; +comment on function update_pki_server_worker_update_last_status_time_column is + 'function used to update the last_status_time column in server_worker with type pki to now'; + +create trigger update_pki_server_worker_last_status_time_column before update of address on server_worker + for each row execute procedure update_pki_server_worker_update_last_status_time_column(); + +create function insert_kms_server_worker_update_last_status_time_column() + returns trigger +as $$ +begin + if new.type = 'kms' then + new.last_status_time = now(); + end if; + return new; +end; +$$ language plpgsql; +comment on function insert_kms_server_worker_update_last_status_time_column is + 'function used to update the last_status_time column in server_worker with type kms to now'; + +create trigger insert_server_worker_last_update_time_column before insert on server_worker + for each row execute procedure insert_kms_server_worker_update_last_status_time_column(); + +-- Create table worker tag +create table server_worker_tag_enm ( + source text primary key + constraint only_predefined_server_worker_tag_sources_allowed + check ( + source in ('configuration', 'api') + ) +); + +insert into server_worker_tag_enm (source) +values + ('configuration'), + ('api'); + +create table server_worker_tag ( + worker_id wt_public_id + constraint server_worker_fkey + references server_worker(public_id) + on delete cascade + on update cascade, + key wt_tagpair, + value wt_tagpair, + source text not null + constraint server_worker_tag_enm_fkey + references server_worker_tag_enm(source) + on delete restrict + on update cascade, + primary key(worker_id, key, value, source) +); + + +-- Aaand drop server_tag +drop table server_tag; + +-- Update session table to use worker_id instead of server_id, drop view first because of dependency on server type +drop view session_list; + +-- Update session table to use worker_id instead of server_id +-- Updating the session table modified in 01/01_server_tags_migrations.up.sql +drop trigger update_version_column + on session; +alter table session + drop constraint session_server_id_fkey, + drop column server_type, + drop column server_id; +create trigger + update_version_column + after update of version, termination_reason, key_id, tofu_token on session + for each row execute procedure update_version_column(); + +-- Update session_connection table to use worker_id instead of server_id +-- Table last updated in 21/02_session.up.sql +alter table session_connection + drop column server_id, + add column worker_id wt_public_id, + add constraint server_worker_fkey + foreign key (worker_id) + references server_worker (public_id) + on delete set null + on update cascade; + +-- Update job run table so that server id references controller id +-- We are not migrating the values from server_id to controller_id. The fkey +-- constraint says that server_id can be set to null when the server is deleted +-- which is what this migration does (removing all records from the server table). +-- Not migrating values make it easier to change types in the server_worker and +-- server_controller tables (like from text to wt_public_id or text to wt_address) +-- without having to worry about old values being valid in the new types. +-- Finally, neither jobs nor servers are exposed out of boundary so the risk of +-- losing data that would be useful later on is diminished. +alter table job_run + add column controller_id text, + drop column server_id; +alter table job_run + add constraint controller_id_must_be_at_least_10_characters + check( + length(trim(controller_id)) > 10 + ), + add constraint server_controller_fkey + foreign key (controller_id) + references server_controller (private_id) + on delete set null + on update cascade; + +-- Since the above alter tables sets all controller_ids to null running jobs +-- can no longer be reclaimed by any controller and should be considered +-- interrupted. +update job_run +set + status = 'interrupted', + end_time = current_timestamp +where + status = 'running'; + +-- Drop the server and server_type_enm tables +drop table server; +drop table server_type_enm; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql b/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql new file mode 100644 index 000000000..b6e39a48e --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/34/03_worker_authentication.up.sql @@ -0,0 +1,168 @@ +begin; + +create table worker_auth_operational_indicator_enm( + state text primary key + constraint only_predefined_worker_auth_states_allowed + check ( + state in ('next','current') + ) +); + +comment on table worker_auth_operational_indicator_enm is + 'worker_auth_operational_indicator_enm is an enumeration table representing the state of certificates used for worker authentication.'; + +insert into worker_auth_operational_indicator_enm (state) +values + ('next'), + ('current'); + +create table worker_auth_ca( + private_id text primary key + constraint only_roots_id_allowed + check (private_id in ('roots')), + version wt_version, + create_time wt_timestamp, + update_time wt_timestamp +); + +create trigger + immutable_columns + before + update on worker_auth_ca + for each row execute procedure immutable_columns('private_id','create_time'); + +create trigger worker_auth_ca_default_create_time_column + before + insert on worker_auth_ca + for each row execute procedure default_create_time(); + +create trigger worker_auth_ca_update_time_column + before + update on worker_auth_ca + for each row execute procedure update_time_column(); + +insert into worker_auth_ca(private_id) values('roots'); + +create trigger update_version_column after update on worker_auth_ca + for each row execute procedure update_version_column(private_id); + +comment on table worker_auth_ca is + 'worker_auth_ca is a one-row versioned table used for locking for certificate rotation on the worker_auth_ca_certificate table.'; + +create table worker_auth_ca_certificate( + serial_number numeric, + certificate bytea not null + constraint certificate_must_not_be_empty + check(length(certificate) > 0), + not_valid_before wt_timestamp not null, + not_valid_after wt_timestamp not null + constraint not_valid_before_must_be_before_not_valid_after + check(not_valid_before < not_valid_after), + public_key bytea primary key + constraint public_key_must_not_be_empty + check(length(public_key) > 0), + private_key bytea not null -- encrypted PEM encoded private key + constraint private_key_must_not_be_empty + check(length(private_key) > 0), + key_id kms_private_id not null -- key used to encrypt entries via wrapping wrapper. + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade, + state text unique not null + constraint worker_auth_operational_indicator_enm_fkey + references worker_auth_operational_indicator_enm(state) + on delete restrict + on update cascade, + issuing_ca text not null + constraint worker_auth_ca_fkey + references worker_auth_ca(private_id) +); + +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'); + +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.'; + +create table worker_auth_authorized( + worker_key_identifier text primary key, -- The public key id for this WorkerAuth entry, generated from the signing pub key + worker_id wt_public_id not null + constraint server_worker_fkey + references server_worker (public_id) + on delete cascade + on update cascade, + worker_signing_pub_key bytea not null + constraint worker_signing_pub_key_must_not_be_empty + check(length(worker_signing_pub_key) > 0), + worker_encryption_pub_key bytea not null + constraint worker_encryption_pub_key_must_not_be_empty + check(length(worker_encryption_pub_key) > 0), + controller_encryption_priv_key bytea not null -- encrypted PEM encoded private key + constraint controller_encryption_priv_key_must_not_be_empty + check(length(controller_encryption_priv_key) > 0), + key_id kms_private_id not null -- key used to encrypt entries via wrapping wrapper. + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade, + nonce bytea -- encrypted value +); +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.'; + +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'); + +create function insert_worker_auth_authorized_type_check() + returns trigger +as $$ +begin + perform from server_worker where type = 'pki' and public_id = new.worker_id; + if not found then + raise exception 'invalid type: non pki worker cannot be authorized in worker_auth_authorized table'; + end if; + return new; +end; +$$ language plpgsql; +comment on function insert_worker_auth_authorized_type_check is + 'function used to only allow workers of type pki to be authorized'; + +create trigger insert_worker_auth_authorized_type_check before insert on worker_auth_authorized + for each row execute procedure insert_worker_auth_authorized_type_check(); + +create table worker_auth_certificate_bundle( + root_certificate_public_key bytea not null + constraint worker_auth_ca_certificate_fkey + references worker_auth_ca_certificate(public_key) + on delete cascade + on update cascade, + worker_key_identifier text not null + constraint worker_auth_authorized_fkey + references worker_auth_authorized(worker_key_identifier) + on delete cascade + on update cascade, + cert_bundle bytea + constraint current_cert_bundle_must_not_be_empty + check(length(cert_bundle) > 0), + primary key(root_certificate_public_key, worker_key_identifier) +); + +comment on table worker_auth_certificate_bundle is + 'worker_auth_certificate_bundle is a table where each row represents a cert bundle issued by a ca certificate for a worker.'; + +create trigger + immutable_columns + before + update on worker_auth_certificate_bundle + for each row execute procedure immutable_columns('root_certificate_public_key', 'worker_key_identifier', 'cert_bundle'); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/34/04_views.up.sql b/internal/db/schema/migrations/oss/postgres/34/04_views.up.sql new file mode 100644 index 000000000..c67d3ee4a --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/34/04_views.up.sql @@ -0,0 +1,67 @@ +begin; + +-- worker_aggregate view allows the worker and configuration to be read at the +-- same time. +create view server_worker_aggregate as +with worker_config_tags(worker_id, source, tags) as ( + select + ct.worker_id, + ct.source, + -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. + string_agg(distinct concat_ws('Y', ct.key, ct.value), 'Z') as tags + from server_worker_tag ct + group by ct.worker_id, ct.source +), + connection_count (worker_id, count) as ( + select + worker_id, + count(1) as count + from session_connection + where closed_reason is null + group by worker_id + ) +select + w.public_id, + w.scope_id, + w.description, + w.name, + w.address, + w.create_time, + w.update_time, + w.version, + w.last_status_time, + w.type, + cc.count as active_connection_count, + -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. + wt.tags as api_tags, + ct.tags as worker_config_tags +from server_worker w + left join worker_config_tags wt on + w.public_id = wt.worker_id and wt.source = 'api' + left join worker_config_tags ct on + w.public_id = ct.worker_id and ct.source = 'configuration' + left join connection_count as cc on + w.public_id = cc.worker_id; +comment on view server_worker_aggregate is + 'server_worker_aggregate contains the worker resource with its worker provided config values and its configuration and api provided tags.'; + +-- Replaces the view created in 9/01. +-- Remove the worker id from this view. In actuality this is almost a no-op +-- because no server information was ever getting populated here due to a bug +-- in the update mask when updating a session at the time we activate a session. +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.scope_id, s.certificate,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; \ No newline at end of file diff --git a/internal/db/testing.go b/internal/db/testing.go index 34fe68e7f..b97c58ff3 100644 --- a/internal/db/testing.go +++ b/internal/db/testing.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/DATA-DOG/go-sqlmock" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/hashicorp/boundary/internal/db/schema" @@ -20,6 +21,7 @@ import ( aead "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + pgDriver "gorm.io/driver/postgres" ) // setup the tests (initialize the database one-time and intialized @@ -73,6 +75,23 @@ func TestSetup(t testing.TB, dialect string, opt ...TestOption) (*DB, string) { return db, url } +// TestSetupWithMock will return a test DB and an associated Sqlmock which can +// be used to mock out the db responses. +func TestSetupWithMock(t *testing.T) (*DB, sqlmock.Sqlmock) { + t.Helper() + require := require.New(t) + db, mock, err := sqlmock.New() + require.NoError(err) + require.NoError(err) + dbw, err := dbw.OpenWith(pgDriver.New(pgDriver.Config{ + Conn: db, + })) + require.NoError(err) + return &DB{ + wrapped: dbw, + }, mock +} + // TestWrapper initializes an AEAD wrapping.Wrapper for testing the oplog func TestWrapper(t testing.TB) wrapping.Wrapper { t.Helper() diff --git a/internal/db/timestamp/timestamp.go b/internal/db/timestamp/timestamp.go index 4910d738b..774a79076 100644 --- a/internal/db/timestamp/timestamp.go +++ b/internal/db/timestamp/timestamp.go @@ -22,5 +22,5 @@ func Now() *Timestamp { // AsTime converts x to a time.Time. func (x *Timestamp) AsTime() time.Time { - return x.Timestamp.AsTime() + return x.GetTimestamp().AsTime() } diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index e83b08ab8..53ff00d02 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -52,6 +52,9 @@ }, { "name": "controller.api.services.v1.UserService" + }, + { + "name": "controller.api.services.v1.WorkerService" } ], "schemes": [ @@ -3661,6 +3664,155 @@ "controller.api.services.v1.UserService" ] } + }, + "/v1/workers": { + "get": { + "summary": "Lists all Workers.", + "operationId": "WorkerService_ListWorkers", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.ListWorkersResponse" + } + } + }, + "parameters": [ + { + "name": "scope_id", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "recursive", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "filter", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.WorkerService" + ] + } + }, + "/v1/workers/{id}": { + "get": { + "summary": "Gets a single Worker.", + "operationId": "WorkerService_GetWorker", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.WorkerService" + ] + }, + "delete": { + "summary": "Deletes a Worker.", + "operationId": "WorkerService_DeleteWorker", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/controller.api.services.v1.DeleteWorkerResponse" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.WorkerService" + ] + }, + "patch": { + "summary": "Updates a Worker.", + "operationId": "WorkerService_UpdateWorker", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + }, + { + "name": "update_mask", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "controller.api.services.v1.WorkerService" + ] + } + }, + "/v1/workers:create:worker-led": { + "post": { + "summary": "Creates a single Worker.", + "operationId": "WorkerService_CreateWorkerLed", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, + "parameters": [ + { + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + ], + "tags": [ + "controller.api.services.v1.WorkerService" + ] + } } }, "definitions": { @@ -4909,14 +5061,6 @@ "description": "Output only. The current status of this Session.", "readOnly": true }, - "worker_info": { - "type": "array", - "items": { - "$ref": "#/definitions/controller.api.resources.sessions.v1.WorkerInfo" - }, - "description": "Output only. Worker information given to the client.", - "readOnly": true - }, "certificate": { "type": "string", "format": "byte", @@ -4968,15 +5112,6 @@ } } }, - "controller.api.resources.sessions.v1.WorkerInfo": { - "type": "object", - "properties": { - "address": { - "type": "string", - "description": "The address of the worker." - } - } - }, "controller.api.resources.targets.v1.CredentialSource": { "type": "object", "properties": { @@ -5386,6 +5521,107 @@ }, "title": "User contains all fields related to a User resource" }, + "controller.api.resources.workers.v1.Worker": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Output only. The ID of the User.", + "readOnly": true + }, + "scope_id": { + "type": "string", + "description": "The ID of the Scope this resource is in." + }, + "scope": { + "$ref": "#/definitions/controller.api.resources.scopes.v1.ScopeInfo", + "description": "Output only. Scope information for this resource.", + "readOnly": true + }, + "name": { + "type": "string", + "description": "Optional name for identification purposes. Can only be set through the API\nfor `pki`-type workers; read-only for `kms`-type workers." + }, + "description": { + "type": "string", + "description": "Optional user-set description for identification purposes. Can only be set\nthrough the API for `pki`-type workers; read-only for `kms`-type workers." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "Output only. The time this resource was created.", + "readOnly": true + }, + "updated_time": { + "type": "string", + "format": "date-time", + "description": "Output only. The time this resource was last updated.", + "readOnly": true + }, + "version": { + "type": "integer", + "format": "int64", + "description": "Version is used in mutation requests, after the initial creation, to ensure this resource has not changed.\nThe mutation will fail if the version does not match the latest known good version." + }, + "address": { + "type": "string", + "description": "Output only. The address that this worker is reachable at, as sourced from\nthe worker's configuration file.", + "readOnly": true + }, + "canonical_tags": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object" + } + }, + "description": "Output only. The deduplicated union of the tags reported by the worker from\nits configuration and any tags added through other means. This is used\nwhen applying worker filters.", + "readOnly": true + }, + "config_tags": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object" + } + }, + "description": "Output only. The tags set in the worker's configuration file.", + "readOnly": true + }, + "last_status_time": { + "type": "string", + "format": "date-time", + "description": "Output only. The time this worker daemon last reported its status.", + "readOnly": true + }, + "worker_generated_auth_token": { + "type": "string", + "title": "worker_generated_auth_token is input only. Supports the worker led node\nenrollment flow where this credentials token is produced by a worker. This\ntoken is a base58 encoded types.FetchNodeCredentialsRequest from\nhttps://github.com/hashicorp/nodeenrollment" + }, + "active_connection_count": { + "type": "integer", + "format": "int64", + "description": "Output only. The number of connections that this worker is currently handling.", + "readOnly": true + }, + "type": { + "type": "string", + "description": "Output only. The type of the worker, denoted by how it authenticates: `pki`\nor `kms`.", + "readOnly": true + }, + "authorized_actions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Output only. The available actions on this resource for the requester.", + "readOnly": true + } + }, + "title": "Worker contains all fields related to a Worker resource" + }, "controller.api.services.v1.AddGroupMembersResponse": { "type": "object", "properties": { @@ -5653,6 +5889,17 @@ } } }, + "controller.api.services.v1.CreateWorkerLedResponse": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "item": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, "controller.api.services.v1.DeleteAccountResponse": { "type": "object" }, @@ -5698,6 +5945,9 @@ "controller.api.services.v1.DeleteUserResponse": { "type": "object" }, + "controller.api.services.v1.DeleteWorkerResponse": { + "type": "object" + }, "controller.api.services.v1.GetAccountResponse": { "type": "object", "properties": { @@ -5826,6 +6076,14 @@ } } }, + "controller.api.services.v1.GetWorkerResponse": { + "type": "object", + "properties": { + "item": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, "controller.api.services.v1.ListAccountsResponse": { "type": "object", "properties": { @@ -6002,6 +6260,17 @@ } } }, + "controller.api.services.v1.ListWorkersResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + } + }, "controller.api.services.v1.RemoveGroupMembersResponse": { "type": "object", "properties": { @@ -6250,6 +6519,14 @@ } } }, + "controller.api.services.v1.UpdateWorkerResponse": { + "type": "object", + "properties": { + "item": { + "$ref": "#/definitions/controller.api.resources.workers.v1.Worker" + } + } + }, "google.protobuf.NullValue": { "type": "string", "enum": [ diff --git a/internal/gen/controller/api/services/worker_service.pb.go b/internal/gen/controller/api/services/worker_service.pb.go new file mode 100644 index 000000000..aa1a7ff14 --- /dev/null +++ b/internal/gen/controller/api/services/worker_service.pb.go @@ -0,0 +1,858 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/api/services/v1/worker_service.proto + +package services + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + workers "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetWorkerRequest 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 *GetWorkerRequest) Reset() { + *x = GetWorkerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetWorkerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkerRequest) ProtoMessage() {} + +func (x *GetWorkerRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkerRequest.ProtoReflect.Descriptor instead. +func (*GetWorkerRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{0} +} + +func (x *GetWorkerRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetWorkerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item *workers.Worker `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *GetWorkerResponse) Reset() { + *x = GetWorkerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetWorkerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkerResponse) ProtoMessage() {} + +func (x *GetWorkerResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkerResponse.ProtoReflect.Descriptor instead. +func (*GetWorkerResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GetWorkerResponse) GetItem() *workers.Worker { + if x != nil { + return x.Item + } + return nil +} + +type ListWorkersRequest 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"` + 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:"sensitive"` // @gotags: `class:"sensitive"` +} + +func (x *ListWorkersRequest) Reset() { + *x = ListWorkersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListWorkersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkersRequest) ProtoMessage() {} + +func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkersRequest.ProtoReflect.Descriptor instead. +func (*ListWorkersRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListWorkersRequest) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *ListWorkersRequest) GetRecursive() bool { + if x != nil { + return x.Recursive + } + return false +} + +func (x *ListWorkersRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +type ListWorkersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*workers.Worker `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *ListWorkersResponse) Reset() { + *x = ListWorkersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListWorkersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkersResponse) ProtoMessage() {} + +func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkersResponse.ProtoReflect.Descriptor instead. +func (*ListWorkersResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ListWorkersResponse) GetItems() []*workers.Worker { + if x != nil { + return x.Items + } + return nil +} + +type CreateWorkerLedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item *workers.Worker `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *CreateWorkerLedRequest) Reset() { + *x = CreateWorkerLedRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateWorkerLedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkerLedRequest) ProtoMessage() {} + +func (x *CreateWorkerLedRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateWorkerLedRequest.ProtoReflect.Descriptor instead. +func (*CreateWorkerLedRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateWorkerLedRequest) GetItem() *workers.Worker { + if x != nil { + return x.Item + } + return nil +} + +type CreateWorkerLedResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty" class:"public"` // @gotags: `class:"public"` + Item *workers.Worker `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *CreateWorkerLedResponse) Reset() { + *x = CreateWorkerLedResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateWorkerLedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkerLedResponse) ProtoMessage() {} + +func (x *CreateWorkerLedResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[5] + 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 CreateWorkerLedResponse.ProtoReflect.Descriptor instead. +func (*CreateWorkerLedResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateWorkerLedResponse) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *CreateWorkerLedResponse) GetItem() *workers.Worker { + if x != nil { + return x.Item + } + return nil +} + +type UpdateWorkerRequest 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"` + Item *workers.Worker `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"` +} + +func (x *UpdateWorkerRequest) Reset() { + *x = UpdateWorkerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateWorkerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateWorkerRequest) ProtoMessage() {} + +func (x *UpdateWorkerRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[6] + 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 UpdateWorkerRequest.ProtoReflect.Descriptor instead. +func (*UpdateWorkerRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateWorkerRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateWorkerRequest) GetItem() *workers.Worker { + if x != nil { + return x.Item + } + return nil +} + +func (x *UpdateWorkerRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type UpdateWorkerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item *workers.Worker `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *UpdateWorkerResponse) Reset() { + *x = UpdateWorkerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateWorkerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateWorkerResponse) ProtoMessage() {} + +func (x *UpdateWorkerResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[7] + 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 UpdateWorkerResponse.ProtoReflect.Descriptor instead. +func (*UpdateWorkerResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateWorkerResponse) GetItem() *workers.Worker { + if x != nil { + return x.Item + } + return nil +} + +type DeleteWorkerRequest 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 *DeleteWorkerRequest) Reset() { + *x = DeleteWorkerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteWorkerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkerRequest) ProtoMessage() {} + +func (x *DeleteWorkerRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[8] + 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 DeleteWorkerRequest.ProtoReflect.Descriptor instead. +func (*DeleteWorkerRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteWorkerRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type DeleteWorkerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *DeleteWorkerResponse) Reset() { + *x = DeleteWorkerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteWorkerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkerResponse) ProtoMessage() {} + +func (x *DeleteWorkerResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_worker_service_proto_msgTypes[9] + 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 DeleteWorkerResponse.ProtoReflect.Descriptor instead. +func (*DeleteWorkerResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_worker_service_proto_rawDescGZIP(), []int{9} +} + +var File_controller_api_services_v1_worker_service_proto protoreflect.FileDescriptor + +var file_controller_api_services_v1_worker_service_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x1a, 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, 0x1a, 0x30, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2f, + 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, + 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x22, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x22, 0x54, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 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, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x65, 0x0a, 0x12, 0x4c, 0x69, 0x73, + 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 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, 0x1c, 0x0a, 0x09, 0x72, 0x65, + 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, + 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x22, 0x58, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 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, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x59, 0x0a, 0x16, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2b, 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, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x6c, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, + 0x72, 0x69, 0x12, 0x3f, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2b, 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, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x22, 0xa4, 0x01, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3f, 0x0a, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 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, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0b, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x57, 0x0a, 0x14, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2b, 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, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x22, 0x25, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0xf2, 0x06, 0x0a, 0x0d, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0xa2, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x57, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x12, 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 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, 0x47, 0x65, + 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x38, 0x92, 0x41, 0x17, 0x12, 0x15, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, + 0x67, 0x6c, 0x65, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x18, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2f, 0x7b, + 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x9a, 0x01, 0x0a, 0x0b, 0x4c, 0x69, + 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 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, 0x4c, 0x69, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 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, 0x4c, 0x69, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x92, 0x41, 0x14, 0x12, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x12, 0x0b, 0x2f, 0x76, 0x31, 0x2f, 0x77, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x12, 0xca, 0x01, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x12, 0x32, 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, 0x57, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x92, 0x41, 0x1a, 0x12, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x22, 0x1d, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x73, 0x3a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x3a, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x2d, 0x6c, 0x65, 0x64, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x12, 0xad, 0x01, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x12, 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x92, 0x41, 0x13, 0x12, 0x11, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x32, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x12, 0xa1, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x12, 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 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, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e, 0x92, 0x41, 0x13, 0x12, 0x11, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x2a, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x42, 0x4d, 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, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_api_services_v1_worker_service_proto_rawDescOnce sync.Once + file_controller_api_services_v1_worker_service_proto_rawDescData = file_controller_api_services_v1_worker_service_proto_rawDesc +) + +func file_controller_api_services_v1_worker_service_proto_rawDescGZIP() []byte { + file_controller_api_services_v1_worker_service_proto_rawDescOnce.Do(func() { + file_controller_api_services_v1_worker_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_services_v1_worker_service_proto_rawDescData) + }) + return file_controller_api_services_v1_worker_service_proto_rawDescData +} + +var file_controller_api_services_v1_worker_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_controller_api_services_v1_worker_service_proto_goTypes = []interface{}{ + (*GetWorkerRequest)(nil), // 0: controller.api.services.v1.GetWorkerRequest + (*GetWorkerResponse)(nil), // 1: controller.api.services.v1.GetWorkerResponse + (*ListWorkersRequest)(nil), // 2: controller.api.services.v1.ListWorkersRequest + (*ListWorkersResponse)(nil), // 3: controller.api.services.v1.ListWorkersResponse + (*CreateWorkerLedRequest)(nil), // 4: controller.api.services.v1.CreateWorkerLedRequest + (*CreateWorkerLedResponse)(nil), // 5: controller.api.services.v1.CreateWorkerLedResponse + (*UpdateWorkerRequest)(nil), // 6: controller.api.services.v1.UpdateWorkerRequest + (*UpdateWorkerResponse)(nil), // 7: controller.api.services.v1.UpdateWorkerResponse + (*DeleteWorkerRequest)(nil), // 8: controller.api.services.v1.DeleteWorkerRequest + (*DeleteWorkerResponse)(nil), // 9: controller.api.services.v1.DeleteWorkerResponse + (*workers.Worker)(nil), // 10: controller.api.resources.workers.v1.Worker + (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask +} +var file_controller_api_services_v1_worker_service_proto_depIdxs = []int32{ + 10, // 0: controller.api.services.v1.GetWorkerResponse.item:type_name -> controller.api.resources.workers.v1.Worker + 10, // 1: controller.api.services.v1.ListWorkersResponse.items:type_name -> controller.api.resources.workers.v1.Worker + 10, // 2: controller.api.services.v1.CreateWorkerLedRequest.item:type_name -> controller.api.resources.workers.v1.Worker + 10, // 3: controller.api.services.v1.CreateWorkerLedResponse.item:type_name -> controller.api.resources.workers.v1.Worker + 10, // 4: controller.api.services.v1.UpdateWorkerRequest.item:type_name -> controller.api.resources.workers.v1.Worker + 11, // 5: controller.api.services.v1.UpdateWorkerRequest.update_mask:type_name -> google.protobuf.FieldMask + 10, // 6: controller.api.services.v1.UpdateWorkerResponse.item:type_name -> controller.api.resources.workers.v1.Worker + 0, // 7: controller.api.services.v1.WorkerService.GetWorker:input_type -> controller.api.services.v1.GetWorkerRequest + 2, // 8: controller.api.services.v1.WorkerService.ListWorkers:input_type -> controller.api.services.v1.ListWorkersRequest + 4, // 9: controller.api.services.v1.WorkerService.CreateWorkerLed:input_type -> controller.api.services.v1.CreateWorkerLedRequest + 6, // 10: controller.api.services.v1.WorkerService.UpdateWorker:input_type -> controller.api.services.v1.UpdateWorkerRequest + 8, // 11: controller.api.services.v1.WorkerService.DeleteWorker:input_type -> controller.api.services.v1.DeleteWorkerRequest + 1, // 12: controller.api.services.v1.WorkerService.GetWorker:output_type -> controller.api.services.v1.GetWorkerResponse + 3, // 13: controller.api.services.v1.WorkerService.ListWorkers:output_type -> controller.api.services.v1.ListWorkersResponse + 5, // 14: controller.api.services.v1.WorkerService.CreateWorkerLed:output_type -> controller.api.services.v1.CreateWorkerLedResponse + 7, // 15: controller.api.services.v1.WorkerService.UpdateWorker:output_type -> controller.api.services.v1.UpdateWorkerResponse + 9, // 16: controller.api.services.v1.WorkerService.DeleteWorker:output_type -> controller.api.services.v1.DeleteWorkerResponse + 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 +} + +func init() { file_controller_api_services_v1_worker_service_proto_init() } +func file_controller_api_services_v1_worker_service_proto_init() { + if File_controller_api_services_v1_worker_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_api_services_v1_worker_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetWorkerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetWorkerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListWorkersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListWorkersResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateWorkerLedRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateWorkerLedResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateWorkerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateWorkerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteWorkerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_worker_service_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteWorkerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_api_services_v1_worker_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_controller_api_services_v1_worker_service_proto_goTypes, + DependencyIndexes: file_controller_api_services_v1_worker_service_proto_depIdxs, + MessageInfos: file_controller_api_services_v1_worker_service_proto_msgTypes, + }.Build() + File_controller_api_services_v1_worker_service_proto = out.File + file_controller_api_services_v1_worker_service_proto_rawDesc = nil + file_controller_api_services_v1_worker_service_proto_goTypes = nil + file_controller_api_services_v1_worker_service_proto_depIdxs = nil +} diff --git a/internal/gen/controller/api/services/worker_service.pb.gw.go b/internal/gen/controller/api/services/worker_service.pb.gw.go new file mode 100644 index 000000000..a9bfb14f3 --- /dev/null +++ b/internal/gen/controller/api/services/worker_service.pb.gw.go @@ -0,0 +1,632 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: controller/api/services/v1/worker_service.proto + +/* +Package services is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package services + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_WorkerService_GetWorker_0(ctx context.Context, marshaler runtime.Marshaler, client WorkerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetWorkerRequest + 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.GetWorker(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WorkerService_GetWorker_0(ctx context.Context, marshaler runtime.Marshaler, server WorkerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetWorkerRequest + 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.GetWorker(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_WorkerService_ListWorkers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_WorkerService_ListWorkers_0(ctx context.Context, marshaler runtime.Marshaler, client WorkerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListWorkersRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkerService_ListWorkers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListWorkers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WorkerService_ListWorkers_0(ctx context.Context, marshaler runtime.Marshaler, server WorkerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListWorkersRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkerService_ListWorkers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListWorkers(ctx, &protoReq) + return msg, metadata, err + +} + +func request_WorkerService_CreateWorkerLed_0(ctx context.Context, marshaler runtime.Marshaler, client WorkerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateWorkerLedRequest + 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.Item); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateWorkerLed(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WorkerService_CreateWorkerLed_0(ctx context.Context, marshaler runtime.Marshaler, server WorkerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateWorkerLedRequest + 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.Item); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateWorkerLed(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_WorkerService_UpdateWorker_0 = &utilities.DoubleArray{Encoding: map[string]int{"item": 0, "id": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} +) + +func request_WorkerService_UpdateWorker_0(ctx context.Context, marshaler runtime.Marshaler, client WorkerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateWorkerRequest + 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.Item); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Item); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + + 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) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkerService_UpdateWorker_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.UpdateWorker(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WorkerService_UpdateWorker_0(ctx context.Context, marshaler runtime.Marshaler, server WorkerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateWorkerRequest + 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.Item); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Item); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + + 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) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkerService_UpdateWorker_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UpdateWorker(ctx, &protoReq) + return msg, metadata, err + +} + +func request_WorkerService_DeleteWorker_0(ctx context.Context, marshaler runtime.Marshaler, client WorkerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteWorkerRequest + 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.DeleteWorker(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WorkerService_DeleteWorker_0(ctx context.Context, marshaler runtime.Marshaler, server WorkerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteWorkerRequest + 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.DeleteWorker(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterWorkerServiceHandlerServer registers the http handlers for service WorkerService to "mux". +// UnaryRPC :call WorkerServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWorkerServiceHandlerFromEndpoint instead. +func RegisterWorkerServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WorkerServiceServer) error { + + mux.Handle("GET", pattern_WorkerService_GetWorker_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 + ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/GetWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkerService_GetWorker_0(ctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_GetWorker_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_GetWorker_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_WorkerService_ListWorkers_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 + ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/ListWorkers", runtime.WithHTTPPathPattern("/v1/workers")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkerService_ListWorkers_0(ctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_ListWorkers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WorkerService_CreateWorkerLed_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 + ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/CreateWorkerLed", runtime.WithHTTPPathPattern("/v1/workers:create:worker-led")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkerService_CreateWorkerLed_0(ctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_CreateWorkerLed_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_CreateWorkerLed_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PATCH", pattern_WorkerService_UpdateWorker_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 + ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/UpdateWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkerService_UpdateWorker_0(ctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_UpdateWorker_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_UpdateWorker_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_WorkerService_DeleteWorker_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 + ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/DeleteWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkerService_DeleteWorker_0(ctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_DeleteWorker_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterWorkerServiceHandlerFromEndpoint is same as RegisterWorkerServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterWorkerServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterWorkerServiceHandler(ctx, mux, conn) +} + +// RegisterWorkerServiceHandler registers the http handlers for service WorkerService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterWorkerServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterWorkerServiceHandlerClient(ctx, mux, NewWorkerServiceClient(conn)) +} + +// RegisterWorkerServiceHandlerClient registers the http handlers for service WorkerService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WorkerServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WorkerServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "WorkerServiceClient" to call the correct interceptors. +func RegisterWorkerServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WorkerServiceClient) error { + + mux.Handle("GET", pattern_WorkerService_GetWorker_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 + ctx, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/GetWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkerService_GetWorker_0(ctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_GetWorker_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_GetWorker_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_WorkerService_ListWorkers_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 + ctx, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/ListWorkers", runtime.WithHTTPPathPattern("/v1/workers")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkerService_ListWorkers_0(ctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_ListWorkers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WorkerService_CreateWorkerLed_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 + ctx, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/CreateWorkerLed", runtime.WithHTTPPathPattern("/v1/workers:create:worker-led")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkerService_CreateWorkerLed_0(ctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_CreateWorkerLed_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_CreateWorkerLed_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PATCH", pattern_WorkerService_UpdateWorker_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 + ctx, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/UpdateWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkerService_UpdateWorker_0(ctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_UpdateWorker_0(ctx, mux, outboundMarshaler, w, req, response_WorkerService_UpdateWorker_0{resp}, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_WorkerService_DeleteWorker_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 + ctx, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.WorkerService/DeleteWorker", runtime.WithHTTPPathPattern("/v1/workers/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkerService_DeleteWorker_0(ctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WorkerService_DeleteWorker_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +type response_WorkerService_GetWorker_0 struct { + proto.Message +} + +func (m response_WorkerService_GetWorker_0) XXX_ResponseBody() interface{} { + response := m.Message.(*GetWorkerResponse) + return response.Item +} + +type response_WorkerService_CreateWorkerLed_0 struct { + proto.Message +} + +func (m response_WorkerService_CreateWorkerLed_0) XXX_ResponseBody() interface{} { + response := m.Message.(*CreateWorkerLedResponse) + return response.Item +} + +type response_WorkerService_UpdateWorker_0 struct { + proto.Message +} + +func (m response_WorkerService_UpdateWorker_0) XXX_ResponseBody() interface{} { + response := m.Message.(*UpdateWorkerResponse) + return response.Item +} + +var ( + pattern_WorkerService_GetWorker_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "workers", "id"}, "")) + + pattern_WorkerService_ListWorkers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "workers"}, "")) + + pattern_WorkerService_CreateWorkerLed_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "workers:create"}, "worker-led")) + + pattern_WorkerService_UpdateWorker_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "workers", "id"}, "")) + + pattern_WorkerService_DeleteWorker_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "workers", "id"}, "")) +) + +var ( + forward_WorkerService_GetWorker_0 = runtime.ForwardResponseMessage + + forward_WorkerService_ListWorkers_0 = runtime.ForwardResponseMessage + + forward_WorkerService_CreateWorkerLed_0 = runtime.ForwardResponseMessage + + forward_WorkerService_UpdateWorker_0 = runtime.ForwardResponseMessage + + forward_WorkerService_DeleteWorker_0 = runtime.ForwardResponseMessage +) diff --git a/internal/gen/controller/api/services/worker_service_grpc.pb.go b/internal/gen/controller/api/services/worker_service_grpc.pb.go new file mode 100644 index 000000000..ee1a181b2 --- /dev/null +++ b/internal/gen/controller/api/services/worker_service_grpc.pb.go @@ -0,0 +1,291 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package services + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// WorkerServiceClient is the client API for WorkerService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WorkerServiceClient interface { + // GetWorker returns a stored Worker if present. The provided request + // must include the Worker ID for the Worker being retrieved. If + // that ID is missing, malformed or reference a non existing + // resource an error is returned. + GetWorker(ctx context.Context, in *GetWorkerRequest, opts ...grpc.CallOption) (*GetWorkerResponse, error) + // ListWorkers returns a list of stored Workers which exist inside the provided + // scope. The request must include the scope ID for the Workers being listed. + // If the scope ID is missing, malformed, or reference a non existing scope, + // an error is returned. + ListWorkers(ctx context.Context, in *ListWorkersRequest, opts ...grpc.CallOption) (*ListWorkersResponse, error) + // CreateWorkerLed creates and stores a Worker in boundary. The provided + // request must include the Scope id in which the Worker will be created. + // If the Scope id is missing, malformed or references a non existing + // resource, an error is returned. If a name is provided that is in + // use in another Worker in the same scope, an error is returned. + CreateWorkerLed(ctx context.Context, in *CreateWorkerLedRequest, opts ...grpc.CallOption) (*CreateWorkerLedResponse, error) + // UpdateWorker updates an existing Worker in boundary. The provided + // Worker must not have any read only fields set. The update mask must be + // included in the request and contain at least 1 mutable field. To unset + // a field's value, include the field in the update mask and don't set it + // in the provided Worker. An error is returned if either the Worker id is + // missing or reference a non existing resource. An error is also returned + // if the request attempts to update the name to one that is already in use + // in this Scope. + UpdateWorker(ctx context.Context, in *UpdateWorkerRequest, opts ...grpc.CallOption) (*UpdateWorkerResponse, error) + // DeleteWorker removes a Worker from Boundary. If the provided Worker ID + // is malformed or not provided an error is returned. + DeleteWorker(ctx context.Context, in *DeleteWorkerRequest, opts ...grpc.CallOption) (*DeleteWorkerResponse, error) +} + +type workerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWorkerServiceClient(cc grpc.ClientConnInterface) WorkerServiceClient { + return &workerServiceClient{cc} +} + +func (c *workerServiceClient) GetWorker(ctx context.Context, in *GetWorkerRequest, opts ...grpc.CallOption) (*GetWorkerResponse, error) { + out := new(GetWorkerResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.WorkerService/GetWorker", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workerServiceClient) ListWorkers(ctx context.Context, in *ListWorkersRequest, opts ...grpc.CallOption) (*ListWorkersResponse, error) { + out := new(ListWorkersResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.WorkerService/ListWorkers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workerServiceClient) CreateWorkerLed(ctx context.Context, in *CreateWorkerLedRequest, opts ...grpc.CallOption) (*CreateWorkerLedResponse, error) { + out := new(CreateWorkerLedResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.WorkerService/CreateWorkerLed", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workerServiceClient) UpdateWorker(ctx context.Context, in *UpdateWorkerRequest, opts ...grpc.CallOption) (*UpdateWorkerResponse, error) { + out := new(UpdateWorkerResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.WorkerService/UpdateWorker", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workerServiceClient) DeleteWorker(ctx context.Context, in *DeleteWorkerRequest, opts ...grpc.CallOption) (*DeleteWorkerResponse, error) { + out := new(DeleteWorkerResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.WorkerService/DeleteWorker", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WorkerServiceServer is the server API for WorkerService service. +// All implementations must embed UnimplementedWorkerServiceServer +// for forward compatibility +type WorkerServiceServer interface { + // GetWorker returns a stored Worker if present. The provided request + // must include the Worker ID for the Worker being retrieved. If + // that ID is missing, malformed or reference a non existing + // resource an error is returned. + GetWorker(context.Context, *GetWorkerRequest) (*GetWorkerResponse, error) + // ListWorkers returns a list of stored Workers which exist inside the provided + // scope. The request must include the scope ID for the Workers being listed. + // If the scope ID is missing, malformed, or reference a non existing scope, + // an error is returned. + ListWorkers(context.Context, *ListWorkersRequest) (*ListWorkersResponse, error) + // CreateWorkerLed creates and stores a Worker in boundary. The provided + // request must include the Scope id in which the Worker will be created. + // If the Scope id is missing, malformed or references a non existing + // resource, an error is returned. If a name is provided that is in + // use in another Worker in the same scope, an error is returned. + CreateWorkerLed(context.Context, *CreateWorkerLedRequest) (*CreateWorkerLedResponse, error) + // UpdateWorker updates an existing Worker in boundary. The provided + // Worker must not have any read only fields set. The update mask must be + // included in the request and contain at least 1 mutable field. To unset + // a field's value, include the field in the update mask and don't set it + // in the provided Worker. An error is returned if either the Worker id is + // missing or reference a non existing resource. An error is also returned + // if the request attempts to update the name to one that is already in use + // in this Scope. + UpdateWorker(context.Context, *UpdateWorkerRequest) (*UpdateWorkerResponse, error) + // DeleteWorker removes a Worker from Boundary. If the provided Worker ID + // is malformed or not provided an error is returned. + DeleteWorker(context.Context, *DeleteWorkerRequest) (*DeleteWorkerResponse, error) + mustEmbedUnimplementedWorkerServiceServer() +} + +// UnimplementedWorkerServiceServer must be embedded to have forward compatible implementations. +type UnimplementedWorkerServiceServer struct { +} + +func (UnimplementedWorkerServiceServer) GetWorker(context.Context, *GetWorkerRequest) (*GetWorkerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetWorker not implemented") +} +func (UnimplementedWorkerServiceServer) ListWorkers(context.Context, *ListWorkersRequest) (*ListWorkersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListWorkers not implemented") +} +func (UnimplementedWorkerServiceServer) CreateWorkerLed(context.Context, *CreateWorkerLedRequest) (*CreateWorkerLedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateWorkerLed not implemented") +} +func (UnimplementedWorkerServiceServer) UpdateWorker(context.Context, *UpdateWorkerRequest) (*UpdateWorkerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateWorker not implemented") +} +func (UnimplementedWorkerServiceServer) DeleteWorker(context.Context, *DeleteWorkerRequest) (*DeleteWorkerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteWorker not implemented") +} +func (UnimplementedWorkerServiceServer) mustEmbedUnimplementedWorkerServiceServer() {} + +// UnsafeWorkerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WorkerServiceServer will +// result in compilation errors. +type UnsafeWorkerServiceServer interface { + mustEmbedUnimplementedWorkerServiceServer() +} + +func RegisterWorkerServiceServer(s grpc.ServiceRegistrar, srv WorkerServiceServer) { + s.RegisterService(&WorkerService_ServiceDesc, srv) +} + +func _WorkerService_GetWorker_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkerServiceServer).GetWorker(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.WorkerService/GetWorker", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkerServiceServer).GetWorker(ctx, req.(*GetWorkerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkerService_ListWorkers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWorkersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkerServiceServer).ListWorkers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.WorkerService/ListWorkers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkerServiceServer).ListWorkers(ctx, req.(*ListWorkersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkerService_CreateWorkerLed_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateWorkerLedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkerServiceServer).CreateWorkerLed(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.WorkerService/CreateWorkerLed", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkerServiceServer).CreateWorkerLed(ctx, req.(*CreateWorkerLedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkerService_UpdateWorker_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateWorkerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkerServiceServer).UpdateWorker(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.WorkerService/UpdateWorker", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkerServiceServer).UpdateWorker(ctx, req.(*UpdateWorkerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkerService_DeleteWorker_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWorkerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkerServiceServer).DeleteWorker(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.WorkerService/DeleteWorker", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkerServiceServer).DeleteWorker(ctx, req.(*DeleteWorkerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WorkerService_ServiceDesc is the grpc.ServiceDesc for WorkerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WorkerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "controller.api.services.v1.WorkerService", + HandlerType: (*WorkerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetWorker", + Handler: _WorkerService_GetWorker_Handler, + }, + { + MethodName: "ListWorkers", + Handler: _WorkerService_ListWorkers_Handler, + }, + { + MethodName: "CreateWorkerLed", + Handler: _WorkerService_CreateWorkerLed_Handler, + }, + { + MethodName: "UpdateWorker", + Handler: _WorkerService_UpdateWorker_Handler, + }, + { + MethodName: "DeleteWorker", + Handler: _WorkerService_DeleteWorker_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "controller/api/services/v1/worker_service.proto", +} diff --git a/internal/gen/controller/servers/services/server_coordination_service.pb.go b/internal/gen/controller/servers/services/server_coordination_service.pb.go index bf6698ec1..9f04a6d8a 100644 --- a/internal/gen/controller/servers/services/server_coordination_service.pb.go +++ b/internal/gen/controller/servers/services/server_coordination_service.pb.go @@ -222,12 +222,61 @@ func (CHANGETYPE) EnumDescriptor() ([]byte, []int) { return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{3} } +type UpstreamServer_TYPE int32 + +const ( + UpstreamServer_TYPE_UNSPECIFIED UpstreamServer_TYPE = 0 + UpstreamServer_TYPE_CONTROLLER UpstreamServer_TYPE = 1 + UpstreamServer_TYPE_WORKER UpstreamServer_TYPE = 2 +) + +// Enum value maps for UpstreamServer_TYPE. +var ( + UpstreamServer_TYPE_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "TYPE_CONTROLLER", + 2: "TYPE_WORKER", + } + UpstreamServer_TYPE_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "TYPE_CONTROLLER": 1, + "TYPE_WORKER": 2, + } +) + +func (x UpstreamServer_TYPE) Enum() *UpstreamServer_TYPE { + p := new(UpstreamServer_TYPE) + *p = x + return p +} + +func (x UpstreamServer_TYPE) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UpstreamServer_TYPE) Descriptor() protoreflect.EnumDescriptor { + return file_controller_servers_services_v1_server_coordination_service_proto_enumTypes[4].Descriptor() +} + +func (UpstreamServer_TYPE) Type() protoreflect.EnumType { + return &file_controller_servers_services_v1_server_coordination_service_proto_enumTypes[4] +} + +func (x UpstreamServer_TYPE) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UpstreamServer_TYPE.Descriptor instead. +func (UpstreamServer_TYPE) EnumDescriptor() ([]byte, []int) { + return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{4, 0} +} + type Connection struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty" class:"public"` // @gotags: `class:"public"` Status CONNECTIONSTATUS `protobuf:"varint,2,opt,name=status,proto3,enum=controller.servers.services.v1.CONNECTIONSTATUS" json:"status,omitempty"` } @@ -282,7 +331,7 @@ type SessionJobInfo struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty" class:"public"` // @gotags: `class:"public"` Status SESSIONSTATUS `protobuf:"varint,2,opt,name=status,proto3,enum=controller.servers.services.v1.SESSIONSTATUS" json:"status,omitempty"` Connections []*Connection `protobuf:"bytes,3,rep,name=connections,proto3" json:"connections,omitempty"` } @@ -462,15 +511,70 @@ func (x *JobStatus) GetJob() *Job { return nil } +// UpstreamServer returns the controller address in the StatusResponse but may be used in the future to return +// worker address. +type UpstreamServer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // type specifies between controller or worker. + Type UpstreamServer_TYPE `protobuf:"varint,10,opt,name=type,proto3,enum=controller.servers.services.v1.UpstreamServer_TYPE" json:"type,omitempty"` + // Address of the daemon + Address string `protobuf:"bytes,20,opt,name=address,proto3" json:"address,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *UpstreamServer) Reset() { + *x = UpstreamServer{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpstreamServer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpstreamServer) ProtoMessage() {} + +func (x *UpstreamServer) ProtoReflect() protoreflect.Message { + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpstreamServer.ProtoReflect.Descriptor instead. +func (*UpstreamServer) Descriptor() ([]byte, []int) { + return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{4} +} + +func (x *UpstreamServer) GetType() UpstreamServer_TYPE { + if x != nil { + return x.Type + } + return UpstreamServer_TYPE_UNSPECIFIED +} + +func (x *UpstreamServer) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + type StatusRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The worker info. We could use information from the TLS connection but this - // is easier and going the other route doesn't provijde much benefit -- if you - // get access to the key and spoof the connection, you're already compromised. - Worker *servers.Server `protobuf:"bytes,10,opt,name=worker,proto3" json:"worker,omitempty"` // Jobs which this worker wants to report the status. Jobs []*JobStatus `protobuf:"bytes,20,rep,name=jobs,proto3" json:"jobs,omitempty"` // Whether to update tags from the Server block on this RPC. We only need to @@ -478,12 +582,17 @@ type StatusRequest struct { // changed allows us to avoid constant database operations for something that // won't change very often, if ever. UpdateTags bool `protobuf:"varint,30,opt,name=update_tags,json=updateTags,proto3" json:"update_tags,omitempty"` + // Replaces the old worker field: + // The worker info. We could use information from the TLS connection but this + // is easier and going the other route doesn't provide much benefit -- if you + // get access to the key and spoof the connection, you're already compromised. + WorkerStatus *servers.ServerWorkerStatus `protobuf:"bytes,40,opt,name=worker_status,json=workerStatus,proto3" json:"worker_status,omitempty"` } func (x *StatusRequest) Reset() { *x = StatusRequest{} if protoimpl.UnsafeEnabled { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[4] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -496,7 +605,7 @@ func (x *StatusRequest) String() string { func (*StatusRequest) ProtoMessage() {} func (x *StatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[4] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -509,14 +618,7 @@ func (x *StatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StatusRequest.ProtoReflect.Descriptor instead. func (*StatusRequest) Descriptor() ([]byte, []int) { - return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{4} -} - -func (x *StatusRequest) GetWorker() *servers.Server { - if x != nil { - return x.Worker - } - return nil + return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{5} } func (x *StatusRequest) GetJobs() []*JobStatus { @@ -533,6 +635,13 @@ func (x *StatusRequest) GetUpdateTags() bool { return false } +func (x *StatusRequest) GetWorkerStatus() *servers.ServerWorkerStatus { + if x != nil { + return x.WorkerStatus + } + return nil +} + type JobChangeRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -545,7 +654,7 @@ type JobChangeRequest struct { func (x *JobChangeRequest) Reset() { *x = JobChangeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[5] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -558,7 +667,7 @@ func (x *JobChangeRequest) String() string { func (*JobChangeRequest) ProtoMessage() {} func (x *JobChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[5] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -571,7 +680,7 @@ func (x *JobChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use JobChangeRequest.ProtoReflect.Descriptor instead. func (*JobChangeRequest) Descriptor() ([]byte, []int) { - return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{5} + return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{6} } func (x *JobChangeRequest) GetJob() *Job { @@ -593,21 +702,23 @@ type StatusResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Active controllers. This can be used (eventually) for connection - // management. - Controllers []*servers.Server `protobuf:"bytes,10,rep,name=controllers,proto3" json:"controllers,omitempty"` // List of jobs and the expected state changes. For example, this will // include jobs witch change type of canceled for jobs which are active on a // worker but should be canceled. This could also contain a request to start a // job such as a worker -> worker proxy for establishing a session through an // enclave. JobsRequests []*JobChangeRequest `protobuf:"bytes,20,rep,name=jobs_requests,json=jobsRequests,proto3" json:"jobs_requests,omitempty"` + // UpstreamServer currently returns the controller address in the StatusResponse. + CalculatedUpstreams []*UpstreamServer `protobuf:"bytes,30,rep,name=calculated_upstreams,json=calculatedUpstreams,proto3" json:"calculated_upstreams,omitempty"` + // The ID of the worker which made the request. The worker can send this value in subsequent requests so the + // controller does not need to do a database lookup for the id using the name field. + WorkerId string `protobuf:"bytes,40,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *StatusResponse) Reset() { *x = StatusResponse{} if protoimpl.UnsafeEnabled { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[6] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +731,7 @@ func (x *StatusResponse) String() string { func (*StatusResponse) ProtoMessage() {} func (x *StatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[6] + mi := &file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,14 +744,7 @@ func (x *StatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead. func (*StatusResponse) Descriptor() ([]byte, []int) { - return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{6} -} - -func (x *StatusResponse) GetControllers() []*servers.Server { - if x != nil { - return x.Controllers - } - return nil + return file_controller_servers_services_v1_server_coordination_service_proto_rawDescGZIP(), []int{7} } func (x *StatusResponse) GetJobsRequests() []*JobChangeRequest { @@ -650,6 +754,20 @@ func (x *StatusResponse) GetJobsRequests() []*JobChangeRequest { return nil } +func (x *StatusResponse) GetCalculatedUpstreams() []*UpstreamServer { + if x != nil { + return x.CalculatedUpstreams + } + return nil +} + +func (x *StatusResponse) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + var File_controller_servers_services_v1_server_coordination_service_proto protoreflect.FileDescriptor var file_controller_servers_services_v1_server_coordination_service_proto_rawDesc = []byte{ @@ -696,79 +814,99 @@ var file_controller_servers_services_v1_server_coordination_service_proto_rawDes 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4a, 0x6f, 0x62, - 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0xa6, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x06, 0x77, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x3d, - 0x0a, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4a, 0x6f, - 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x12, 0x1f, 0x0a, - 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x1e, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, 0x73, 0x22, 0x98, - 0x01, 0x0a, 0x10, 0x4a, 0x6f, 0x62, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x12, 0x4d, 0x0a, 0x0c, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, 0x45, 0x52, 0x0b, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0xa8, 0x01, 0x0a, 0x0e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0b, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x73, 0x12, 0x55, 0x0a, + 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0xb7, 0x01, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x54, 0x59, 0x50, 0x45, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x42, 0x0a, 0x04, 0x54, + 0x59, 0x50, 0x45, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x52, 0x4f, 0x4c, 0x4c, 0x45, 0x52, 0x10, 0x01, 0x12, 0x0f, + 0x0a, 0x0b, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x10, 0x02, 0x22, + 0xcd, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x3d, 0x0a, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x04, 0x6a, 0x6f, 0x62, 0x73, + 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, + 0x1e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x61, 0x67, + 0x73, 0x12, 0x4e, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x52, 0x06, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x22, + 0x98, 0x01, 0x0a, 0x10, 0x4a, 0x6f, 0x62, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x12, 0x4d, 0x0a, 0x0c, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, 0x45, 0x52, 0x0b, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0xfa, 0x01, 0x0a, 0x0e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0d, 0x6a, 0x6f, 0x62, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4a, 0x6f, 0x62, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0c, 0x6a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x73, 0x2a, 0x92, 0x01, 0x0a, 0x10, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, - 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x12, 0x20, 0x0a, 0x1c, 0x43, 0x4f, 0x4e, - 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, 0x0a, 0x1b, 0x43, - 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, - 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, - 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x9e, 0x01, 0x0a, 0x0d, 0x53, 0x45, - 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x12, 0x1d, 0x0a, 0x19, 0x53, - 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x45, - 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, - 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x02, 0x12, - 0x1b, 0x0a, 0x17, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, - 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x54, 0x45, - 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x45, 0x44, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x07, 0x4a, 0x4f, - 0x42, 0x54, 0x59, 0x50, 0x45, 0x12, 0x17, 0x0a, 0x13, 0x4a, 0x4f, 0x42, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, - 0x0a, 0x0f, 0x4a, 0x4f, 0x42, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, - 0x4e, 0x10, 0x01, 0x2a, 0x45, 0x0a, 0x0a, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, - 0x45, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, - 0x17, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, - 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x10, 0x01, 0x32, 0x86, 0x01, 0x0a, 0x19, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x65, 0x73, 0x74, 0x73, 0x12, 0x61, 0x0a, 0x14, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x18, 0x1e, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0x51, 0x5a, 0x4f, 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, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x13, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x55, 0x70, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x49, 0x64, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x73, 0x2a, 0x92, 0x01, 0x0a, 0x10, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x12, 0x20, 0x0a, 0x1c, + 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, + 0x0a, 0x1b, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, + 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x9e, 0x01, 0x0a, + 0x0d, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x12, 0x1d, + 0x0a, 0x19, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, + 0x15, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, + 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x53, 0x53, + 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, + 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, + 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x45, 0x44, 0x10, 0x04, 0x2a, 0x37, 0x0a, + 0x07, 0x4a, 0x4f, 0x42, 0x54, 0x59, 0x50, 0x45, 0x12, 0x17, 0x0a, 0x13, 0x4a, 0x4f, 0x42, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4a, 0x4f, 0x42, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, 0x53, + 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x2a, 0x45, 0x0a, 0x0a, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, + 0x54, 0x59, 0x50, 0x45, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x10, 0x01, 0x32, 0x86, 0x01, + 0x0a, 0x19, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x06, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x5a, 0x4f, 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, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -783,42 +921,45 @@ func file_controller_servers_services_v1_server_coordination_service_proto_rawDe return file_controller_servers_services_v1_server_coordination_service_proto_rawDescData } -var file_controller_servers_services_v1_server_coordination_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_controller_servers_services_v1_server_coordination_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_controller_servers_services_v1_server_coordination_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_controller_servers_services_v1_server_coordination_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_controller_servers_services_v1_server_coordination_service_proto_goTypes = []interface{}{ - (CONNECTIONSTATUS)(0), // 0: controller.servers.services.v1.CONNECTIONSTATUS - (SESSIONSTATUS)(0), // 1: controller.servers.services.v1.SESSIONSTATUS - (JOBTYPE)(0), // 2: controller.servers.services.v1.JOBTYPE - (CHANGETYPE)(0), // 3: controller.servers.services.v1.CHANGETYPE - (*Connection)(nil), // 4: controller.servers.services.v1.Connection - (*SessionJobInfo)(nil), // 5: controller.servers.services.v1.SessionJobInfo - (*Job)(nil), // 6: controller.servers.services.v1.Job - (*JobStatus)(nil), // 7: controller.servers.services.v1.JobStatus - (*StatusRequest)(nil), // 8: controller.servers.services.v1.StatusRequest - (*JobChangeRequest)(nil), // 9: controller.servers.services.v1.JobChangeRequest - (*StatusResponse)(nil), // 10: controller.servers.services.v1.StatusResponse - (*servers.Server)(nil), // 11: controller.servers.v1.Server + (CONNECTIONSTATUS)(0), // 0: controller.servers.services.v1.CONNECTIONSTATUS + (SESSIONSTATUS)(0), // 1: controller.servers.services.v1.SESSIONSTATUS + (JOBTYPE)(0), // 2: controller.servers.services.v1.JOBTYPE + (CHANGETYPE)(0), // 3: controller.servers.services.v1.CHANGETYPE + (UpstreamServer_TYPE)(0), // 4: controller.servers.services.v1.UpstreamServer.TYPE + (*Connection)(nil), // 5: controller.servers.services.v1.Connection + (*SessionJobInfo)(nil), // 6: controller.servers.services.v1.SessionJobInfo + (*Job)(nil), // 7: controller.servers.services.v1.Job + (*JobStatus)(nil), // 8: controller.servers.services.v1.JobStatus + (*UpstreamServer)(nil), // 9: controller.servers.services.v1.UpstreamServer + (*StatusRequest)(nil), // 10: controller.servers.services.v1.StatusRequest + (*JobChangeRequest)(nil), // 11: controller.servers.services.v1.JobChangeRequest + (*StatusResponse)(nil), // 12: controller.servers.services.v1.StatusResponse + (*servers.ServerWorkerStatus)(nil), // 13: controller.servers.v1.ServerWorkerStatus } var file_controller_servers_services_v1_server_coordination_service_proto_depIdxs = []int32{ 0, // 0: controller.servers.services.v1.Connection.status:type_name -> controller.servers.services.v1.CONNECTIONSTATUS 1, // 1: controller.servers.services.v1.SessionJobInfo.status:type_name -> controller.servers.services.v1.SESSIONSTATUS - 4, // 2: controller.servers.services.v1.SessionJobInfo.connections:type_name -> controller.servers.services.v1.Connection + 5, // 2: controller.servers.services.v1.SessionJobInfo.connections:type_name -> controller.servers.services.v1.Connection 2, // 3: controller.servers.services.v1.Job.type:type_name -> controller.servers.services.v1.JOBTYPE - 5, // 4: controller.servers.services.v1.Job.session_info:type_name -> controller.servers.services.v1.SessionJobInfo - 6, // 5: controller.servers.services.v1.JobStatus.job:type_name -> controller.servers.services.v1.Job - 11, // 6: controller.servers.services.v1.StatusRequest.worker:type_name -> controller.servers.v1.Server - 7, // 7: controller.servers.services.v1.StatusRequest.jobs:type_name -> controller.servers.services.v1.JobStatus - 6, // 8: controller.servers.services.v1.JobChangeRequest.job:type_name -> controller.servers.services.v1.Job - 3, // 9: controller.servers.services.v1.JobChangeRequest.request_type:type_name -> controller.servers.services.v1.CHANGETYPE - 11, // 10: controller.servers.services.v1.StatusResponse.controllers:type_name -> controller.servers.v1.Server - 9, // 11: controller.servers.services.v1.StatusResponse.jobs_requests:type_name -> controller.servers.services.v1.JobChangeRequest - 8, // 12: controller.servers.services.v1.ServerCoordinationService.Status:input_type -> controller.servers.services.v1.StatusRequest - 10, // 13: controller.servers.services.v1.ServerCoordinationService.Status:output_type -> controller.servers.services.v1.StatusResponse - 13, // [13:14] is the sub-list for method output_type - 12, // [12:13] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 6, // 4: controller.servers.services.v1.Job.session_info:type_name -> controller.servers.services.v1.SessionJobInfo + 7, // 5: controller.servers.services.v1.JobStatus.job:type_name -> controller.servers.services.v1.Job + 4, // 6: controller.servers.services.v1.UpstreamServer.type:type_name -> controller.servers.services.v1.UpstreamServer.TYPE + 8, // 7: controller.servers.services.v1.StatusRequest.jobs:type_name -> controller.servers.services.v1.JobStatus + 13, // 8: controller.servers.services.v1.StatusRequest.worker_status:type_name -> controller.servers.v1.ServerWorkerStatus + 7, // 9: controller.servers.services.v1.JobChangeRequest.job:type_name -> controller.servers.services.v1.Job + 3, // 10: controller.servers.services.v1.JobChangeRequest.request_type:type_name -> controller.servers.services.v1.CHANGETYPE + 11, // 11: controller.servers.services.v1.StatusResponse.jobs_requests:type_name -> controller.servers.services.v1.JobChangeRequest + 9, // 12: controller.servers.services.v1.StatusResponse.calculated_upstreams:type_name -> controller.servers.services.v1.UpstreamServer + 10, // 13: controller.servers.services.v1.ServerCoordinationService.Status:input_type -> controller.servers.services.v1.StatusRequest + 12, // 14: controller.servers.services.v1.ServerCoordinationService.Status:output_type -> controller.servers.services.v1.StatusResponse + 14, // [14:15] is the sub-list for method output_type + 13, // [13:14] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_controller_servers_services_v1_server_coordination_service_proto_init() } @@ -876,7 +1017,7 @@ func file_controller_servers_services_v1_server_coordination_service_proto_init( } } file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StatusRequest); i { + switch v := v.(*UpstreamServer); i { case 0: return &v.state case 1: @@ -888,7 +1029,7 @@ func file_controller_servers_services_v1_server_coordination_service_proto_init( } } file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*JobChangeRequest); i { + switch v := v.(*StatusRequest); i { case 0: return &v.state case 1: @@ -900,6 +1041,18 @@ func file_controller_servers_services_v1_server_coordination_service_proto_init( } } file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*JobChangeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_servers_services_v1_server_coordination_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StatusResponse); i { case 0: return &v.state @@ -920,8 +1073,8 @@ func file_controller_servers_services_v1_server_coordination_service_proto_init( File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_servers_services_v1_server_coordination_service_proto_rawDesc, - NumEnums: 4, - NumMessages: 7, + NumEnums: 5, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/gen/controller/servers/services/session_service.pb.go b/internal/gen/controller/servers/services/session_service.pb.go index 9665872f9..6d57182bd 100644 --- a/internal/gen/controller/servers/services/session_service.pb.go +++ b/internal/gen/controller/servers/services/session_service.pb.go @@ -32,6 +32,9 @@ type LookupSessionRequest struct { // The name of the requesting worker, used for filtering to ensure this // worker is allowed to handle this session. ServerId string `protobuf:"bytes,20,opt,name=server_id,json=serverId,proto3" json:"server_id,omitempty" class:"public"` // @gotags: `class:"public"` + // The key id of the requesting worker, used for filtering + // if the server name is not provided + WorkerKeyId string `protobuf:"bytes,30,opt,name=worker_key_id,json=workerKeyId,proto3" json:"worker_key_id,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *LookupSessionRequest) Reset() { @@ -80,6 +83,13 @@ func (x *LookupSessionRequest) GetServerId() string { return "" } +func (x *LookupSessionRequest) GetWorkerKeyId() string { + if x != nil { + return x.WorkerKeyId + } + return "" +} + // LookupSessionResponse contains information necessary for a client to // establish a session. type LookupSessionResponse struct { @@ -947,216 +957,218 @@ var file_controller_servers_services_v1_session_service_proto_rawDesc = []byte{ 0x5f, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x14, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x76, 0x0a, 0x14, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, - 0x22, 0xe8, 0x04, 0x0a, 0x15, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x3d, 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, 0x74, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x66, 0x75, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x66, 0x75, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x46, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x5f, 0x6c, 0x65, 0x66, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x66, 0x74, 0x12, 0x17, 0x0a, - 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, - 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x68, 0x6f, 0x73, - 0x74, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x78, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x4d, 0x0a, 0x0b, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x82, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x0b, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x16, - 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x66, 0x75, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x66, 0x75, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, - 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x45, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x45, 0x53, - 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x60, 0x0a, 0x17, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, - 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x5e, 0x0a, 0x15, 0x43, - 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, + 0x12, 0x22, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4b, + 0x65, 0x79, 0x49, 0x64, 0x22, 0xe8, 0x04, 0x0a, 0x15, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, + 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 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, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x44, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x66, 0x75, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x66, 0x75, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x58, 0x0a, 0x1a, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x1b, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x46, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x6c, 0x65, 0x66, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x66, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x5a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x68, 0x6f, + 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x68, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x4d, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, + 0x82, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, + 0xd4, 0x01, 0x0a, 0x16, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x66, + 0x75, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, + 0x6f, 0x66, 0x75, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x60, 0x0a, 0x17, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, + 0x5e, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x45, 0x53, 0x53, 0x49, 0x4f, + 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x58, 0x0a, 0x1a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x1b, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x48, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x6c, 0x65, 0x66, 0x74, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4c, + 0x65, 0x66, 0x74, 0x22, 0xad, 0x02, 0x0a, 0x18, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x63, + 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x65, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2a, 0x0a, + 0x11, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x54, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, + 0x0e, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, + 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x70, 0x22, 0x65, 0x0a, 0x19, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x48, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x1a, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, + 0x0a, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x55, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x22, 0x82, 0x01, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x68, 0x0a, 0x12, 0x63, + 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x10, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x22, 0x8c, 0x01, 0x0a, 0x1b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x48, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x5f, 0x6c, 0x65, 0x66, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x66, 0x74, 0x22, - 0xad, 0x02, 0x0a, 0x18, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x26, 0x0a, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, - 0x72, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x54, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, - 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x32, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x63, - 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x3c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x46, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x22, - 0x65, 0x0a, 0x19, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x4f, - 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x1a, 0x43, 0x6c, 0x6f, 0x73, 0x65, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x55, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, - 0x6f, 0x77, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x28, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x82, 0x01, 0x0a, - 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x68, 0x0a, 0x12, 0x63, 0x6c, 0x6f, 0x73, 0x65, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, - 0x10, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, - 0x61, 0x22, 0x8c, 0x01, 0x0a, 0x1b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x44, 0x61, 0x74, - 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x48, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, - 0x4f, 0x4e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x86, 0x01, 0x0a, 0x17, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x13, - 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x11, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x32, 0xbe, 0x06, 0x0a, 0x0e, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x0d, - 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x2e, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x86, 0x01, 0x0a, 0x17, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x6b, 0x0a, 0x13, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 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, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x84, 0x01, 0x0a, - 0x0f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x11, 0x63, 0x6c, 0x6f, 0x73, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x32, 0xbe, 0x06, + 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x7e, 0x0a, 0x0d, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 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, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x84, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 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, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x63, 0x6f, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, + 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x53, 0x65, 0x73, 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, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8a, 0x01, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x84, 0x01, 0x0a, 0x0f, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x5a, 0x4f, 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, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8a, 0x01, 0x0a, 0x11, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x38, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x84, 0x01, 0x0a, 0x0f, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, + 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, + 0x5a, 0x4f, 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, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/kms/const.go b/internal/kms/const.go index a08fddf3d..a2349fc6d 100644 --- a/internal/kms/const.go +++ b/internal/kms/const.go @@ -27,6 +27,9 @@ const ( // KeyPurposeWorkerAuth is used for worker auth KeyPurposeWorkerAuth + // KeyPurposeWorkerAuthStorage is used for worker credential storage + KeyPurposeWorkerAuthStorage + // KeyPurposeTokens is used for token encryption KeyPurposeTokens @@ -56,6 +59,8 @@ func (k KeyPurpose) String() string { return "recovery" case KeyPurposeWorkerAuth: return "workerauth" + case KeyPurposeWorkerAuthStorage: + return "workerauthstorage" case KeyPurposeTokens: return "tokens" case KeyPurposeSessions: diff --git a/internal/kms/external_wrappers_test.go b/internal/kms/external_wrappers_test.go index be43c61a6..c0cbea429 100644 --- a/internal/kms/external_wrappers_test.go +++ b/internal/kms/external_wrappers_test.go @@ -20,9 +20,10 @@ func TestExternalWrappers(t *testing.T) { rootWrapper := db.TestWrapper(t) recoveryWrapper := db.TestWrapper(t) workerAuthWrapper := db.TestWrapper(t) + workerAuthStorageWrapper := db.TestWrapper(t) k := TestKms(t, conn, rootWrapper) - err := k.AddExternalWrappers(testCtx, WithRecoveryWrapper(recoveryWrapper), WithWorkerAuthWrapper(workerAuthWrapper)) + err := k.AddExternalWrappers(testCtx, WithRecoveryWrapper(recoveryWrapper), WithWorkerAuthWrapper(workerAuthWrapper), WithWorkerAuthStorageWrapper(workerAuthStorageWrapper)) require.NoError(err) assert.Equal(rootWrapper, k.GetExternalWrappers(testCtx).Root()) diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 701329b75..c26e869bb 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -258,7 +258,8 @@ func stdNewKmsPurposes() []wrappingKms.KeyPurpose { for _, p := range ValidDekPurposes() { purposes = append(purposes, wrappingKms.KeyPurpose(p.String())) } - purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), wrappingKms.KeyPurpose(KeyPurposeRecovery.String())) + purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), wrappingKms.KeyPurpose(KeyPurposeWorkerAuthStorage.String()), + wrappingKms.KeyPurpose(KeyPurposeRecovery.String())) return purposes } diff --git a/internal/kms/kms_test.go b/internal/kms/kms_test.go index 83eb47522..f0c5bf91f 100644 --- a/internal/kms/kms_test.go +++ b/internal/kms/kms_test.go @@ -52,7 +52,8 @@ func Test_New(t *testing.T) { for _, p := range ValidDekPurposes() { purposes = append(purposes, wrappingKms.KeyPurpose(p.String())) } - purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), wrappingKms.KeyPurpose(KeyPurposeRecovery.String())) + purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), + wrappingKms.KeyPurpose(KeyPurposeWorkerAuthStorage.String()), wrappingKms.KeyPurpose(KeyPurposeRecovery.String())) r := dbw.New(rw.UnderlyingDB()) w := dbw.New(rw.UnderlyingDB()) diff --git a/internal/kms/options.go b/internal/kms/options.go index ab52ccaa1..6b97f8ed8 100644 --- a/internal/kms/options.go +++ b/internal/kms/options.go @@ -21,16 +21,17 @@ type Option func(*options) // options = how options are represented type options struct { - withLimit int - withRootWrapper wrapping.Wrapper - withWorkerAuthWrapper wrapping.Wrapper - withRecoveryWrapper wrapping.Wrapper - withOrderByVersion db.OrderBy - withKeyId string - withScopeIds []string - withRandomReader io.Reader - withReader db.Reader - withWriter db.Writer + withLimit int + withRootWrapper wrapping.Wrapper + withWorkerAuthWrapper wrapping.Wrapper + withWorkerAuthStorageWrapper wrapping.Wrapper + withRecoveryWrapper wrapping.Wrapper + withOrderByVersion db.OrderBy + withKeyId string + withScopeIds []string + withRandomReader io.Reader + withReader db.Reader + withWriter db.Writer } func getDefaultOptions() options { @@ -61,6 +62,14 @@ func WithWorkerAuthWrapper(w wrapping.Wrapper) Option { } } +// WithWorkerAuthStorageWrapper sets the external pki worker storage wrapper for a +// given scope +func WithWorkerAuthStorageWrapper(w wrapping.Wrapper) Option { + return func(o *options) { + o.withWorkerAuthStorageWrapper = w + } +} + // WithRecoveryWrapper sets the recovery wrapper for a given scope func WithRecoveryWrapper(w wrapping.Wrapper) Option { return func(o *options) { diff --git a/internal/observability/event/cloudevents_formatter_node.go b/internal/observability/event/cloudevents_formatter_node.go index 3dee6dc8f..d457703f2 100644 --- a/internal/observability/event/cloudevents_formatter_node.go +++ b/internal/observability/event/cloudevents_formatter_node.go @@ -143,7 +143,9 @@ func newFilter(f string) (*filter, error) { if f == "" { return nil, fmt.Errorf("%s: missing filter: %w", op, ErrInvalidParameter) } - e, err := bexpr.CreateEvaluator(f, bexpr.WithHookFn(filterpkg.WellKnownTypeFilterHook)) + // explicitly tell the filter to use the "json" tags so we don't have to + // re-tag everything with a mapstructure tag via bexpr.WithTagName("json") + e, err := bexpr.CreateEvaluator(f, bexpr.WithHookFn(filterpkg.WellKnownTypeFilterHook), bexpr.WithTagName("json")) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } diff --git a/internal/observability/event/cloudevents_formatter_node_test.go b/internal/observability/event/cloudevents_formatter_node_test.go index 6ad44f316..2aee2598d 100644 --- a/internal/observability/event/cloudevents_formatter_node_test.go +++ b/internal/observability/event/cloudevents_formatter_node_test.go @@ -154,7 +154,7 @@ func TestNode_Process(t *testing.T) { testNode, err := newCloudEventsFormatterFilter(testUrl, cloudevents.FormatJSON, WithSchema(testUrl)) require.NoError(t, err) - f, err := newFilter(`Data == "match-filter"`) + f, err := newFilter(`data == "match-filter"`) require.NoError(t, err) tests := []struct { diff --git a/internal/observability/event/context_test.go b/internal/observability/event/context_test.go index c145c6863..69990ea29 100644 --- a/internal/observability/event/context_test.go +++ b/internal/observability/event/context_test.go @@ -517,25 +517,25 @@ func Test_Filtering(t *testing.T) { }{ { name: "allowed", - allow: []string{`"/Data/list" contains "1"`}, + allow: []string{`"/data/list" contains "1"`}, hdr: []interface{}{"list", []string{"1", "2"}}, found: true, }, { name: "not-allowed", - allow: []string{`"/Data/list" contains "22"`}, + allow: []string{`"/data/list" contains "22"`}, hdr: []interface{}{"list", []string{"1", "2"}}, found: false, }, { name: "deny", - deny: []string{`"/Data/list" contains "1"`}, + deny: []string{`"/data/list" contains "1"`}, hdr: []interface{}{"list", []string{"1", "2"}}, found: false, }, { name: "not-deny", - deny: []string{`"/Data/list" contains "22"`}, + deny: []string{`"/data/list" contains "22"`}, hdr: []interface{}{"list", []string{"1", "2"}}, found: true, }, diff --git a/internal/observability/event/hclog_formatter_node.go b/internal/observability/event/hclog_formatter_node.go index 7cb36562a..480ba0a6a 100644 --- a/internal/observability/event/hclog_formatter_node.go +++ b/internal/observability/event/hclog_formatter_node.go @@ -36,7 +36,7 @@ type hclogFormatterFilter struct { } func newHclogFormatterFilter(jsonFormat bool, opt ...Option) (*hclogFormatterFilter, error) { - const op = "event.NewHclogFormatter" + const op = "event.newHclogFormatterFilter" opts := getOpts(opt...) var s signer n := hclogFormatterFilter{ @@ -147,7 +147,9 @@ func (f *hclogFormatterFilter) Process(ctx context.Context, e *eventlogger.Event var m map[string]interface{} switch string(e.Type) { case string(ErrorType), string(AuditType), string(SystemType): - m = structs.Map(e.Payload) + s := structs.New(e.Payload) + s.TagName = "json" + m = s.Map() case string(ObservationType): m = e.Payload.(map[string]interface{}) default: diff --git a/internal/observability/event/hclog_formatter_node_test.go b/internal/observability/event/hclog_formatter_node_test.go index d8d717fbd..ab9ba3104 100644 --- a/internal/observability/event/hclog_formatter_node_test.go +++ b/internal/observability/event/hclog_formatter_node_test.go @@ -15,7 +15,7 @@ func TestHclogFormatter_Process(t *testing.T) { t.Parallel() ctx := context.Background() - f, e := newFilter(`Op == "match-filter"`) + f, e := newFilter(`op == "match-filter"`) require.NoError(t, e) testPredicate := newPredicate([]*filter{f}, nil) @@ -60,10 +60,9 @@ func TestHclogFormatter_Process(t *testing.T) { }, want: []string{ "[INFO] system event:", - "Data:msg=hello", - "Id=1", - "Version=v0.1", - "Op=text", + "data:msg=hello", + "version=v0.1", + "op=text", }, }, { @@ -122,10 +121,10 @@ func TestHclogFormatter_Process(t *testing.T) { }, want: []string{ "[ERROR] error event:", - "Error=\"invalid parameter\"", - "Id=1", - "Version=v0.1", - "Op=text", + "error=\"invalid parameter\"", + "id=1", + "version=v0.1", + "op=text", }, }, { @@ -144,10 +143,10 @@ func TestHclogFormatter_Process(t *testing.T) { }, want: []string{ "{\"@level\":\"error\",\"@message\":\"error event\"", - "\"Error\":\"invalid parameter\"", - "\"Id\":\"1\"", - "\"Version\":\"v0.1\"", - "\"Op\":\"text\"", + "\"error\":\"invalid parameter\"", + "\"id\":\"1\"", + "\"version\":\"v0.1\"", + "\"op\":\"text\"", }, }, { @@ -167,11 +166,11 @@ func TestHclogFormatter_Process(t *testing.T) { }, want: []string{ "[ERROR] error event:", - "Error=\"invalid parameter\"", - "Id=1", - "Version=v0.1", - "Info:name=alice", - "Op=text", + "error=\"invalid parameter\"", + "id=1", + "version=v0.1", + "info:name=alice", + "op=text", }, }, { @@ -193,10 +192,9 @@ func TestHclogFormatter_Process(t *testing.T) { }, want: []string{ "[INFO] system event:", - "Data:msg=hello", - "Id=1", - "Version=v0.1", - "Op=match-filter", + "data:msg=hello", + "version=v0.1", + "op=match-filter", }, }, { diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 50bb00bb2..b8e2f3c0d 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -1,18 +1,5 @@ package perms -/* -A really useful page to be aware of when looking at ACLs is -https://hashicorp.atlassian.net/wiki/spaces/ICU/pages/866976600/API+Actions+and+Permissions -speaking of which: TODO: put that chart in public docs. - -Anyways, from that page you can see that there are really only a few patterns of -ACLs that are ever allowed; see the Allowed function for a description along -with the code. - -This makes it actually quite simple to perform the ACL checking. Much of ACL -construction is thus synthesizing something reasonable from a set of Grants. -*/ - import ( "strings" @@ -116,6 +103,13 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option // If the action was not found above but we did find output fields in // patterns that match, we do not authorize the request, but we do build // up the output fields patterns. + // + // Note that when using IsActionOrParent it is merely to test whether it + // is an allowed format since some formats operate ony on collections + // (or don't operate at all on collections) and we want to ensure that + // it is/isn't a create or list command or subcommand to know whether + // that form is valid. The actual checking of whether the given action + // is granted to the user already happened above. var found bool switch { // Case 1: We only allow specific actions on specific types for the @@ -149,8 +143,8 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option grant.id != "" && grant.id != "*" && grant.typ == resource.Unknown && - aType != action.List && - aType != action.Create: + !action.List.IsActionOrParent(aType) && + !action.Create.IsActionOrParent(aType): found = true @@ -174,8 +168,8 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option grant.typ == r.Type && grant.typ != resource.Unknown && topLevelType(r.Type) && - (aType == action.List || - aType == action.Create): + (action.List.IsActionOrParent(aType) || + action.Create.IsActionOrParent(aType)): found = true @@ -226,7 +220,8 @@ func topLevelType(typ resource.Type) bool { resource.Scope, resource.Session, resource.Target, - resource.User: + resource.User, + resource.Worker: return true } return false diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index f0af444f5..14088b423 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -316,6 +316,36 @@ func Test_ACLAllowed(t *testing.T) { {action: action.ReadSelf, authorized: true}, }, }, + { + name: "create worker with create", + resource: Resource{ScopeId: scope.Global.String(), Type: resource.Worker}, + scopeGrants: []scopeGrant{ + { + scope: scope.Global.String(), + grants: []string{ + "type=worker;actions=create", + }, + }, + }, + actionsAuthorized: []actionAuthorized{ + {action: action.CreateWorkerLed, authorized: true}, + }, + }, + { + name: "create worker with request only", + resource: Resource{ScopeId: scope.Global.String(), Type: resource.Worker}, + scopeGrants: []scopeGrant{ + { + scope: scope.Global.String(), + grants: []string{ + "type=worker;actions=create:worker-led", + }, + }, + }, + actionsAuthorized: []actionAuthorized{ + {action: action.CreateWorkerLed, authorized: true}, + }, + }, } for _, test := range tests { diff --git a/internal/perms/grants.go b/internal/perms/grants.go index 440ed0afb..f4817e495 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -74,6 +74,20 @@ func (g Grant) Actions() (typs []action.Type, strs []string) { return } +// hasActionOrSubaction checks whether a grant's action set contains the given +// action or contains an action that is a subaction of the passed-in parameter. +// This is used for validation checking of parsed grants. N.B.: this is the +// opposite check of action.Type.IsActionOrParent, which is why the ordering is +// reversed going into that call. +func (g Grant) hasActionOrSubaction(act action.Type) bool { + for k := range g.actions { + if act.IsActionOrParent(k) { + return true + } + } + return false +} + func (g Grant) clone() *Grant { ret := &Grant{ scope: g.scope, @@ -376,12 +390,12 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "parsed grant string contains no actions or output fields") } case 1: - if !grant.actions[action.Create] && - !grant.actions[action.List] { + if !grant.hasActionOrSubaction(action.Create) && + !grant.hasActionOrSubaction(action.List) { return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "parsed grant string contains non-create or non-list action in a format that only allows these") } case 2: - if !grant.actions[action.Create] || !grant.actions[action.List] { + if !grant.hasActionOrSubaction(action.Create) || !grant.hasActionOrSubaction(action.List) { return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "parsed grant string contains non-create or non-list action in a format that only allows these") } default: @@ -411,6 +425,7 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { results := acl.Allowed(r, k, AnonymousUserId, WithSkipAnonymousUserRestrictions(true)) if results.Authorized { allowed = true + break } } if !allowed { @@ -429,7 +444,7 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { func (g Grant) validateType() error { const op = "perms.(Grant).validateType" switch g.typ { - case resource.Controller, resource.Worker: + case resource.Controller: return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unknown type specifier %q", g.typ)) } return nil diff --git a/internal/perms/grants_test.go b/internal/perms/grants_test.go index 60fff56a3..df1a4671b 100644 --- a/internal/perms/grants_test.go +++ b/internal/perms/grants_test.go @@ -110,7 +110,7 @@ func Test_ValidateType(t *testing.T) { var g Grant for i := resource.Unknown; i <= resource.Credential; i++ { g.typ = i - if i == resource.Controller || i == resource.Worker { + if i == resource.Controller { assert.Error(t, g.validateType()) } else { assert.NoError(t, g.validateType()) @@ -705,3 +705,52 @@ func Test_Parse(t *testing.T) { }) } } + +func TestHasActionOrSubaction(t *testing.T) { + tests := []struct { + name string + base []action.Type + act action.Type + want bool + }{ + { + name: "no actions", + base: []action.Type{}, + act: action.Read, + }, + { + name: "has direct action", + base: []action.Type{action.Cancel, action.Read}, + act: action.Read, + want: true, + }, + { + name: "has parent action", + base: []action.Type{action.Cancel, action.ReadSelf}, + act: action.Read, + want: true, + }, + { + name: "has direct sub action", + base: []action.Type{action.Cancel, action.ReadSelf}, + act: action.ReadSelf, + want: true, + }, + { + name: "has sub action needs parent", + base: []action.Type{action.Cancel, action.Read}, + act: action.ReadSelf, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := Grant{ + actions: make(map[action.Type]bool), + } + for _, act := range tt.base { + g.actions[act] = true + } + assert.Equal(t, tt.want, g.hasActionOrSubaction(tt.act)) + }) + } +} diff --git a/internal/proto/buf.yaml b/internal/proto/buf.yaml index 6aa5d8858..c951d7b3c 100644 --- a/internal/proto/buf.yaml +++ b/internal/proto/buf.yaml @@ -6,6 +6,15 @@ deps: breaking: use: - WIRE_JSON + ignore_only: + FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED: + # The proto files in controller/api are only exposed as json files so enforcing + # proto backward compatability doesn't make sense, except for maybe the names. + - controller/api + # Storage protos are used by gorm. Gorm relies on go tags and the json + # names to determine what columns to update so we don't care of the field + # numbers remain the same. + - controller/storage lint: use: - DEFAULT diff --git a/internal/proto/controller/api/resources/sessions/v1/session.proto b/internal/proto/controller/api/resources/sessions/v1/session.proto index d05233766..f30967a37 100644 --- a/internal/proto/controller/api/resources/sessions/v1/session.proto +++ b/internal/proto/controller/api/resources/sessions/v1/session.proto @@ -7,11 +7,6 @@ import "google/protobuf/timestamp.proto"; option go_package = "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/sessions;sessions"; -message WorkerInfo { - // The address of the worker. - string address = 10; // @gotags: `class:"public"` -} - message SessionState { // The status of the Session, e.g. "pending", "active", "canceling", "terminated". string status = 10; // @gotags: `class:"public"` @@ -49,6 +44,13 @@ message Connection { // Session contains all fields related to a Session resource message Session { + // Worker info previously contained only the worker's address but was never + // populated either in the database or on the API. While worker id information + // may become helpful on a session in the future, the worker id seems the most + // relevant information since the address can change between the time the + // session is created and a session connection is made through that worker. + reserved "worker_info"; + // Output only. The ID of the Session. string id = 10; // @gotags: `class:"public"` @@ -97,9 +99,6 @@ message Session { // Output only. The current status of this Session. string status = 180; // @gotags: `class:"public"` - // Output only. Worker information given to the client. - repeated WorkerInfo worker_info = 190 [json_name = "worker_info"]; - // Output only. The certificate generated for the session. Raw DER bytes. bytes certificate = 200; // @gotags: `class:"public"` diff --git a/internal/proto/controller/api/resources/workers/v1/worker.proto b/internal/proto/controller/api/resources/workers/v1/worker.proto new file mode 100644 index 000000000..9119a6cc4 --- /dev/null +++ b/internal/proto/controller/api/resources/workers/v1/worker.proto @@ -0,0 +1,84 @@ +syntax = "proto3"; + +package controller.api.resources.workers.v1; + +import "controller/api/resources/scopes/v1/scope.proto"; +import "controller/custom_options/v1/options.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/workers;workers"; + +// Worker contains all fields related to a Worker resource +message Worker { + // Output only. The ID of the User. + string id = 10; // @gotags: `class:"public"` + + // The ID of the Scope this resource is in. + string scope_id = 20 [json_name = "scope_id"]; // @gotags: `class:"public"` + + // Output only. Scope information for this resource. + resources.scopes.v1.ScopeInfo scope = 30; + + // Optional name for identification purposes. Can only be set through the API + // for `pki`-type workers; read-only for `kms`-type workers. + google.protobuf.StringValue name = 40 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "name" + that: "name" + } + ]; // @gotags: `class:"sensitive"` + + // Optional user-set description for identification purposes. Can only be set + // through the API for `pki`-type workers; read-only for `kms`-type workers. + google.protobuf.StringValue description = 50 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + } + ]; // @gotags: `class:"sensitive"` + + // Output only. The time this resource was created. + google.protobuf.Timestamp created_time = 60 [json_name = "created_time"]; // @gotags: `class:"public"` + + // Output only. The time this resource was last updated. + google.protobuf.Timestamp updated_time = 70 [json_name = "updated_time"]; // @gotags: `class:"public"` + + // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + uint32 version = 80; // @gotags: `class:"public"` + + // Output only. The address that this worker is reachable at, as sourced from + // the worker's configuration file. + string address = 90; // @gotags: `class:"public"` + + // Output only. The deduplicated union of the tags reported by the worker from + // its configuration and any tags added through other means. This is used + // when applying worker filters. + map canonical_tags = 120 [json_name = "canonical_tags"]; // @gotags: `class:"public"` + + // Output only. The tags set in the worker's configuration file. + map config_tags = 130 [json_name = "config_tags"]; // @gotags: `class:"public"` + + // Output only. The time this worker daemon last reported its status. + google.protobuf.Timestamp last_status_time = 140 [json_name = "last_status_time"]; // @gotags: `class:"public"` + + // worker_generated_auth_token is input only. Supports the worker led node + // enrollment flow where this credentials token is produced by a worker. This + // token is a base58 encoded types.FetchNodeCredentialsRequest from + // https://github.com/hashicorp/nodeenrollment + google.protobuf.StringValue worker_generated_auth_token = 150 [json_name = "worker_generated_auth_token"]; // @gotags: `class:"public"` + + // Output only. The number of connections that this worker is currently handling. + google.protobuf.UInt32Value active_connection_count = 160 [json_name = "active_connection_count"]; // @gotags: `class:"public"` + + // Output only. The type of the worker, denoted by how it authenticates: `pki` + // or `kms`. + string type = 170; + + // Output only. The available actions on this resource for the requester. + repeated string authorized_actions = 300 [json_name = "authorized_actions"]; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/api/services/v1/worker_service.proto b/internal/proto/controller/api/services/v1/worker_service.proto new file mode 100644 index 000000000..f1aac2936 --- /dev/null +++ b/internal/proto/controller/api/services/v1/worker_service.proto @@ -0,0 +1,128 @@ +syntax = "proto3"; + +package controller.api.services.v1; + +import "controller/api/resources/workers/v1/worker.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/field_mask.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/api/services;services"; + +service WorkerService { + // GetWorker returns a stored Worker if present. The provided request + // must include the Worker ID for the Worker being retrieved. If + // that ID is missing, malformed or reference a non existing + // resource an error is returned. + rpc GetWorker(GetWorkerRequest) returns (GetWorkerResponse) { + option (google.api.http) = { + get: "/v1/workers/{id}" + response_body: "item" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Gets a single Worker." + }; + } + + // ListWorkers returns a list of stored Workers which exist inside the provided + // scope. The request must include the scope ID for the Workers being listed. + // If the scope ID is missing, malformed, or reference a non existing scope, + // an error is returned. + rpc ListWorkers(ListWorkersRequest) returns (ListWorkersResponse) { + option (google.api.http) = { + get: "/v1/workers" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Lists all Workers." + }; + } + + // CreateWorkerLed creates and stores a Worker in boundary. The provided + // request must include the Scope id in which the Worker will be created. + // If the Scope id is missing, malformed or references a non existing + // resource, an error is returned. If a name is provided that is in + // use in another Worker in the same scope, an error is returned. + rpc CreateWorkerLed(CreateWorkerLedRequest) returns (CreateWorkerLedResponse) { + option (google.api.http) = { + post: "/v1/workers:create:worker-led" + body: "item" + response_body: "item" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Creates a single Worker." + }; + } + + // UpdateWorker updates an existing Worker in boundary. The provided + // Worker must not have any read only fields set. The update mask must be + // included in the request and contain at least 1 mutable field. To unset + // a field's value, include the field in the update mask and don't set it + // in the provided Worker. An error is returned if either the Worker id is + // missing or reference a non existing resource. An error is also returned + // if the request attempts to update the name to one that is already in use + // in this Scope. + rpc UpdateWorker(UpdateWorkerRequest) returns (UpdateWorkerResponse) { + option (google.api.http) = { + patch: "/v1/workers/{id}" + body: "item" + response_body: "item" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Updates a Worker." + }; + } + + // DeleteWorker removes a Worker from Boundary. If the provided Worker ID + // is malformed or not provided an error is returned. + rpc DeleteWorker(DeleteWorkerRequest) returns (DeleteWorkerResponse) { + option (google.api.http) = { + delete: "/v1/workers/{id}" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Deletes a Worker." + }; + } +} + +message GetWorkerRequest { + string id = 1; // @gotags: `class:"public"` +} + +message GetWorkerResponse { + resources.workers.v1.Worker item = 1; +} + +message ListWorkersRequest { + string scope_id = 1; // @gotags: `class:"public"` + bool recursive = 20 [json_name = "recursive"]; // @gotags: `class:"public"` + string filter = 30 [json_name = "filter"]; // @gotags: `class:"sensitive"` +} + +message ListWorkersResponse { + repeated resources.workers.v1.Worker items = 1; +} + +message CreateWorkerLedRequest { + resources.workers.v1.Worker item = 1; +} + +message CreateWorkerLedResponse { + string uri = 1; // @gotags: `class:"public"` + resources.workers.v1.Worker item = 2; +} + +message UpdateWorkerRequest { + string id = 1; // @gotags: `class:"public"` + resources.workers.v1.Worker item = 2; + google.protobuf.FieldMask update_mask = 3 [json_name = "update_mask"]; +} + +message UpdateWorkerResponse { + resources.workers.v1.Worker item = 1; +} + +message DeleteWorkerRequest { + string id = 1; // @gotags: `class:"public"` +} + +message DeleteWorkerResponse {} diff --git a/internal/proto/controller/servers/services/v1/server_coordination_service.proto b/internal/proto/controller/servers/services/v1/server_coordination_service.proto index 098b2d7fc..ca43cb101 100644 --- a/internal/proto/controller/servers/services/v1/server_coordination_service.proto +++ b/internal/proto/controller/servers/services/v1/server_coordination_service.proto @@ -21,7 +21,7 @@ enum CONNECTIONSTATUS { } message Connection { - string connection_id = 1; + string connection_id = 1; // @gotags: `class:"public"` CONNECTIONSTATUS status = 2; } @@ -34,7 +34,7 @@ enum SESSIONSTATUS { } message SessionJobInfo { - string session_id = 1; + string session_id = 1; // @gotags: `class:"public"` SESSIONSTATUS status = 2; repeated Connection connections = 3; } @@ -56,11 +56,25 @@ message JobStatus { Job job = 1; } +// UpstreamServer returns the controller address in the StatusResponse but may be used in the future to return +// worker address. +message UpstreamServer { + enum TYPE { + TYPE_UNSPECIFIED = 0; + TYPE_CONTROLLER = 1; + TYPE_WORKER = 2; + } + + // type specifies between controller or worker. + TYPE type = 10; + + // Address of the daemon + string address = 20; // @gotags: `class:"public"` +} + message StatusRequest { - // The worker info. We could use information from the TLS connection but this - // is easier and going the other route doesn't provijde much benefit -- if you - // get access to the key and spoof the connection, you're already compromised. - servers.v1.Server worker = 10; + reserved 10; + reserved "worker"; // Jobs which this worker wants to report the status. repeated JobStatus jobs = 20; @@ -70,6 +84,12 @@ message StatusRequest { // changed allows us to avoid constant database operations for something that // won't change very often, if ever. bool update_tags = 30; + + // Replaces the old worker field: + // The worker info. We could use information from the TLS connection but this + // is easier and going the other route doesn't provide much benefit -- if you + // get access to the key and spoof the connection, you're already compromised. + servers.v1.ServerWorkerStatus worker_status = 40; } enum CHANGETYPE { @@ -85,9 +105,8 @@ message JobChangeRequest { } message StatusResponse { - // Active controllers. This can be used (eventually) for connection - // management. - repeated servers.v1.Server controllers = 10; + reserved 10; + reserved "controllers"; // List of jobs and the expected state changes. For example, this will // include jobs witch change type of canceled for jobs which are active on a @@ -95,4 +114,11 @@ message StatusResponse { // job such as a worker -> worker proxy for establishing a session through an // enclave. repeated JobChangeRequest jobs_requests = 20; + + // UpstreamServer currently returns the controller address in the StatusResponse. + repeated UpstreamServer calculated_upstreams = 30; + + // The ID of the worker which made the request. The worker can send this value in subsequent requests so the + // controller does not need to do a database lookup for the id using the name field. + string worker_id = 40; // @gotags: `class:"public"` } diff --git a/internal/proto/controller/servers/services/v1/session_service.proto b/internal/proto/controller/servers/services/v1/session_service.proto index ea022b449..3286a8870 100644 --- a/internal/proto/controller/servers/services/v1/session_service.proto +++ b/internal/proto/controller/servers/services/v1/session_service.proto @@ -36,6 +36,9 @@ message LookupSessionRequest { // The name of the requesting worker, used for filtering to ensure this // worker is allowed to handle this session. string server_id = 20; // @gotags: `class:"public"` + // The key id of the requesting worker, used for filtering + // if the server name is not provided + string worker_key_id = 30; // @gotags: `class:"public"` } // LookupSessionResponse contains information necessary for a client to diff --git a/internal/proto/controller/servers/v1/servers.proto b/internal/proto/controller/servers/v1/servers.proto index 493db240a..2b2ac63cd 100644 --- a/internal/proto/controller/servers/v1/servers.proto +++ b/internal/proto/controller/servers/v1/servers.proto @@ -2,44 +2,31 @@ syntax = "proto3"; package controller.servers.v1; -import "controller/storage/timestamp/v1/timestamp.proto"; - option go_package = "github.com/hashicorp/boundary/internal/servers;servers"; -// Server contains all fields related to a Controller or Worker resource -message Server { - // Private ID of the resource - string private_id = 10; +// TagPair matches a key to a value. +message TagPair { + string key = 1; // @gotags: `class:"public"` + string value = 2; // @gotags: `class:"public"` +} - // Type of the resource (controller, worker) - string type = 20; +// ServerWorkerStatus is the new message used in place of Server to relay status request info. +message ServerWorkerStatus { + // Id of the worker. + string public_id = 10; // @gotags: `class:"public"` - // Name of the resource. This is deprecated because we ended up setting the - // private ID and name to be identical, and there is currently no reason we - // can think of to allow a given server's name to match another within the - // same cluster. So we simply use the private ID and deprecate this. - // @inject_tag: `gorm:"-"` - string name = 30 [deprecated = true]; + // Name of the resource (optional) + string name = 20; // @gotags: `class:"public"` - // Description of the resource - string description = 40; + // Description of the resource (optional) + string description = 25; // @gotags: `class:"public"` - // Address for the server - string address = 50; - - // First seen time from the RDBMS - storage.timestamp.v1.Timestamp create_time = 60; - - // Last time there was an update - storage.timestamp.v1.Timestamp update_time = 70; + // Address for the worker. This must be set. + string address = 30; // @gotags: `class:"public"` // Tags for workers - // @inject_tag: `gorm:"-"` - map tags = 80; -} + repeated TagPair tags = 40; -// TagValues is used because map fields cannot be repeated but can be a -// message -message TagValues { - repeated string values = 10; + // The key id for this worker, if applicable (optional) + string key_id = 50; // @gotags: `class:"public"` } diff --git a/internal/proto/controller/storage/job/store/v1/job.proto b/internal/proto/controller/storage/job/store/v1/job.proto index dd8c3caca..f5cdb1638 100644 --- a/internal/proto/controller/storage/job/store/v1/job.proto +++ b/internal/proto/controller/storage/job/store/v1/job.proto @@ -26,6 +26,12 @@ message Job { } message JobRun { + // server_id referenced an server table which was replaced with both a + // server_controller table and a server_worker table. For the purpose of + // jobs, only the controller table is relevant thus we replaced this column + // with controller_id. + reserved "server_id"; + // private_id is used to access the job run but not intended to be available via the API // @inject_tag: `gorm:"primary_key;default:null"` string private_id = 1; @@ -38,10 +44,6 @@ message JobRun { // @inject_tag: `gorm:"not_null"` string job_name = 3; - // The server_id of the server running the job and must be set. - // @inject_tag: `gorm:"not_null"` - string server_id = 4; - // The create_time is set by the database. // @inject_tag: `gorm:"default:current_timestamp"` timestamp.v1.Timestamp create_time = 5; @@ -65,4 +67,8 @@ message JobRun { // status of the job run (running, completed, failed or interrupted). // @inject_tag: `gorm:"not_null"` string status = 10; + + // The controller_id of the controller running the job and must be set. + // @inject_tag: `gorm:"not_null"` + string controller_id = 11; } diff --git a/internal/proto/controller/storage/servers/store/v1/controller.proto b/internal/proto/controller/storage/servers/store/v1/controller.proto new file mode 100644 index 000000000..f952ae983 --- /dev/null +++ b/internal/proto/controller/storage/servers/store/v1/controller.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package controller.storage.servers.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/servers/store;store"; + +// Controller contains all fields related to a Controller resource +message Controller { + // Private ID of the resource + string private_id = 10; + + // Address for the controller + string address = 20; + + // Description of the resource (optional) + // @inject_tag: `gorm:"default:null"` + string description = 30; + + // First seen time from the RDBMS + storage.timestamp.v1.Timestamp create_time = 40; + + // Last time there was an update + storage.timestamp.v1.Timestamp update_time = 50; +} diff --git a/internal/proto/controller/storage/servers/store/v1/root_certificate.proto b/internal/proto/controller/storage/servers/store/v1/root_certificate.proto new file mode 100644 index 000000000..1094e0bde --- /dev/null +++ b/internal/proto/controller/storage/servers/store/v1/root_certificate.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the pki package. +package controller.storage.servers.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/servers/store;store"; + +// CertificateAuthority is a versioned entity used to lock the database when rotation RootCertificates +message CertificateAuthority { + // @inject_tag: `gorm:"primary_key"` + string private_id = 10; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 20; +} + +// RootCertificate contains all fields related to a RootCertificate resource +message RootCertificate { + // The serial number of the root certificate + // @inject_tag: `gorm:"not_null"` + uint64 serial_number = 10; + + // Certificate is the PEM encoded certificate. + // @inject_tag: `gorm:"not_null"` + bytes certificate = 20; + + // Not valid before is the timestamp at which this certificate's validity period starts + storage.timestamp.v1.Timestamp not_valid_before = 30; + + // Not valid after is the timestamp at which this certificate's validity period ends + storage.timestamp.v1.Timestamp not_valid_after = 40; + + // The public key associated with this certificate + // @inject_tag: `gorm:"not_null"` + bytes public_key = 50; + + // The private key associated with this certificate + // This is a ciphertext field + // @inject_tag: `gorm:"not_null"` + bytes private_key = 60; + + // The id of the kms database key used for encrypting this entry. + // @inject_tag: `gorm:"not_null"` + string key_id = 70; + + // State is an enum value indicating if this is the next or current root cert + // @inject_tag: `gorm:"not_null"` + string state = 80; + + // A reference to the CertificateAuthority + // @inject_tag: `gorm:"not_null"` + string issuing_ca = 90; +} diff --git a/internal/proto/controller/storage/servers/store/v1/worker.proto b/internal/proto/controller/storage/servers/store/v1/worker.proto new file mode 100644 index 000000000..20c83d8a3 --- /dev/null +++ b/internal/proto/controller/storage/servers/store/v1/worker.proto @@ -0,0 +1,77 @@ +syntax = "proto3"; + +package controller.storage.servers.store.v1; + +import "controller/custom_options/v1/options.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/servers/store;store"; + +// Worker contains all fields related to a Worker resource +message Worker { + // public_id is a surrogate key suitable for use in a public API + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // Name of the resource (optional) + // @inject_tag: `gorm:"default:null"` + string name = 20 [(custom_options.v1.mask_mapping) = { + this: "name" + that: "name" + }]; + + // Description of the resource (optional) + // @inject_tag: `gorm:"default:null"` + string description = 30 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // Address for the worker. This is optional. + // @inject_tag: `gorm:"default:null"` + string address = 40; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 50; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 60; + + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 70; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 80; + + // The last_status_time is the last time a worker daemon sent a status update. + // @inject_tag: `gorm:"default:null"` + timestamp.v1.Timestamp last_status_time = 110; + + // The type of the worker, denoted by how it authenticates: pki or kms. + // @inject_tag: `gorm:"not_null"` + string type = 130; +} + +// WorkerTag is a tag for a worker. The primary key is comprised of the +// worker_id, key, value, and source. +message WorkerTag { + // worker_id is the public key that key of the worker this tag is for. + // @inject_tag: `gorm:"primary_key"` + string worker_id = 10; + + // key is the key of the tag. This must be set. + // @inject_tag: `gorm:"primary_key"` + string key = 20; + + // value is the value + // @inject_tag: `gorm:"primary_key"` + string value = 30; + + // source is the source of the tag. Either 'configuration' or 'api'. + // @inject_tag: `gorm:"default:not_null"` + string source = 40; +} diff --git a/internal/proto/controller/storage/servers/store/v1/worker_auth.proto b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto new file mode 100644 index 000000000..fd7d91d87 --- /dev/null +++ b/internal/proto/controller/storage/servers/store/v1/worker_auth.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the pki package. +package controller.storage.servers.store.v1; + +option go_package = "github.com/hashicorp/boundary/internal/servers/store;store"; + +// WorkerAuth contains all fields related to an authorized Worker resource +message WorkerAuth { + // The key id for this worker auth record, generated from the signing pub key + // @inject_tag: `gorm:"primary_key"` + string worker_key_identifier = 10; + + // The worker id this worker authentication record is for + // @inject_tag: `gorm:"not_null"` + string worker_id = 20; + + // The worker's public signing key + // @inject_tag: `gorm:"not_null"` + bytes worker_signing_pub_key = 30; + + // The worker's public encryption key + // @inject_tag: `gorm:"not_null"` + bytes worker_encryption_pub_key = 40; + + // The private key associated with this certificate + // This is a ciphertext field + // @inject_tag: `gorm:"not_null"` + bytes controller_encryption_priv_key = 50; + + // The id of the kms database key used for encrypting this entry. + // @inject_tag: `gorm:"not_null"` + string key_id = 60; + + // Nonce used by a worker in authenticating + // @inject_tag: `gorm:"default:null"` + bytes nonce = 70; +} + +// WorkerCertBundle contains all fields related to a WorkerCertBundle resource +message WorkerCertBundle { + // The public key of the issuing root certificate + // @inject_tag: `gorm:"primary_key"` + bytes root_certificate_public_key = 10; + + // The WorkerAuth worker_key_identifier this cert bundle record is for + // @inject_tag: `gorm:"primary_key"` + string worker_key_identifier = 20; + + // CertBundle is the marshaled protobuf certificate bundle for a WorkerAuth + // @inject_tag: `gorm:"not_null"` + bytes cert_bundle = 30; +} diff --git a/internal/scheduler/job/options.go b/internal/scheduler/job/options.go index f1413d379..961f15dbd 100644 --- a/internal/scheduler/job/options.go +++ b/internal/scheduler/job/options.go @@ -27,7 +27,7 @@ type options struct { withRunJobsLimit int withLimit int withName string - withServerId string + withControllerId string } func getDefaultOptions() options { @@ -73,9 +73,9 @@ func WithName(n string) Option { } } -// WithServerId provides an option to provide the server id to match when calling InterruptRuns -func WithServerId(id string) Option { +// WithControllerId provides an option to provide the server id to match when calling InterruptRuns +func WithControllerId(id string) Option { return func(o *options) { - o.withServerId = id + o.withControllerId = id } } diff --git a/internal/scheduler/job/options_test.go b/internal/scheduler/job/options_test.go index cf7f768c9..b2574db34 100644 --- a/internal/scheduler/job/options_test.go +++ b/internal/scheduler/job/options_test.go @@ -49,12 +49,12 @@ func Test_GetOpts(t *testing.T) { testOpts.withName = "name" assert.Equal(opts, testOpts) }) - t.Run("WithServerId", func(t *testing.T) { + t.Run("WithControllerId", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithServerId("server_id")) + opts := getOpts(WithControllerId("controller_id")) testOpts := getDefaultOptions() assert.NotEqual(opts, testOpts) - testOpts.withServerId = "server_id" + testOpts.withControllerId = "controller_id" assert.Equal(opts, testOpts) }) } diff --git a/internal/scheduler/job/query.go b/internal/scheduler/job/query.go index a4e93ee15..72341844b 100644 --- a/internal/scheduler/job/query.go +++ b/internal/scheduler/job/query.go @@ -2,7 +2,7 @@ package job const runJobsQuery = ` insert into job_run ( - job_plugin_id, job_name, server_id + job_plugin_id, job_name, controller_id ) select job_plugin_id, job_name, ? diff --git a/internal/scheduler/job/repository_run.go b/internal/scheduler/job/repository_run.go index 0137fb29d..17962bd27 100644 --- a/internal/scheduler/job/repository_run.go +++ b/internal/scheduler/job/repository_run.go @@ -265,7 +265,7 @@ func (r *Repository) FailRun(ctx context.Context, runId string, completed, total // Once a run has been persisted with a final run status (completed, failed // or interrupted), any future calls to InterruptRuns will return an error with Code // errors.InvalidJobRunState. -// WithServerId is the only valid option +// WithControllerId is the only valid option func (r *Repository) InterruptRuns(ctx context.Context, interruptThreshold time.Duration, opt ...Option) ([]*Run, error) { const op = "job.(Repository).InterruptRuns" @@ -273,12 +273,12 @@ func (r *Repository) InterruptRuns(ctx context.Context, interruptThreshold time. // interruptThreshold is seconds in past so * -1 args := []interface{}{-1 * int(interruptThreshold.Round(time.Second).Seconds())} - var whereServerId string - if opts.withServerId != "" { - whereServerId = "and server_id = ?" - args = append(args, opts.withServerId) + var whereControllerId string + if opts.withControllerId != "" { + whereControllerId = "and controller_id = ?" + args = append(args, opts.withControllerId) } - query := fmt.Sprintf(interruptRunsQuery, whereServerId) + query := fmt.Sprintf(interruptRunsQuery, whereControllerId) var runs []*Run _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, diff --git a/internal/scheduler/job/repository_run_test.go b/internal/scheduler/job/repository_run_test.go index ed8b66160..73947fc95 100644 --- a/internal/scheduler/job/repository_run_test.go +++ b/internal/scheduler/job/repository_run_test.go @@ -27,30 +27,30 @@ func TestRepository_RunJobs(t *testing.T) { server := testController(t, conn, wrapper) tests := []struct { - name string - serverId string - job *Job - wantRun bool - wantErr bool - wantErrCode errors.Code - wantErrMsg string + name string + ControllerId string + job *Job + wantRun bool + wantErr bool + wantErrCode errors.Code + wantErrMsg string }{ { - name: "missing-server-id", - serverId: "", - wantErr: true, - wantErrCode: errors.InvalidParameter, - wantErrMsg: "job.(Repository).RunJobs: missing server id: parameter violation: error #100", + name: "missing-server-id", + ControllerId: "", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrMsg: "job.(Repository).RunJobs: missing server id: parameter violation: error #100", }, { - name: "no-work", - serverId: server.PrivateId, - wantRun: false, - wantErr: false, + name: "no-work", + ControllerId: server.PrivateId, + wantRun: false, + wantErr: false, }, { - name: "valid", - serverId: server.PrivateId, + name: "valid", + ControllerId: server.PrivateId, job: &Job{ Job: &store.Job{ Name: "valid-test", @@ -60,8 +60,8 @@ func TestRepository_RunJobs(t *testing.T) { wantRun: true, }, { - name: "fake-server-id", - serverId: "fake-server-id", + name: "fake-server-id", + ControllerId: "fake-server-id", job: &Job{ Job: &store.Job{ Name: "fake-server-id-test", @@ -70,7 +70,7 @@ func TestRepository_RunJobs(t *testing.T) { }, wantErr: true, wantErrCode: errors.NotSpecificIntegrity, - wantErrMsg: "job.(Repository).RunJobs: db.DoTx: job.(Repository).RunJobs: db.Query: insert or update on table \"job_run\" violates foreign key constraint \"server_fkey\": integrity violation: error #1003", + wantErrMsg: "job.(Repository).RunJobs: db.DoTx: job.(Repository).RunJobs: db.Query: insert or update on table \"job_run\" violates foreign key constraint \"server_controller_fkey\": integrity violation: error #1003", }, } for _, tt := range tests { @@ -84,7 +84,7 @@ func TestRepository_RunJobs(t *testing.T) { assert.NoError(err) require.NotNil(repo) - got, err := repo.RunJobs(context.Background(), tt.serverId) + got, err := repo.RunJobs(context.Background(), tt.ControllerId) if tt.wantErr { require.Error(err) assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "Unexpected error %s", err) @@ -263,10 +263,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "status-already-interrupted", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Interrupted.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Interrupted.string(), }, }, wantErr: true, @@ -277,10 +277,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "status-already-failed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Failed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Failed.string(), }, }, wantErr: true, @@ -291,10 +291,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "status-already-completed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Completed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Completed.string(), }, }, wantErr: true, @@ -305,10 +305,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "valid-no-changes", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, }, @@ -316,10 +316,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "valid-update-total", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, args: args{ @@ -333,11 +333,11 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "valid-update-completed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), - TotalCount: 10, + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), + TotalCount: 10, }, }, args: args{ @@ -353,10 +353,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "valid-update-completed-and-total", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, args: args{ @@ -372,10 +372,10 @@ func TestRepository_UpdateProgress(t *testing.T) { name: "invalid-completed-greater-than-total", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, args: args{ @@ -477,10 +477,10 @@ func TestRepository_CompleteRun(t *testing.T) { name: "status-already-interrupted", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Interrupted.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Interrupted.string(), }, }, wantErr: true, @@ -491,10 +491,10 @@ func TestRepository_CompleteRun(t *testing.T) { name: "status-already-failed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Failed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Failed.string(), }, }, wantErr: true, @@ -505,10 +505,10 @@ func TestRepository_CompleteRun(t *testing.T) { name: "status-already-completed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Completed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Completed.string(), }, }, wantErr: true, @@ -519,10 +519,10 @@ func TestRepository_CompleteRun(t *testing.T) { name: "valid", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, nextRunIn: time.Hour, @@ -531,10 +531,10 @@ func TestRepository_CompleteRun(t *testing.T) { name: "valid-with-progress", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, args: args{completed: 10, total: 20}, @@ -641,10 +641,10 @@ func TestRepository_FailRun(t *testing.T) { name: "status-already-interrupted", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Interrupted.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Interrupted.string(), }, }, wantErr: true, @@ -655,10 +655,10 @@ func TestRepository_FailRun(t *testing.T) { name: "status-already-failed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Failed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Failed.string(), }, }, wantErr: true, @@ -669,10 +669,10 @@ func TestRepository_FailRun(t *testing.T) { name: "status-already-completed", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Completed.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Completed.string(), }, }, wantErr: true, @@ -683,10 +683,10 @@ func TestRepository_FailRun(t *testing.T) { name: "valid", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, }, @@ -694,10 +694,10 @@ func TestRepository_FailRun(t *testing.T) { name: "valid-with-progress", orig: &Run{ JobRun: &store.JobRun{ - JobName: job.Name, - JobPluginId: job.PluginId, - ServerId: server.PrivateId, - Status: Running.string(), + JobName: job.Name, + JobPluginId: job.PluginId, + ControllerId: server.PrivateId, + Status: Running.string(), }, }, args: args{completed: 10, total: 20}, @@ -869,7 +869,7 @@ func TestRepository_InterruptServerRuns(t *testing.T) { job3 := testJob(t, conn, "job3", "description", wrapper) type args struct { - serverId string + ControllerId string opts []Option expectedJobs []*Job } @@ -882,7 +882,7 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "all-runs", runs: []args{ { - serverId: server1.PrivateId, + ControllerId: server1.PrivateId, opts: []Option{WithRunJobsLimit(3)}, expectedJobs: []*Job{job1, job2, job3}, }, @@ -897,22 +897,22 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "all-runs-on-single-server-with-server-id", runs: []args{ { - serverId: server2.PrivateId, + ControllerId: server2.PrivateId, opts: []Option{WithRunJobsLimit(3)}, expectedJobs: []*Job{job1, job2, job3}, }, }, interrupts: []args{ { - opts: []Option{WithServerId(server1.PrivateId)}, + opts: []Option{WithControllerId(server1.PrivateId)}, expectedJobs: []*Job{}, }, { - opts: []Option{WithServerId(server2.PrivateId)}, + opts: []Option{WithControllerId(server2.PrivateId)}, expectedJobs: []*Job{job1, job2, job3}, }, { - opts: []Option{WithServerId(server3.PrivateId)}, + opts: []Option{WithControllerId(server3.PrivateId)}, expectedJobs: []*Job{}, }, }, @@ -931,15 +931,15 @@ func TestRepository_InterruptServerRuns(t *testing.T) { runs: []args{}, interrupts: []args{ { - opts: []Option{WithServerId(server1.PrivateId)}, + opts: []Option{WithControllerId(server1.PrivateId)}, expectedJobs: []*Job{}, }, { - opts: []Option{WithServerId(server2.PrivateId)}, + opts: []Option{WithControllerId(server2.PrivateId)}, expectedJobs: []*Job{}, }, { - opts: []Option{WithServerId(server3.PrivateId)}, + opts: []Option{WithControllerId(server3.PrivateId)}, expectedJobs: []*Job{}, }, }, @@ -948,17 +948,17 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "multiple-servers-interrupt-all", runs: []args{ { - serverId: server1.PrivateId, + ControllerId: server1.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job1}, }, { - serverId: server2.PrivateId, + ControllerId: server2.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job2}, }, { - serverId: server3.PrivateId, + ControllerId: server3.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job3}, }, @@ -973,32 +973,32 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "multiple-servers-with-server-id", runs: []args{ { - serverId: server1.PrivateId, + ControllerId: server1.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job1}, }, { - serverId: server2.PrivateId, + ControllerId: server2.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job2}, }, { - serverId: server3.PrivateId, + ControllerId: server3.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job3}, }, }, interrupts: []args{ { - opts: []Option{WithServerId(server1.PrivateId)}, + opts: []Option{WithControllerId(server1.PrivateId)}, expectedJobs: []*Job{job1}, }, { - opts: []Option{WithServerId(server2.PrivateId)}, + opts: []Option{WithControllerId(server2.PrivateId)}, expectedJobs: []*Job{job2}, }, { - opts: []Option{WithServerId(server3.PrivateId)}, + opts: []Option{WithControllerId(server3.PrivateId)}, expectedJobs: []*Job{job3}, }, }, @@ -1007,32 +1007,32 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "multiple-servers-distributed-runs", runs: []args{ { - serverId: server1.PrivateId, + ControllerId: server1.PrivateId, opts: []Option{WithRunJobsLimit(2)}, expectedJobs: []*Job{job1, job2}, }, { - serverId: server2.PrivateId, + ControllerId: server2.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job3}, }, { - serverId: server3.PrivateId, + ControllerId: server3.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{}, }, }, interrupts: []args{ { - opts: []Option{WithServerId(server1.PrivateId)}, + opts: []Option{WithControllerId(server1.PrivateId)}, expectedJobs: []*Job{job1, job2}, }, { - opts: []Option{WithServerId(server2.PrivateId)}, + opts: []Option{WithControllerId(server2.PrivateId)}, expectedJobs: []*Job{job3}, }, { - opts: []Option{WithServerId(server3.PrivateId)}, + opts: []Option{WithControllerId(server3.PrivateId)}, expectedJobs: []*Job{}, }, }, @@ -1041,17 +1041,17 @@ func TestRepository_InterruptServerRuns(t *testing.T) { name: "multiple-servers-distributed-runs-interrupt-all", runs: []args{ { - serverId: server1.PrivateId, + ControllerId: server1.PrivateId, opts: []Option{WithRunJobsLimit(2)}, expectedJobs: []*Job{job1, job2}, }, { - serverId: server2.PrivateId, + ControllerId: server2.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{job3}, }, { - serverId: server3.PrivateId, + ControllerId: server3.PrivateId, opts: []Option{WithRunJobsLimit(1)}, expectedJobs: []*Job{}, }, @@ -1070,7 +1070,7 @@ func TestRepository_InterruptServerRuns(t *testing.T) { require.NoError(err) for _, r := range tt.runs { - runs, err := repo.RunJobs(context.Background(), r.serverId, r.opts...) + runs, err := repo.RunJobs(context.Background(), r.ControllerId, r.opts...) require.NoError(err) assert.Len(runs, len(r.expectedJobs)) sort.Slice(runs, func(i, j int) bool { return runs[i].JobName < runs[j].JobName }) diff --git a/internal/scheduler/job/store/job.pb.go b/internal/scheduler/job/store/job.pb.go index 77c96a057..e04d18c12 100644 --- a/internal/scheduler/job/store/job.pb.go +++ b/internal/scheduler/job/store/job.pb.go @@ -116,9 +116,6 @@ type JobRun struct { // The job_name is the name of the job and must be set. // @inject_tag: `gorm:"not_null"` JobName string `protobuf:"bytes,3,opt,name=job_name,json=jobName,proto3" json:"job_name,omitempty" gorm:"not_null"` - // The server_id of the server running the job and must be set. - // @inject_tag: `gorm:"not_null"` - ServerId string `protobuf:"bytes,4,opt,name=server_id,json=serverId,proto3" json:"server_id,omitempty" gorm:"not_null"` // The create_time is set by the database. // @inject_tag: `gorm:"default:current_timestamp"` CreateTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` @@ -137,6 +134,9 @@ type JobRun struct { // status of the job run (running, completed, failed or interrupted). // @inject_tag: `gorm:"not_null"` Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty" gorm:"not_null"` + // The controller_id of the controller running the job and must be set. + // @inject_tag: `gorm:"not_null"` + ControllerId string `protobuf:"bytes,11,opt,name=controller_id,json=controllerId,proto3" json:"controller_id,omitempty" gorm:"not_null"` } func (x *JobRun) Reset() { @@ -192,13 +192,6 @@ func (x *JobRun) GetJobName() string { return "" } -func (x *JobRun) GetServerId() string { - if x != nil { - return x.ServerId - } - return "" -} - func (x *JobRun) GetCreateTime() *timestamp.Timestamp { if x != nil { return x.CreateTime @@ -241,6 +234,13 @@ func (x *JobRun) GetStatus() string { return "" } +func (x *JobRun) GetControllerId() string { + if x != nil { + return x.ControllerId + } + return "" +} + var File_controller_storage_job_store_v1_job_proto protoreflect.FileDescriptor var file_controller_storage_job_store_v1_job_proto_rawDesc = []byte{ @@ -263,40 +263,41 @@ var file_controller_storage_job_store_v1_job_proto_rawDesc = []byte{ 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x64, 0x52, - 0x75, 0x6e, 0x22, 0xc6, 0x03, 0x0a, 0x06, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, + 0x75, 0x6e, 0x22, 0xd9, 0x03, 0x0a, 0x06, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x6a, 0x6f, 0x62, 0x5f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6a, 0x6f, 0x62, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, - 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, - 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x42, 0x5a, 0x40, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x2f, - 0x6a, 0x6f, 0x62, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, + 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x49, 0x64, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x42, 0x42, + 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x72, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/scheduler/job/testing.go b/internal/scheduler/job/testing.go index 8a2fa6852..9d00670e9 100644 --- a/internal/scheduler/job/testing.go +++ b/internal/scheduler/job/testing.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" @@ -33,7 +35,7 @@ func testJob(t testing.TB, conn *db.DB, name, description string, wrapper wrappi func testRun(conn *db.DB, pluginId, name, cId string) (*Run, error) { query := ` insert into job_run ( - job_plugin_id, job_name, server_id + job_plugin_id, job_name, controller_id ) values (?,?,?) on conflict (job_plugin_id, job_name) where status = 'running' @@ -63,7 +65,7 @@ func testRun(conn *db.DB, pluginId, name, cId string) (*Run, error) { func testRunWithUpdateTime(conn *db.DB, pluginId, name, cId string, updateTime time.Time) (*Run, error) { query := ` insert into job_run ( - job_plugin_id, job_name, server_id, update_time + job_plugin_id, job_name, controller_id, update_time ) values (?,?,?,?) returning *; @@ -88,7 +90,7 @@ func testRunWithUpdateTime(conn *db.DB, pluginId, name, cId string, updateTime t return run, nil } -func testController(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper) *servers.Server { +func testController(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) *store.Controller { t.Helper() rw := db.New(conn) kms := kms.TestKms(t, conn, wrapper) @@ -97,13 +99,11 @@ func testController(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper) *server id, err := uuid.GenerateUUID() require.NoError(t, err) - controller := &servers.Server{ - PrivateId: "test-job-server-" + id, - Type: servers.ServerTypeController.String(), - Description: "Test Job Controller", - Address: "127.0.0.1", + controller := &store.Controller{ + PrivateId: "test-job-server-" + id, + Address: "127.0.0.1", } - _, _, err = serversRepo.UpsertServer(context.Background(), controller) + _, err = serversRepo.UpsertController(context.Background(), controller) require.NoError(t, err) return controller } diff --git a/internal/scheduler/job/testing_test.go b/internal/scheduler/job/testing_test.go index cd3e6ddd4..4abd3f74a 100644 --- a/internal/scheduler/job/testing_test.go +++ b/internal/scheduler/job/testing_test.go @@ -68,7 +68,7 @@ func Test_TestRun(t *testing.T) { var got Run err = rw.LookupWhere(context.Background(), &got, "private_id = ?", []interface{}{run.PrivateId}) require.NoError(err) - assert.Equal(server.PrivateId, got.ServerId) + assert.Equal(server.PrivateId, got.ControllerId) assert.Equal(job.Name, got.JobName) assert.Equal(job.PluginId, got.JobPluginId) assert.NotEmpty(got.CreateTime) diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 626b5d00c..c7d5167af 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -154,7 +154,7 @@ func (s *Scheduler) Start(ctx context.Context, wg *sync.WaitGroup) error { } // Interrupt all runs previously allocated to this server - _, err = repo.InterruptRuns(ctx, 0, job.WithServerId(s.serverId)) + _, err = repo.InterruptRuns(ctx, 0, job.WithControllerId(s.serverId)) if err != nil { return errors.Wrap(ctx, err, op) } diff --git a/internal/scheduler/testing.go b/internal/scheduler/testing.go index e11672b0b..93c6a4752 100644 --- a/internal/scheduler/testing.go +++ b/internal/scheduler/testing.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" @@ -32,13 +34,11 @@ func TestScheduler(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, opt ...O id, err := uuid.GenerateUUID() require.NoError(t, err) - controller := &servers.Server{ - PrivateId: "test-job-server-" + id, - Type: servers.ServerTypeController.String(), - Description: "Test Job Controller", - Address: "127.0.0.1", + controller := &store.Controller{ + PrivateId: "test-job-server-" + id, + Address: "127.0.0.1", } - _, _, err = serversRepo.UpsertServer(context.Background(), controller) + _, err = serversRepo.UpsertController(context.Background(), controller) require.NoError(t, err) jobRepoFn := func() (*job.Repository, error) { diff --git a/internal/servers/gorm.go b/internal/servers/gorm.go deleted file mode 100644 index 4ec277ef7..000000000 --- a/internal/servers/gorm.go +++ /dev/null @@ -1,5 +0,0 @@ -package servers - -func (s *Server) TableName() string { - return "server" -} diff --git a/internal/servers/job/jobs.go b/internal/servers/job/jobs.go new file mode 100644 index 000000000..3c900a865 --- /dev/null +++ b/internal/servers/job/jobs.go @@ -0,0 +1,50 @@ +package servers + +import ( + "context" + "reflect" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" +) + +// RegisterJobs registers the rotate roots job with the provided scheduler. +func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, r db.Reader, w db.Writer, kms *kms.Kms) error { + const op = "servers.(Jobs).RegisterJobs" + + if isNil(scheduler) { + return errors.New(ctx, errors.InvalidParameter, op, "missing scheduler") + } + if isNil(r) { + return errors.New(ctx, errors.InvalidParameter, op, "missing reader") + } + if isNil(w) { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + if kms == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms") + } + + rotateRootsJob, err := newRotateRootsJob(ctx, r, w, kms) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if err = scheduler.RegisterJob(ctx, rotateRootsJob); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} diff --git a/internal/servers/job/rotate_roots_job.go b/internal/servers/job/rotate_roots_job.go new file mode 100644 index 000000000..ccdc83046 --- /dev/null +++ b/internal/servers/job/rotate_roots_job.go @@ -0,0 +1,82 @@ +package servers + +import ( + "context" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/hashicorp/boundary/internal/servers" +) + +const rotateFrequency = time.Hour + +// rotateRootsJob defines a periodic job that initiates root certificate rotation +// It runs every hour; the root rotation function in the library is designed to not +// do anything if it's not time to rotate (roots are within their valid ranges) +type rotateRootsJob struct { + workerAuthRepo *servers.WorkerAuthRepositoryStorage + + totalRotates int +} + +// newRotateRootsJob instantiates the rotate roots job. +func newRotateRootsJob(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms) (*rotateRootsJob, error) { + const op = "servers.newRotateRootsJob" + switch { + case isNil(r): + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader") + case isNil(w): + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer") + case kms == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms") + } + + workerAuthRepo, err := servers.NewRepositoryStorage(ctx, r, w, kms) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return &rotateRootsJob{ + workerAuthRepo: workerAuthRepo, + totalRotates: 0, + }, nil +} + +// Name returns a short, unique name for the job. +func (r *rotateRootsJob) Name() string { return "rotate_roots" } + +// Description returns the description for the job. +func (r *rotateRootsJob) Description() string { + return "Rotate root certificates" +} + +// NextRunIn returns the next run time after a job is completed. +// This is represented by RotateFrequency +func (r *rotateRootsJob) NextRunIn(_ context.Context) (time.Duration, error) { + return rotateFrequency, nil +} + +// Status returns the status of the running job. +func (r *rotateRootsJob) Status() scheduler.JobStatus { + return scheduler.JobStatus{ + Completed: r.totalRotates, + Total: r.totalRotates, + } +} + +// Run executes the job by calling the rotateRoots domain function +func (r *rotateRootsJob) Run(ctx context.Context) error { + const op = "servers.(rotateRootsJob).Run" + + err := servers.RotateRoots(ctx, r.workerAuthRepo) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + r.totalRotates += 1 + + return nil +} diff --git a/internal/servers/job/rotate_roots_job_test.go b/internal/servers/job/rotate_roots_job_test.go new file mode 100644 index 000000000..bf32879c7 --- /dev/null +++ b/internal/servers/job/rotate_roots_job_test.go @@ -0,0 +1,221 @@ +package servers + +import ( + "context" + "crypto/rand" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/nodeenrollment/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRotateRootsJob(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + kmsCache := kms.TestKms(t, conn, wrapper) + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + + workerAuthRepo, err := servers.NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + type args struct { + w db.Writer + r db.Reader + kms *kms.Kms + } + tests := []struct { + name string + args args + options []servers.Option + wantLimit int + wantErr bool + wantErrCode errors.Code + }{ + { + name: "nil writer", + args: args{ + r: rw, + kms: kmsCache, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil reader", + args: args{ + w: rw, + kms: kmsCache, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil kms", + args: args{ + w: rw, + r: rw, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + args: args{ + w: rw, + r: rw, + kms: kmsCache, + }, + wantLimit: db.DefaultLimit, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newRotateRootsJob(ctx, tt.args.r, tt.args.w, tt.args.kms) + if tt.wantErr { + require.Error(err) + assert.Nil(got) + assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "Unexpected error %s", err) + return + } + require.NoError(err) + require.NotNil(got) + require.Equal(0, got.totalRotates) + rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 0) + assert.Equal("rotate_roots", got.Name()) + assert.Equal("Rotate root certificates", got.Description()) + nextRun, err := got.NextRunIn(ctx) + require.NoError(err) + assert.Equal(time.Hour, nextRun) + + // Run job and ensure rotation was performed + err = got.Run(ctx) + require.NoError(err) + require.Equal(1, got.totalRotates) + rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 2) + }) + } +} + +func TestRotateRootsJobFailure(t *testing.T) { + require := require.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + + got, err := newRotateRootsJob(ctx, &db.Db{}, &db.Db{}, kmsCache) + require.NoError(err) + + err = got.Run(ctx) + require.Error(err) +} + +func TestRegisterRotateRootsJob(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + kmsCache := kms.TestKms(t, conn, wrapper) + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + + sched := scheduler.TestScheduler(t, conn, wrapper) + + type args struct { + s *scheduler.Scheduler + w db.Writer + r db.Reader + kms *kms.Kms + } + tests := []struct { + name string + args args + options []servers.Option + wantLimit int + wantErr bool + wantErrCode errors.Code + }{ + { + name: "nil scheduler", + args: args{ + w: rw, + r: rw, + kms: kmsCache, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil writer", + args: args{ + s: sched, + r: rw, + kms: kmsCache, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil reader", + args: args{ + s: sched, + w: rw, + kms: kmsCache, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil kms", + args: args{ + s: sched, + w: rw, + r: rw, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + args: args{ + s: sched, + w: rw, + r: rw, + kms: kmsCache, + }, + wantLimit: db.DefaultLimit, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := RegisterJobs(ctx, tt.args.s, tt.args.r, tt.args.w, tt.args.kms) + if tt.wantErr { + require.Error(err) + assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "Unexpected error %s", err) + return + } + require.NoError(err) + }) + } +} diff --git a/internal/servers/options.go b/internal/servers/options.go index f82f5a52d..b2a883624 100644 --- a/internal/servers/options.go +++ b/internal/servers/options.go @@ -1,6 +1,11 @@ package servers -import "time" +import ( + "context" + "time" + + "github.com/hashicorp/nodeenrollment/types" +) // getOpts - iterate the inbound Options and return a struct func getOpts(opt ...Option) options { @@ -16,15 +21,56 @@ type Option func(*options) // options = how options are represented type options struct { - withLimit int - withLiveness time.Duration - withUpdateTags bool + withName string + withPublicId string + withDescription string + withAddress string + withLimit int + withLiveness time.Duration + withUpdateTags bool + withWorkerTags []*Tag + withWorkerKeyIdentifier string + withWorkerKeys WorkerKeys + withControllerEncryptionPrivateKey []byte + withKeyId string + withNonce []byte + withNewIdFunc func(context.Context) (string, error) + withFetchNodeCredentialsRequest *types.FetchNodeCredentialsRequest + withTestPkiWorkerAuthorized bool + withTestPkiWorkerKeyId *string } func getDefaultOptions() options { return options{ - withLimit: 0, - withLiveness: 0, + withNewIdFunc: newWorkerId, + } +} + +// WithDescription provides an optional description. +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// WithName provides an optional name. +func WithName(name string) Option { + return func(o *options) { + o.withName = name + } +} + +// WithPublicId provides an optional public Id used for skipping one db call. +func WithPublicId(id string) Option { + return func(o *options) { + o.withPublicId = id + } +} + +// WithAddress provides an optional address. +func WithAddress(address string) Option { + return func(o *options) { + o.withAddress = address } } @@ -54,3 +100,66 @@ func WithUpdateTags(updateTags bool) Option { o.withUpdateTags = updateTags } } + +// WithWorkerTags provides worker tags. +func WithWorkerTags(tags ...*Tag) Option { + return func(o *options) { + o.withWorkerTags = tags + } +} + +func WithWorkerKeyIdentifier(workerKeyIdentifier string) Option { + return func(o *options) { + o.withWorkerKeyIdentifier = workerKeyIdentifier + } +} + +func WithWorkerKeys(workerKeys WorkerKeys) Option { + return func(o *options) { + o.withWorkerKeys = workerKeys + } +} + +func WithControllerEncryptionPrivateKey(controllerKey []byte) Option { + return func(o *options) { + o.withControllerEncryptionPrivateKey = controllerKey + } +} + +func WithKeyId(keyId string) Option { + return func(o *options) { + o.withKeyId = keyId + } +} + +func WithNonce(nonce []byte) Option { + return func(o *options) { + o.withNonce = nonce + } +} + +// WithNewIdFunc allows an optional factory function for new worker IDs to be +// specified (this option is likely only useful for tests). +func WithNewIdFunc(fn func(context.Context) (string, error)) Option { + return func(o *options) { + o.withNewIdFunc = fn + } +} + +// WithFetchNodeCredentialsRequest allows an optional +// FetchNodeCredentialsRequest to be specified. +func WithFetchNodeCredentialsRequest(req *types.FetchNodeCredentialsRequest) Option { + return func(o *options) { + o.withFetchNodeCredentialsRequest = req + } +} + +// WithTestPkiWorkerAuthorizedKeyId should only be used in tests. +// It specifies that the test worker should be authorized when returned and +// assigns the key id for that worker to the string pointer in this option. +func WithTestPkiWorkerAuthorizedKeyId(id *string) Option { + return func(o *options) { + o.withTestPkiWorkerAuthorized = true + o.withTestPkiWorkerKeyId = id + } +} diff --git a/internal/servers/options_test.go b/internal/servers/options_test.go index 6ceedfc52..d5839c21d 100644 --- a/internal/servers/options_test.go +++ b/internal/servers/options_test.go @@ -1,6 +1,9 @@ package servers import ( + "context" + "reflect" + "runtime" "testing" "time" @@ -10,18 +13,149 @@ import ( // Test_GetOpts provides unit tests for GetOpts and all the options func Test_GetOpts(t *testing.T) { t.Parallel() - opts := getOpts() - assert.Equal(t, options{}, opts) + t.Run("WithName", func(t *testing.T) { + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithPublicId", func(t *testing.T) { + opts := getOpts(WithPublicId("test")) + testOpts := getDefaultOptions() + testOpts.withPublicId = "test" + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithLimit", func(t *testing.T) { + opts := getOpts(WithLimit(5)) + testOpts := getDefaultOptions() + testOpts.withLimit = 5 + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithAddress", func(t *testing.T) { + opts := getOpts(WithAddress("test")) + testOpts := getDefaultOptions() + testOpts.withAddress = "test" + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithLiveness", func(t *testing.T) { + opts := getOpts(WithLiveness(time.Hour)) + testOpts := getDefaultOptions() + testOpts.withLiveness = time.Hour + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithUpdateTags", func(t *testing.T) { + opts := getOpts(WithUpdateTags(true)) + testOpts := getDefaultOptions() + testOpts.withUpdateTags = true + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithWorkerTags", func(t *testing.T) { + tags := []*Tag{ + {Key: "key1", Value: "val1"}, + {Key: "key2", Value: "val2"}, + } + opts := getOpts(WithWorkerTags(tags...)) + testOpts := getDefaultOptions() + testOpts.withWorkerTags = tags + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(t, opts, testOpts) + }) + t.Run("WithWorkerKeys", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() - opts = getOpts( - WithLimit(5), - WithLiveness(time.Hour), - WithUpdateTags(true), - ) - exp := options{ - withLimit: 5, - withLiveness: time.Hour, - withUpdateTags: true, - } - assert.Equal(t, exp, opts) + keys := WorkerKeys{ + workerEncryptionPubKey: populateBytes(20), + workerSigningPubKey: populateBytes(20), + } + + opts := getOpts(WithWorkerKeys(keys)) + testOpts = getDefaultOptions() + testOpts.withWorkerKeys = keys + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(opts, testOpts) + }) + t.Run("WithControllerEncryptionPrivateKey", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + + key := populateBytes(20) + + opts := getOpts(WithControllerEncryptionPrivateKey(key)) + testOpts = getDefaultOptions() + testOpts.withControllerEncryptionPrivateKey = key + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(opts, testOpts) + }) + t.Run("WithKeyId", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + + opts := getOpts(WithKeyId("hi i'm another key id")) + testOpts = getDefaultOptions() + testOpts.withKeyId = "hi i'm another key id" + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(opts, testOpts) + }) + t.Run("WithNonce", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + + nonce := populateBytes(20) + + opts := getOpts(WithNonce(nonce)) + testOpts = getDefaultOptions() + testOpts.withNonce = nonce + opts.withNewIdFunc = nil + testOpts.withNewIdFunc = nil + assert.Equal(opts, testOpts) + }) + t.Run("WithNewIdFunc", func(t *testing.T) { + assert := assert.New(t) + testFn := func(context.Context) (string, error) { return "", nil } + opts := getOpts(WithNewIdFunc(testFn)) + testOpts := getDefaultOptions() + testOpts.withNewIdFunc = testFn + assert.Equal( + runtime.FuncForPC(reflect.ValueOf(opts.withNewIdFunc).Pointer()).Name(), + runtime.FuncForPC(reflect.ValueOf(testOpts.withNewIdFunc).Pointer()).Name(), + ) + }) + t.Run("WithTestPkiWorkerAuthorizedKeyId", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + + var keyId string + opts := getOpts(WithTestPkiWorkerAuthorizedKeyId(&keyId)) + testOpts = getDefaultOptions() + testOpts.withTestPkiWorkerAuthorized = true + testOpts.withTestPkiWorkerKeyId = &keyId + testOpts.withNewIdFunc = nil + opts.withNewIdFunc = nil + assert.Equal(opts, testOpts) + }) } diff --git a/internal/servers/public_ids.go b/internal/servers/public_ids.go new file mode 100644 index 000000000..0658c36f2 --- /dev/null +++ b/internal/servers/public_ids.go @@ -0,0 +1,33 @@ +package servers + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +// PublicId prefixes for the resources in the servers package. +const ( + WorkerPrefix = "w" +) + +func newWorkerId(ctx context.Context) (string, error) { + id, err := db.NewPublicId(WorkerPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, "servers.newWorkerId") + } + return id, nil +} + +// newWorkerIdFromScopeAndName generates a predictable public id based on the +// scope and the worker name. This should only be used on kms workers at +// upsert time. +func newWorkerIdFromScopeAndName(ctx context.Context, scope, name string) (string, error) { + const op = "servers.newWorkerIdFromScopeAndName" + id, err := db.NewPublicId(WorkerPrefix, db.WithPrngValues([]string{scope, name})) + if err != nil { + return "", errors.Wrap(ctx, err, "servers.newWorkerId") + } + return id, nil +} diff --git a/internal/servers/query.go b/internal/servers/query.go index c0f865a17..d83f5428f 100644 --- a/internal/servers/query.go +++ b/internal/servers/query.go @@ -2,5 +2,25 @@ package servers const ( deleteWhereCreateTimeSql = `create_time < ?` - deleteTagsSql = `server_id = ?` + + deleteTagsByWorkerIdSql = ` + delete + from server_worker_tag + where + source = ? + and + worker_id = ?` + + deleteWorkerAuthQuery = ` + delete from worker_auth_authorized + where worker_key_identifier = @worker_key_identifier; + ` + deleteWorkerCertBundlesQuery = ` + delete from worker_auth_certificate_bundle + where worker_key_identifier = @worker_key_identifier; + ` + deleteRootCertificateQuery = ` + delete from worker_auth_ca_certificate + where state = @state; + ` ) diff --git a/internal/servers/repository.go b/internal/servers/repository.go index 4d71ae8b8..0fbcf292c 100644 --- a/internal/servers/repository.go +++ b/internal/servers/repository.go @@ -1,15 +1,12 @@ package servers import ( - "context" - "fmt" + "reflect" "time" - "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/types/resource" ) const ( @@ -24,27 +21,18 @@ const ( DefaultLiveness = 15 * time.Second ) -type ServerType string - -const ( - ServerTypeController ServerType = "controller" - ServerTypeWorker ServerType = "worker" -) - -func (s ServerType) String() string { - return string(s) -} - // Repository is the servers database repository type Repository struct { reader db.Reader writer db.Writer kms *kms.Kms + // defaultLimit provides a default for limiting the number of results returned from the repo + defaultLimit int } // NewRepository creates a new servers Repository. Supports the options: WithLimit // which sets a default limit on results returned by repo operations. -func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms) (*Repository, error) { +func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { const op = "servers.NewRepository" if r == nil { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "nil reader") @@ -55,217 +43,27 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms) (*Repository, error) if kms == nil { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "nil kms") } + + opts := getOpts(opt...) + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } return &Repository{ - reader: r, - writer: w, - kms: kms, + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, }, nil } -// ListServers is a passthrough to listServersWithReader that uses the repo's -// normal reader. -func (r *Repository) ListServers(ctx context.Context, serverType ServerType, opt ...Option) ([]*Server, error) { - return r.listServersWithReader(ctx, r.reader, serverType, opt...) -} - -// listServersWithReader will return a listing of resources and honor the -// WithLimit option or the repo defaultLimit. It accepts a reader, allowing it -// to be used within a transaction or without. -func (r *Repository) listServersWithReader(ctx context.Context, reader db.Reader, serverType ServerType, opt ...Option) ([]*Server, error) { - opts := getOpts(opt...) - liveness := opts.withLiveness - if liveness == 0 { - liveness = DefaultLiveness - } - - var where string - if liveness > 0 { - where = fmt.Sprintf("type = ? and update_time > now() - interval '%d seconds'", uint32(liveness.Seconds())) - } else { - where = "type = ?" - } - - var servers []*Server - if err := reader.SearchWhere( - ctx, - &servers, - where, - []interface{}{serverType}, - db.WithLimit(-1), - ); err != nil { - return nil, errors.Wrap(ctx, err, "servers.listServersWithReader") - } - - return servers, nil -} - -// ServerTag holds the information for the server_tag table for Gorm. -type ServerTag struct { - ServerId string - Key string - Value string -} - -// TableName overrides the table name used by ServerTag to `server_tag` -func (ServerTag) TableName() string { - return "server_tag" -} - -// ListTagsForServers pulls out tag tuples into ServerTag structs for the -// given server ID values. -func (r *Repository) ListTagsForServers(ctx context.Context, serverIds []string, opt ...Option) ([]*ServerTag, error) { - var serverTags []*ServerTag - if err := r.reader.SearchWhere( - ctx, - &serverTags, - "server_id in (?)", - []interface{}{serverIds}, - db.WithLimit(-1), - ); err != nil { - return nil, errors.Wrap(ctx, err, "servers.ListTagsForServers", errors.WithMsg(fmt.Sprintf("server IDs %v", serverIds))) - } - return serverTags, nil -} - -// UpsertServer adds or updates a server in the DB -func (r *Repository) UpsertServer(ctx context.Context, server *Server, opt ...Option) ([]*Server, int, error) { - const op = "servers.UpsertServer" - - if server == nil { - return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "server is nil") - } - - opts := getOpts(opt...) - - var rowsUpdated int64 - var controllers []*Server - _, err := r.writer.DoTx( - ctx, - db.StdRetryCnt, - db.ExpBackoff{}, - func(read db.Reader, w db.Writer) error { - var err error - onConflict := &db.OnConflict{ - Target: db.Constraint("server_pkey"), - Action: append(db.SetColumns([]string{"type", "description", "address"}), db.SetColumnValues(map[string]interface{}{"update_time": "now()"})...), - } - err = w.Create(ctx, server, db.WithOnConflict(onConflict), db.WithReturnRowsAffected(&rowsUpdated)) - if err != nil { - return errors.Wrap(ctx, err, op+":Upsert") - } - - // If it's a worker, fetch the current controllers to feed to them - if server.Type == resource.Worker.String() { - // Fetch current controllers to feed to the workers - controllers, err = r.listServersWithReader(ctx, read, ServerTypeController) - if err != nil { - return errors.Wrap(ctx, err, op+":ListServer") - } - } - - // If we've been told to update tags, we need to clean out old - // ones and add new ones. Within the current transaction, simply - // delete all tags for the given worker, then add the new ones - // we've been sent. - if opts.withUpdateTags { - _, err = w.Delete(ctx, &ServerTag{}, db.WithWhere(deleteTagsSql, server.PrivateId)) - if err != nil { - return errors.Wrap(ctx, err, op+":DeleteTags", errors.WithMsg(server.PrivateId)) - } - - // If tags were cleared out entirely, then we'll have nothing - // to do here, e.g., it will result in deletion of all tags. - // Otherwise, go through and stage each tuple for insertion - // below. - if len(server.Tags) > 0 { - tags := make([]interface{}, 0, len(server.Tags)) - for k, v := range server.Tags { - if v == nil { - return errors.New(ctx, errors.InvalidParameter, op+":RangeTags", fmt.Sprintf("found nil tag value for worker %s and key %s", server.PrivateId, k)) - } - for _, val := range v.Values { - tags = append(tags, ServerTag{ - ServerId: server.PrivateId, - Key: k, - Value: val, - }) - } - } - if err = w.CreateItems(ctx, tags); err != nil { - return errors.Wrap(ctx, err, op+":CreateTags", errors.WithMsg(server.PrivateId)) - } - } - } - - return nil - }, - ) - if err != nil { - return nil, db.NoRowsAffected, err - } - - return controllers, int(rowsUpdated), nil -} - -type Nonce struct { - Nonce string - Purpose string -} - -// TableName returns the table name. -func (n *Nonce) TableName() string { - return "nonce" -} - -const ( - NoncePurposeRecovery = "recovery" - NoncePurposeWorkerAuth = "worker-auth" -) - -// AddNonce adds a nonce -func (r *Repository) AddNonce(ctx context.Context, nonce, purpose string, opt ...Option) error { - const op = "servers.AddNonce" - if nonce == "" { - return errors.New(ctx, errors.InvalidParameter, op, "empty nonce") - } - switch purpose { - case NoncePurposeRecovery, NoncePurposeWorkerAuth: - case "": - return errors.New(ctx, errors.InvalidParameter, op, "empty nonce purpose") - default: - return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown nonce purpose %q", purpose)) - } - if err := r.writer.Create(ctx, &Nonce{ - Nonce: nonce, - Purpose: purpose, - }); err != nil { - return errors.Wrap(ctx, err, op+":Insertion") - } - return nil -} - -// CleanupNonces removes nonces that no longer need to be stored -func (r *Repository) CleanupNonces(ctx context.Context, opt ...Option) (int, error) { - // Use the largest validity period out of the various nonces we're looking at - maxDuration := globals.RecoveryTokenValidityPeriod - if globals.WorkerAuthNonceValidityPeriod > maxDuration { - maxDuration = globals.WorkerAuthNonceValidityPeriod - } - // If something was inserted before 3x the actual validity period, clean it out - endTime := time.Now().Add(-3 * maxDuration) - - rows, err := r.writer.Delete(ctx, &Nonce{}, db.WithWhere(deleteWhereCreateTimeSql, endTime)) - if err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, "servers.CleanupNonces") - } - return rows, nil -} - -// ListNonces lists nonces. Used only for tests at the moment. -func (r *Repository) ListNonces(ctx context.Context, purpose string, opt ...Option) ([]*Nonce, error) { - var nonces []*Nonce - if err := r.reader.SearchWhere(ctx, &nonces, "purpose = ?", []interface{}{purpose}, db.WithLimit(-1)); err != nil { - return nil, errors.Wrap(ctx, err, "servers.ListNonces") - } - return nonces, nil +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false } diff --git a/internal/servers/repository_controller.go b/internal/servers/repository_controller.go new file mode 100644 index 000000000..931071e0e --- /dev/null +++ b/internal/servers/repository_controller.go @@ -0,0 +1,76 @@ +package servers + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/servers/store" +) + +func (r *Repository) ListControllers(ctx context.Context, opt ...Option) ([]*store.Controller, error) { + return r.listControllersWithReader(ctx, r.reader, opt...) +} + +// listControllersWithReader will return a listing of resources and honor the +// WithLimit option or the repo defaultLimit. It accepts a reader, allowing it +// to be used within a transaction or without. +func (r *Repository) listControllersWithReader(ctx context.Context, reader db.Reader, opt ...Option) ([]*store.Controller, error) { + opts := getOpts(opt...) + liveness := opts.withLiveness + if liveness == 0 { + liveness = DefaultLiveness + } + + var where string + if liveness > 0 { + where = fmt.Sprintf("update_time > now() - interval '%d seconds'", uint32(liveness.Seconds())) + } + + var controllers []*store.Controller + if err := reader.SearchWhere( + ctx, + &controllers, + where, + []interface{}{}, + db.WithLimit(-1), + ); err != nil { + return nil, errors.Wrap(ctx, err, "workers.listControllersWithReader") + } + + return controllers, nil +} + +func (r *Repository) UpsertController(ctx context.Context, controller *store.Controller) (int, error) { + const op = "servers.UpsertController" + + if controller == nil { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "controller is nil") + } + + var rowsUpdated int64 + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + var err error + onConflict := &db.OnConflict{ + Target: db.Columns{"private_id"}, + Action: append(db.SetColumns([]string{"description", "address"}), db.SetColumnValues(map[string]interface{}{"update_time": "now()"})...), + } + err = w.Create(ctx, controller, db.WithOnConflict(onConflict), db.WithReturnRowsAffected(&rowsUpdated)) + if err != nil { + return errors.Wrap(ctx, err, op+":Upsert") + } + + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, err + } + + return int(rowsUpdated), nil +} diff --git a/internal/servers/repository_nonce.go b/internal/servers/repository_nonce.go new file mode 100644 index 000000000..b144d78ef --- /dev/null +++ b/internal/servers/repository_nonce.go @@ -0,0 +1,74 @@ +package servers + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +type Nonce struct { + Nonce string + Purpose string +} + +// TableName returns the table name. +func (n *Nonce) TableName() string { + return "nonce" +} + +const ( + NoncePurposeRecovery = "recovery" + NoncePurposeWorkerAuth = "worker-auth" +) + +// AddNonce adds a nonce +func (r *Repository) AddNonce(ctx context.Context, nonce, purpose string, opt ...Option) error { + const op = "servers.AddNonce" + if nonce == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty nonce") + } + switch purpose { + case NoncePurposeRecovery, NoncePurposeWorkerAuth: + case "": + return errors.New(ctx, errors.InvalidParameter, op, "empty nonce purpose") + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown nonce purpose %q", purpose)) + } + if err := r.writer.Create(ctx, &Nonce{ + Nonce: nonce, + Purpose: purpose, + }); err != nil { + return errors.Wrap(ctx, err, op+":Insertion") + } + return nil +} + +// CleanupNonces removes nonces that no longer need to be stored +func (r *Repository) CleanupNonces(ctx context.Context, opt ...Option) (int, error) { + // Use the largest validity period out of the various nonces we're looking at + maxDuration := globals.RecoveryTokenValidityPeriod + if globals.WorkerAuthNonceValidityPeriod > maxDuration { + maxDuration = globals.WorkerAuthNonceValidityPeriod + } + // If something was inserted before 3x the actual validity period, clean it out + endTime := time.Now().Add(-3 * maxDuration) + + rows, err := r.writer.Delete(ctx, &Nonce{}, db.WithWhere(deleteWhereCreateTimeSql, endTime)) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, "servers.CleanupNonces") + } + return rows, nil +} + +// ListNonces lists nonces. Used only for tests at the moment. +func (r *Repository) ListNonces(ctx context.Context, purpose string, opt ...Option) ([]*Nonce, error) { + var nonces []*Nonce + if err := r.reader.SearchWhere(ctx, &nonces, "purpose = ?", []interface{}{purpose}, db.WithLimit(-1)); err != nil { + return nil, errors.Wrap(ctx, err, "servers.ListNonces") + } + return nonces, nil +} diff --git a/internal/servers/repository_nonce_test.go b/internal/servers/repository_nonce_test.go new file mode 100644 index 000000000..18422c6a1 --- /dev/null +++ b/internal/servers/repository_nonce_test.go @@ -0,0 +1,87 @@ +package servers_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/api/recovery" + "github.com/hashicorp/boundary/api/roles" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/daemon/controller" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecoveryNonces(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // Set these low so that we can not have the test run forever + globals.RecoveryTokenValidityPeriod = 10 * time.Second + globals.WorkerAuthNonceValidityPeriod = 10 * time.Second + controller.NonceCleanupInterval = 20 * time.Second + + wrapper := db.TestWrapper(t) + tc := controller.NewTestController(t, &controller.TestControllerOpts{ + RecoveryKms: wrapper, + }) + defer tc.Shutdown() + + externalWrappers := tc.Kms().GetExternalWrappers(context.Background()) + externalRecovery := externalWrappers.Recovery() + require.Equal(externalRecovery, wrapper) + + client := tc.Client() + repo := tc.ServersRepo() + + // First, validate that we can't use the same token twice. Get two tokens, + // try to use the first twice (should succeed first time, fail second), then + // ensure second token works + token1, err := recovery.GenerateRecoveryToken(tc.Context(), wrapper) + require.NoError(err) + token2, err := recovery.GenerateRecoveryToken(tc.Context(), wrapper) + require.NoError(err) + + // Token 1, try 1 + client.SetToken(token1) + roleClient := roles.NewClient(client) + _, err = roleClient.Create(tc.Context(), scope.Global.String()) + require.NoError(err) + nonces, err := repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) + require.NoError(err) + assert.Len(nonces, 1) + + // Token 1, try 2 + _, err = roleClient.Create(tc.Context(), scope.Global.String()) + require.Error(err) + nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) + require.NoError(err) + assert.Len(nonces, 1) + + // Token 2 + roleClient.ApiClient().SetToken(token2) + _, err = roleClient.Create(tc.Context(), scope.Global.String()) + require.NoError(err) + nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) + require.NoError(err) + assert.Len(nonces, 2) + + // Make sure they get cleaned up + time.Sleep(2 * controller.NonceCleanupInterval) + nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) + require.NoError(err) + assert.Len(nonces, 0) + + // And finally, make sure they still can't be used + for _, token := range []string{token1, token2} { + roleClient.ApiClient().SetToken(token) + _, err = roleClient.Create(tc.Context(), scope.Global.String()) + require.Error(err) + nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) + require.NoError(err) + assert.Len(nonces, 0) + } +} diff --git a/internal/servers/repository_test.go b/internal/servers/repository_test.go deleted file mode 100644 index e348f484b..000000000 --- a/internal/servers/repository_test.go +++ /dev/null @@ -1,267 +0,0 @@ -package servers_test - -import ( - "context" - "testing" - "time" - - "github.com/hashicorp/boundary/api/recovery" - "github.com/hashicorp/boundary/api/roles" - "github.com/hashicorp/boundary/globals" - "github.com/hashicorp/boundary/internal/daemon/controller" - "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/servers" - "github.com/hashicorp/boundary/internal/types/scope" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRecoveryNonces(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - // Set these low so that we can not have the test run forever - globals.RecoveryTokenValidityPeriod = 10 * time.Second - globals.WorkerAuthNonceValidityPeriod = 10 * time.Second - controller.NonceCleanupInterval = 20 * time.Second - - wrapper := db.TestWrapper(t) - tc := controller.NewTestController(t, &controller.TestControllerOpts{ - RecoveryKms: wrapper, - }) - defer tc.Shutdown() - - externalWrappers := tc.Kms().GetExternalWrappers(context.Background()) - externalRecovery := externalWrappers.Recovery() - require.Equal(externalRecovery, wrapper) - - client := tc.Client() - repo := tc.ServersRepo() - - // First, validate that we can't use the same token twice. Get two tokens, - // try to use the first twice (should succeed first time, fail second), then - // ensure second token works - token1, err := recovery.GenerateRecoveryToken(tc.Context(), wrapper) - require.NoError(err) - token2, err := recovery.GenerateRecoveryToken(tc.Context(), wrapper) - require.NoError(err) - - // Token 1, try 1 - client.SetToken(token1) - roleClient := roles.NewClient(client) - _, err = roleClient.Create(tc.Context(), scope.Global.String()) - require.NoError(err) - nonces, err := repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) - require.NoError(err) - assert.Len(nonces, 1) - - // Token 1, try 2 - _, err = roleClient.Create(tc.Context(), scope.Global.String()) - require.Error(err) - nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) - require.NoError(err) - assert.Len(nonces, 1) - - // Token 2 - roleClient.ApiClient().SetToken(token2) - _, err = roleClient.Create(tc.Context(), scope.Global.String()) - require.NoError(err) - nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) - require.NoError(err) - assert.Len(nonces, 2) - - // Make sure they get cleaned up - time.Sleep(2 * controller.NonceCleanupInterval) - nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) - require.NoError(err) - assert.Len(nonces, 0) - - // And finally, make sure they still can't be used - for _, token := range []string{token1, token2} { - roleClient.ApiClient().SetToken(token) - _, err = roleClient.Create(tc.Context(), scope.Global.String()) - require.Error(err) - nonces, err = repo.ListNonces(tc.Context(), servers.NoncePurposeRecovery) - require.NoError(err) - assert.Len(nonces, 0) - } -} - -func TestTagUpdatingListing(t *testing.T) { - require := require.New(t) - - wrapper := db.TestWrapper(t) - tc := controller.NewTestController(t, &controller.TestControllerOpts{ - RecoveryKms: wrapper, - }) - defer tc.Shutdown() - - repo := tc.ServersRepo() - srv := &servers.Server{ - PrivateId: "test1", - Type: "worker", - Address: "127.0.0.1", - Tags: map[string]*servers.TagValues{ - "tag1": { - Values: []string{"value1", "value2"}, - }, - }, - } - - _, _, err := repo.UpsertServer(tc.Context(), srv, servers.WithUpdateTags(true)) - require.NoError(err) - - srv = &servers.Server{ - PrivateId: "test2", - Type: "worker", - Address: "127.0.0.1", - Tags: map[string]*servers.TagValues{ - "tag2": { - Values: []string{"value1", "value2"}, - }, - }, - } - _, _, err = repo.UpsertServer(tc.Context(), srv, servers.WithUpdateTags(true)) - require.NoError(err) - - tags, err := repo.ListTagsForServers(tc.Context(), []string{"test1", "test2"}) - require.NoError(err) - - // Base case - exp := []*servers.ServerTag{ - { - ServerId: "test1", - Key: "tag1", - Value: "value1", - }, - { - ServerId: "test1", - Key: "tag1", - Value: "value2", - }, - { - ServerId: "test2", - Key: "tag2", - Value: "value1", - }, - { - ServerId: "test2", - Key: "tag2", - Value: "value2", - }, - } - require.Equal(exp, tags) - - // Update without saying to update tags - srv = &servers.Server{ - PrivateId: "test2", - Type: "worker", - Address: "192.168.1.1", - Tags: map[string]*servers.TagValues{ - "tag22": { - Values: []string{"value21", "value22"}, - }, - }, - } - _, _, err = repo.UpsertServer(tc.Context(), srv) - require.NoError(err) - tags, err = repo.ListTagsForServers(tc.Context(), []string{"test1", "test2"}) - require.NoError(err) - require.Equal(exp, tags) - - // Update tags and test again - _, _, err = repo.UpsertServer(tc.Context(), srv, servers.WithUpdateTags(true)) - require.NoError(err) - tags, err = repo.ListTagsForServers(tc.Context(), []string{"test1", "test2"}) - require.NoError(err) - require.NotEqual(exp, tags) - // Update and try again - exp[2] = &servers.ServerTag{ - ServerId: "test2", - Key: "tag22", - Value: "value21", - } - exp[3] = &servers.ServerTag{ - ServerId: "test2", - Key: "tag22", - Value: "value22", - } - require.Equal(exp, tags) -} - -func TestListServersWithLiveness(t *testing.T) { - t.Parallel() - require := require.New(t) - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - serversRepo, err := servers.NewRepository(rw, rw, kms) - require.NoError(err) - ctx := context.Background() - - newServer := func(privateId string) *servers.Server { - result := &servers.Server{ - PrivateId: privateId, - Type: "worker", - Address: "127.0.0.1", - } - _, rowsUpdated, err := serversRepo.UpsertServer(ctx, result) - require.NoError(err) - require.Equal(1, rowsUpdated) - - return result - } - - server1 := newServer("test1") - server2 := newServer("test2") - server3 := newServer("test3") - - // Sleep the default liveness time (15sec currently) +1s - time.Sleep(time.Second * 16) - - // Push an upsert to the first worker so that its status has been - // updated. - _, rowsUpdated, err := serversRepo.UpsertServer(ctx, server1) - require.NoError(err) - require.Equal(1, rowsUpdated) - - requireIds := func(expected []string, actual []*servers.Server) { - require.Len(expected, len(actual)) - want := make(map[string]struct{}) - for _, v := range expected { - want[v] = struct{}{} - } - - got := make(map[string]struct{}) - for _, v := range actual { - got[v.PrivateId] = struct{}{} - } - - require.Equal(want, got) - } - - // Default liveness, should only list 1 - result, err := serversRepo.ListServers(ctx, servers.ServerTypeWorker) - require.NoError(err) - require.Len(result, 1) - requireIds([]string{server1.PrivateId}, result) - - // Upsert second server. - _, rowsUpdated, err = serversRepo.UpsertServer(ctx, server2) - require.NoError(err) - require.Equal(1, rowsUpdated) - - // Static liveness. Should get two, so long as this did not take - // more than 5s to execute. - result, err = serversRepo.ListServers(ctx, servers.ServerTypeWorker, servers.WithLiveness(time.Second*5)) - require.NoError(err) - require.Len(result, 2) - requireIds([]string{server1.PrivateId, server2.PrivateId}, result) - - // Liveness disabled, should get all three servers. - result, err = serversRepo.ListServers(ctx, servers.ServerTypeWorker, servers.WithLiveness(-1)) - require.NoError(err) - require.Len(result, 3) - requireIds([]string{server1.PrivateId, server2.PrivateId, server3.PrivateId}, result) -} diff --git a/internal/servers/repository_worker.go b/internal/servers/repository_worker.go new file mode 100644 index 000000000..e652bc317 --- /dev/null +++ b/internal/servers/repository_worker.go @@ -0,0 +1,553 @@ +package servers + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/registration" + "google.golang.org/protobuf/types/known/structpb" +) + +// DeleteWorker will delete a worker from the repository. +func (r *Repository) DeleteWorker(ctx context.Context, publicId string, _ ...Option) (int, error) { + const op = "servers.(Repository).DeleteWorker" + if publicId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + worker := allocWorker() + worker.Worker.PublicId = publicId + + var rowsDeleted int + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + deleteWorker := worker.clone() + var err error + rowsDeleted, err = w.Delete( + ctx, + deleteWorker, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsDeleted > 1 { + // return err, which will result in a rollback of the delete + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId))) + } + if rowsDeleted == 0 { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for worker with workerId: %s", publicId))) + } + return rowsDeleted, nil +} + +// LookupWorkerByName returns the worker with the provided name. In the event +// that no worker is found that matches then nil, nil will be returned. +func (r *Repository) LookupWorkerByName(ctx context.Context, name string) (*Worker, error) { + const op = "servers.(Repository).LookupWorkerByName" + switch { + case name == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "name is empty") + } + w, err := lookupWorkerByName(ctx, r.reader, name) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return w, nil +} + +func lookupWorkerByName(ctx context.Context, reader db.Reader, name string) (*Worker, error) { + const op = "servers.lookupWorkerByName" + switch { + case isNil(reader): + return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil") + case name == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "name is empty") + } + + wAgg := &workerAggregate{} + err := reader.LookupWhere(ctx, &wAgg, "name = ?", []interface{}{name}) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + w, err := wAgg.toWorker(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return w, nil +} + +func (r *Repository) LookupWorkerIdByKeyId(ctx context.Context, keyId string) (string, error) { + const op = "servers.(Repository).LookupWorkerIdByKeyId" + switch { + case keyId == "": + return "", errors.New(ctx, errors.InvalidParameter, op, "keyId is empty") + } + + // We're searching for a workerAuth record based on worker id + worker := allocWorkerAuth() + worker.WorkerKeyIdentifier = keyId + + err := r.reader.LookupById(ctx, worker) + if err != nil { + if errors.IsNotFoundError(err) { + return "", nil + } + return "", errors.Wrap(ctx, err, op) + } + + return worker.WorkerId, nil +} + +// LookupWorker returns the worker for the provided publicId. This returns +// nil nil in the situation where no worker can be found with that public id. +func (r *Repository) LookupWorker(ctx context.Context, publicId string, _ ...Option) (*Worker, error) { + const op = "servers.(Repository).LookupWorker" + switch { + case publicId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "publicId is empty") + } + w, err := lookupWorker(ctx, r.reader, publicId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return w, nil +} + +// lookupWorker returns the worker for the provided id. This returns +// nil nil in the situation where no worker can be found with that public id. +func lookupWorker(ctx context.Context, reader db.Reader, id string) (*Worker, error) { + const op = "servers.lookupWorker" + switch { + case id == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "id is empty") + } + wAgg := &workerAggregate{} + wAgg.PublicId = id + err := reader.LookupById(ctx, wAgg) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + w, err := wAgg.toWorker(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return w, nil +} + +// ListWorkers will return a listing of Workers and honor the WithLimit option. +// If WithLiveness is zero the default liveness value is used, if it is negative +// then the last status update time is ignored. +// If WithLimit < 0, then unlimited results are returned. If WithLimit == 0, then +// default limits are used for results. +func (r *Repository) ListWorkers(ctx context.Context, scopeIds []string, opt ...Option) ([]*Worker, error) { + const op = "servers.(Repository).ListWorkers" + switch { + case len(scopeIds) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope ids set") + } + + opts := getOpts(opt...) + liveness := opts.withLiveness + if liveness == 0 { + liveness = DefaultLiveness + } + + var where []string + var whereArgs []interface{} + if liveness > 0 { + where = append(where, fmt.Sprintf("last_status_time > now() - interval '%d seconds'", uint32(liveness.Seconds()))) + } + if len(scopeIds) > 0 { + where = append(where, "scope_id in (?)") + whereArgs = append(whereArgs, scopeIds) + } + + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + + var wAggs []*workerAggregate + if err := r.reader.SearchWhere( + ctx, + &wAggs, + strings.Join(where, " and "), + whereArgs, + db.WithLimit(limit), + ); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error searching for workers")) + } + + workers := make([]*Worker, 0, len(wAggs)) + for _, a := range wAggs { + w, err := a.toWorker(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error converting workerAggregate to Worker")) + } + workers = append(workers, w) + } + return workers, nil +} + +// UpsertWorkerStatus will update the address and last status time for a worker. +// If the worker is a kms worker that hasn't been seen yet, it'll attempt to +// create a new one, but will return an error if another worker (kms or other) +// has the same name. This returns the Worker object with the changes applied. +// The WithPublicId, WithKeyId, and WithUpdateTags options are +// the only ones used. All others are ignored. +// Workers are intentionally not oplogged. +func (r *Repository) UpsertWorkerStatus(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) { + const op = "servers.UpsertWorkerStatus" + + opts := getOpts(opt...) + switch { + case worker == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "worker is nil") + case worker.GetAddress() == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "worker reported address is empty") + case worker.ScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "scope id is empty") + case worker.PublicId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "worker id is not empty") + case worker.GetName() == "" && opts.withKeyId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "worker keyId and reported name are both empty; one is required") + case worker.GetName() != "" && opts.withKeyId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "worker keyId and reported name are both set; no more than one is allowed") + } + + var workerId string + var err error + switch { + case opts.withPublicId != "": + workerId = opts.withPublicId + case opts.withKeyId != "": + workerId, err = r.LookupWorkerIdByKeyId(ctx, opts.withKeyId) + if err != nil || workerId == "" { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error finding worker by keyId")) + } + default: + // generating the worker id based off of the scope and name ensures + // that if 2 kms workers are making requests with the same name they + // are treated as the same worker. Allowing this only for kms workers + // also ensures that we maintain the unique name constraint between pki + // workers and kms workers. + workerId, err = newWorkerIdFromScopeAndName(ctx, worker.GetScopeId(), worker.GetName()) + if err != nil || workerId == "" { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating a worker id")) + } + } + + var ret *Worker + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + worker := worker.clone() + worker.PublicId = workerId + switch { + case opts.withKeyId != "": + // This case goes first in case we want to relax the restriction + // around both name and key ID being supplied to account for + // e.g. config processing bugs. In that case, a key ID being + // supplied should be a clear indicator that we are working with + // a PKI worker, and a lack of one a clear indication we are + // working with a KMS worker. + // + // Note: unlike in the below case, this purposefully leaves out + // "description" since we want description changes for PKI-based + // workers to come via API only. We can't really guard on this + // in the DB so we need to be sure to not include it here. + n, err := w.Update(ctx, worker, []string{"address"}, nil) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update status of pki worker")) + } + switch n { + case 0: + return errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("failed to find worker with key id %q", opts.withKeyId)) + case 1: + break + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("multiple records found when updating worker with id %q", worker.GetPublicId())) + } + + case worker.GetName() != "": + worker.Type = KmsWorkerType.String() + workerCreateConflict := &db.OnConflict{ + Target: db.Columns{"public_id"}, + Action: append(db.SetColumns([]string{"description", "address"}), + db.SetColumnValues(map[string]interface{}{"last_status_time": "now()"})...), + } + var withRowsAffected int64 + err := w.Create(ctx, worker, db.WithOnConflict(workerCreateConflict), db.WithReturnRowsAffected(&withRowsAffected), + // The intent of this WithWhere option is to operate with the OnConflict such that the action + // taken by the OnConflict only applies if the conflict is on a row that is returned by this where + // statement, otherwise it should error out. + db.WithWhere("server_worker.type = 'kms'")) + switch { + case err != nil: + return errors.Wrap(ctx, err, op, errors.WithMsg("error creating a worker")) + case withRowsAffected == 0: + return errors.New(ctx, errors.NotUnique, op, "error updating worker") + } + } + + // If we've been told to update tags, we need to clean out old + // ones and add new ones. Within the current transaction, simply + // delete all tags for the given worker, then add the new ones + // we've been sent. + if opts.withUpdateTags { + if err := setWorkerTags(ctx, w, worker.GetPublicId(), ConfigurationTagSource, worker.inputTags); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("error setting worker tags")) + } + } + + wAgg := &workerAggregate{PublicId: worker.GetPublicId()} + if err := reader.LookupById(ctx, wAgg); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("error looking up worker aggregate")) + } + ret, err = wAgg.toWorker(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("error converting worker aggregate to worker")) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return ret, nil +} + +// setWorkerTags removes all existing tags from the same source and worker id +// and creates new ones based on the ones provided. This function should be +// called from inside a db transaction. +// Workers/worker tags are intentionally not oplogged. +func setWorkerTags(ctx context.Context, w db.Writer, id string, ts TagSource, tags []*Tag) error { + const op = "servers.setWorkerTags" + switch { + case !ts.isValid(): + return errors.New(ctx, errors.InvalidParameter, op, "invalid tag source provided") + case id == "": + return errors.New(ctx, errors.InvalidParameter, op, "worker id is empty") + case isNil(w): + return errors.New(ctx, errors.InvalidParameter, op, "db.Writer is nil") + } + _, err := w.Exec(ctx, deleteTagsByWorkerIdSql, []interface{}{ts.String(), id}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("couldn't delete exist tags for worker %q", id))) + } + + // If tags were cleared out entirely, then we'll have nothing + // to do here, e.g., it will result in deletion of all tags. + // Otherwise, go through and stage each tuple for insertion + // below. + if len(tags) > 0 { + uTags := make([]interface{}, 0, len(tags)) + for _, v := range tags { + if v == nil { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("found nil tag value for worker %s", id)) + } + uTags = append(uTags, &store.WorkerTag{ + WorkerId: id, + Key: v.Key, + Value: v.Value, + Source: ts.String(), + }) + } + if err = w.CreateItems(ctx, uTags); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating tags for worker %q", id))) + } + } + + return nil +} + +// UpdateWorker will update a worker in the repository and return the resulting +// worker. fieldMaskPaths provides field_mask.proto paths for fields that should +// be updated. Fields will be set to NULL if the field is a zero value and +// included in fieldMask. Name, Description, and Address are the only updatable +// fields, if no updatable fields are included in the fieldMaskPaths, then an +// error is returned. If any paths besides those listed above are included in +// the path then an error is returned. +func (r *Repository) UpdateWorker(ctx context.Context, worker *Worker, version uint32, fieldMaskPaths []string, opt ...Option) (*Worker, int, error) { + const ( + nameField = "name" + descField = "description" + ) + const op = "servers.(Repository).UpdateWorker" + switch { + case worker == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "worker is nil") + case worker.PublicId == "": + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case version == 0: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "version is zero") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, fmt.Sprintf("invalid field mask: %s", f)) + } + } + + var dbMask, nullFields []string + dbMask, nullFields = dbcommon.BuildUpdatePaths( + map[string]interface{}{ + nameField: worker.Name, + descField: worker.Description, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "no fields to update") + } + + var rowsUpdated int + var ret *Worker + var err error + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + worker := worker.clone() + rowsUpdated, err = w.Update(ctx, worker, dbMask, nullFields, db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + // return err, which will result in a rollback of the update + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + + wAgg := &workerAggregate{PublicId: worker.GetPublicId()} + if err := reader.LookupById(ctx, wAgg); err != nil { + return errors.Wrap(ctx, err, op) + } + if ret, err = wAgg.toWorker(ctx); err != nil { + return err + } + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("worker with name %q already exists", worker.Name)) + } + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", worker.GetPublicId()))) + } + return ret, rowsUpdated, nil +} + +// CreateWorker will create a worker in the repository and return the written +// worker. Creating a worker is not intentionally oplogged. A worker's +// ReportedStatus and Tags are intentionally ignored when creating a worker (not +// included). Currently, a worker can only be created in the global scope +// +// Options supported: WithFetchNodeCredentialsRequest and WithNewIdFunc (this +// option is likely only useful for tests) +func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) { + const op = "servers.CreateWorker" + switch { + case worker == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing worker") + case worker.PublicId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id is not empty") + case worker.ScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case worker.ScopeId != scope.Global.String(): + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("scope id must be %q", scope.Global.String())) + case worker.Address != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "address is not empty") + case worker.LastStatusTime != nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "last status time is not nil") + } + + opts := getOpts(opt...) + var err error + if worker.PublicId, err = opts.withNewIdFunc(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate worker id")) + } + state, err := structpb.NewStruct(map[string]any{ + "worker_id": worker.PublicId, + }) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating state struct", err)) + } + + var databaseWrapper wrapping.Wrapper + var workerAuthRepo *WorkerAuthRepositoryStorage + if opts.withFetchNodeCredentialsRequest != nil { + // used to encrypt the privKey within the NodeInformation + databaseWrapper, err = r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + + var returnedWorker *Worker + if _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + returnedWorker = worker.clone() + returnedWorker.Type = PkiWorkerType.String() + if err := w.Create( + ctx, + returnedWorker, + ); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker")) + } + if opts.withFetchNodeCredentialsRequest != nil { + workerAuthRepo, err = NewRepositoryStorage(ctx, read, w, r.kms) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker auth repository")) + } + nodeInfo, err := registration.AuthorizeNode(ctx, workerAuthRepo, opts.withFetchNodeCredentialsRequest, nodeenrollment.WithSkipStorage(true)) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to authorize node")) + } + nodeInfo.State = state + if err := StoreNodeInformationTx(ctx, w, databaseWrapper, nodeInfo); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to store node information")) + } + } + return nil + }, + ); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return returnedWorker, nil +} diff --git a/internal/servers/repository_worker_test.go b/internal/servers/repository_worker_test.go new file mode 100644 index 000000000..217a255c8 --- /dev/null +++ b/internal/servers/repository_worker_test.go @@ -0,0 +1,1152 @@ +package servers_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-dbw" + "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestDeleteWorker(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + ctx := context.Background() + + type args struct { + worker *servers.Worker + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + args: args{ + worker: servers.TestKmsWorker(t, conn, wrapper), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-public-id", + args: args{ + worker: func() *servers.Worker { + w := servers.Worker{Worker: &store.Worker{}} + return &w + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "servers.(Repository).DeleteWorker: missing public id: parameter violation: error #100", + }, + { + name: "not-found", + args: args{ + worker: func() *servers.Worker { + w := servers.Worker{Worker: &store.Worker{}} + id, err := db.NewPublicId("w") + require.NoError(t, err) + w.PublicId = id + return &w + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "servers.(Repository).DeleteWorker: delete failed for worker with workerId:", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + deletedRows, err := repo.DeleteWorker(ctx, tt.args.worker.Worker.PublicId) + if tt.wantErr { + assert.Error(err) + assert.Equal(0, deletedRows) + assert.Contains(err.Error(), tt.wantErrMsg) + + return + } + assert.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + + // Validate that the worker no longer exists + err = rw.LookupByPublicId(ctx, tt.args.worker) + assert.ErrorIs(err, dbw.ErrRecordNotFound) + }) + } +} + +func TestLookupWorkerByName(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + ctx := context.Background() + + w := servers.TestKmsWorker(t, conn, wrapper) + t.Run("success", func(t *testing.T) { + got, err := repo.LookupWorkerByName(ctx, w.GetName()) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(w.Worker, got.Worker, protocmp.Transform())) + }) + t.Run("not found", func(t *testing.T) { + got, err := repo.LookupWorkerByName(ctx, "unknown_name") + require.NoError(t, err) + assert.Nil(t, got) + }) + t.Run("db error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + rw := db.New(conn) + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "lookup-error")) + r, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + got, err := r.LookupWorkerByName(ctx, w.GetName()) + assert.NoError(t, mock.ExpectationsWereMet()) + assert.Truef(t, errors.Match(errors.T(errors.Op("servers.(Repository).LookupWorkerByName")), err), "got error %v", err) + assert.Nil(t, got) + }) +} + +func TestLookupWorkerIdByKeyId(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + ctx := context.Background() + var workerKeyId string + w := servers.TestPkiWorker(t, conn, wrapper, servers.WithTestPkiWorkerAuthorizedKeyId(&workerKeyId)) + t.Run("success", func(t *testing.T) { + got, err := repo.LookupWorkerIdByKeyId(ctx, workerKeyId) + require.NoError(t, err) + assert.Equal(t, w.PublicId, got) + }) + t.Run("not found", func(t *testing.T) { + got, err := repo.LookupWorkerIdByKeyId(ctx, "unknown_key") + require.NoError(t, err) + assert.Empty(t, got) + }) + t.Run("db error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + rw := db.New(conn) + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "lookup-error")) + r, err := servers.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + got, err := r.LookupWorkerIdByKeyId(ctx, "somekey") + assert.NoError(t, mock.ExpectationsWereMet()) + assert.Truef(t, errors.Match(errors.T(errors.Op("servers.(Repository).LookupWorkerIdByKeyId")), err), "got error %v", err) + assert.Empty(t, got) + }) +} + +func TestLookupWorker(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + ctx := context.Background() + + w := servers.TestKmsWorker(t, conn, wrapper, + servers.WithName("name"), + servers.WithDescription("description"), + servers.WithAddress("address"), + servers.WithWorkerTags(&servers.Tag{"key", "val"})) + require.NoError(t, err) + + sessRepo, err := session.NewRepository(rw, rw, kms) + require.NoError(t, err) + connRepo, err := session.NewConnectionRepository(ctx, rw, rw, kms, session.WithWorkerStateDelay(0)) + require.NoError(t, err) + // 1 session with 2 connections + { + composedOf := session.TestSessionParams(t, conn, wrapper, iam.TestRepo(t, conn, wrapper)) + future := timestamppb.New(time.Now().Add(time.Hour)) + exp := ×tamp.Timestamp{Timestamp: future} + composedOf.ConnectionLimit = -1 + sess := session.TestSession(t, conn, wrapper, composedOf, session.WithDbOpts(db.WithSkipVetForWrite(true)), session.WithExpirationTime(exp)) + sess, _, err = sessRepo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) + require.NoError(t, err) + c, _, err := connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + c, _, err = connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + } + + // 1 session with 1 connection + { + sess2 := session.TestDefaultSession(t, conn, wrapper, iam.TestRepo(t, conn, wrapper), + session.WithDbOpts(db.WithSkipVetForWrite(true))) + sess2, _, err = sessRepo.ActivateSession(ctx, sess2.GetPublicId(), sess2.Version, []byte("foo")) + require.NoError(t, err) + c, _, err := connRepo.AuthorizeConnection(ctx, sess2.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + } + + t.Run("success", func(t *testing.T) { + got, err := repo.LookupWorker(ctx, w.GetPublicId()) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(w, got, protocmp.Transform())) + assert.Equal(t, uint32(3), got.ActiveConnectionCount()) + assert.Equal(t, map[string][]string{ + "key": {"val"}, + }, got.CanonicalTags()) + assert.Equal(t, got.CanonicalTags(), got.GetConfigTags()) + }) + t.Run("not found", func(t *testing.T) { + got, err := repo.LookupWorker(ctx, "w_unknownid") + require.NoError(t, err) + assert.Nil(t, got) + }) + t.Run("db error", func(t *testing.T) { + conn, mock := db.TestSetupWithMock(t) + rw := db.New(conn) + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "lookup-error")) + r, err := servers.NewRepository(rw, rw, kms) + require.NoError(t, err) + got, err := r.LookupWorker(ctx, w.GetPublicId()) + assert.NoError(t, mock.ExpectationsWereMet()) + assert.Truef(t, errors.Match(errors.T(errors.Op("servers.(Repository).LookupWorker")), err), "got error %v", err) + assert.Nil(t, got) + }) +} + +func TestUpsertWorkerStatus(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + ctx := context.Background() + + t.Run("create an initial kms worker and update status", func(t *testing.T) { + wStatus1 := servers.NewWorker(scope.Global.String(), + servers.WithAddress("address"), servers.WithName("config_name1"), + servers.WithDescription("kms_description1")) + worker, err := repo.UpsertWorkerStatus(ctx, wStatus1) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(worker.GetPublicId(), "w_")) + assert.Equal(t, wStatus1.GetAddress(), worker.GetAddress()) + assert.Equal(t, "config_name1", worker.Name) + assert.Equal(t, "kms_description1", worker.Description) + assert.Equal(t, worker.GetLastStatusTime().AsTime(), worker.GetUpdateTime().AsTime()) + assert.Equal(t, uint32(1), worker.Version) + assert.Equal(t, "address", worker.GetAddress()) + + // update again and see updated last status time + wStatus2 := servers.NewWorker(scope.Global.String(), + servers.WithAddress("new_address"), servers.WithName("config_name1"), + servers.WithDescription("kms_description2")) + worker, err = repo.UpsertWorkerStatus(ctx, wStatus2) + require.NoError(t, err) + assert.Greater(t, worker.GetLastStatusTime().AsTime(), worker.GetCreateTime().AsTime()) + assert.Equal(t, "config_name1", worker.Name) + assert.Equal(t, "kms_description2", worker.Description) + assert.Equal(t, worker.GetLastStatusTime().AsTime(), worker.GetUpdateTime().AsTime()) + assert.Equal(t, uint32(2), worker.Version) + assert.Equal(t, "new_address", worker.GetAddress()) + }) + + // Setup and use a pki worker + var pkiWorkerKeyId string + pkiWorker := servers.TestPkiWorker(t, conn, wrapper, servers.WithName("pki"), servers.WithDescription("pki_description1"), servers.WithTestPkiWorkerAuthorizedKeyId(&pkiWorkerKeyId)) + + t.Run("update status for pki worker", func(t *testing.T) { + wStatus1 := servers.NewWorker(scope.Global.String(), + servers.WithAddress("pki_address"), servers.WithDescription("pki_description2")) + worker, err := repo.UpsertWorkerStatus(ctx, wStatus1, servers.WithKeyId(pkiWorkerKeyId)) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(worker.GetPublicId(), "w_")) + assert.Equal(t, wStatus1.GetAddress(), worker.GetAddress()) + assert.Equal(t, "pki", worker.Name) + assert.Equal(t, "pki_description1", worker.Description) // PKI workers don't update description via status + assert.Greater(t, worker.GetLastStatusTime().AsTime(), worker.GetCreateTime().AsTime()) + assert.Equal(t, worker.GetLastStatusTime().AsTime(), worker.GetUpdateTime().AsTime()) + assert.Equal(t, uint32(1), worker.Version) + assert.Equal(t, "pki_address", worker.GetAddress()) + }) + + failureCases := []struct { + name string + repo *servers.Repository + status *servers.Worker + options []servers.Option + errAssert func(*testing.T, error) + }{ + { + name: "conflicting name with pki", + repo: repo, + status: servers.NewWorker(scope.Global.String(), + servers.WithName(pkiWorker.GetName()), + servers.WithAddress("someaddress")), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.NotUnique), err), err) + }, + }, + { + name: "no address", + repo: repo, + status: servers.NewWorker(scope.Global.String(), + servers.WithName("worker_with_no_address")), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }, + }, + { + name: "cant specify public id", + repo: repo, + status: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String(), + servers.WithName("worker_with_no_address"), + servers.WithAddress("workeraddress")) + w.PublicId = "w_specified" + return w + }(), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }, + }, + { + name: "name and key id provided", + repo: repo, + status: servers.NewWorker(scope.Global.String(), + servers.WithName("name and key id provided"), + servers.WithAddress("someaddress")), + options: []servers.Option{servers.WithKeyId(pkiWorkerKeyId)}, + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err), err) + }, + }, + { + name: "no name or key id", + repo: repo, + status: servers.NewWorker(scope.Global.String(), + servers.WithAddress("no_name_address")), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }, + }, + { + name: "no status", + repo: repo, + status: nil, + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }, + }, + { + name: "empty scope", + repo: repo, + status: servers.NewWorker("", + servers.WithAddress("address"), + servers.WithName("config_name1")), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err)) + }, + }, + { + name: "database failure", + repo: func() *servers.Repository { + conn, mock := db.TestSetupWithMock(t) + rw := db.New(conn) + mock.ExpectBegin() + mock.ExpectQuery(`INSERT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "create-error")) + mock.ExpectRollback() + r, err := servers.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + return r + }(), + status: servers.NewWorker(scope.Global.String(), + servers.WithName("database failure"), + servers.WithAddress("address")), + errAssert: func(t *testing.T, err error) { + t.Helper() + assert.Error(t, err) + }, + }, + } + for _, tc := range failureCases { + t.Run(fmt.Sprintf("Failures %s", tc.name), func(t *testing.T) { + _, err = tc.repo.UpsertWorkerStatus(ctx, tc.status, tc.options...) + assert.Error(t, err) + tc.errAssert(t, err) + + // Still only the original worker exists. + workers, err := repo.ListWorkers(ctx, []string{scope.Global.String()}) + require.NoError(t, err) + assert.Len(t, workers, 2) + }) + } + + t.Run("add another status", func(t *testing.T) { + anotherStatus := servers.NewWorker(scope.Global.String(), + servers.WithName("another_test_worker"), + servers.WithAddress("address")) + _, err = repo.UpsertWorkerStatus(ctx, anotherStatus) + require.NoError(t, err) + + workers, err := repo.ListWorkers(ctx, []string{scope.Global.String()}) + require.NoError(t, err) + assert.Len(t, workers, 3) + }) +} + +func TestTagUpdatingListing(t *testing.T) { + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := servers.NewRepository(rw, rw, kms) + require.NoError(err) + ctx := context.Background() + + worker1 := servers.TestKmsWorker(t, conn, wrapper) + wStatus := servers.NewWorker(scope.Global.String(), + servers.WithName(worker1.GetName()), + servers.WithAddress("somethingnew"), + servers.WithWorkerTags( + &servers.Tag{ + Key: "tag1", + Value: "value1", + }, + &servers.Tag{ + Key: "tag1", + Value: "value2", + })) + + worker1, err = repo.UpsertWorkerStatus(ctx, wStatus, + servers.WithUpdateTags(true)) + require.NoError(err) + assert.Len(t, worker1.CanonicalTags(), 1) + assert.ElementsMatch(t, []string{"value1", "value2"}, worker1.CanonicalTags()["tag1"]) + + // Update without saying to update tags + wStatus = servers.NewWorker(scope.Global.String(), + servers.WithName(worker1.GetName()), + servers.WithAddress(worker1.GetAddress()), + servers.WithWorkerTags( + &servers.Tag{ + Key: "tag22", + Value: "value21", + }, + &servers.Tag{ + Key: "tag22", + Value: "value22", + })) + worker1, err = repo.UpsertWorkerStatus(ctx, wStatus) + require.NoError(err) + assert.Len(t, worker1.CanonicalTags(), 1) + assert.ElementsMatch(t, []string{"value1", "value2"}, worker1.CanonicalTags()["tag1"]) + + // Update tags and test again + worker1, err = repo.UpsertWorkerStatus(ctx, wStatus, servers.WithUpdateTags(true)) + require.NoError(err) + assert.Len(t, worker1.CanonicalTags(), 1) + assert.ElementsMatch(t, []string{"value21", "value22"}, worker1.CanonicalTags()["tag22"]) +} + +func TestListWorkers(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + const testLimit = 10 + repo, err := servers.NewRepository(rw, rw, kms, servers.WithLimit(testLimit)) + require.NoError(t, err) + ctx := context.Background() + + tests := []struct { + name string + createCnt int + reqScopes []string + opts []servers.Option + wantCnt int + wantErr bool + }{ + { + name: "no-limit", + createCnt: testLimit + 1, + reqScopes: []string{scope.Global.String()}, + opts: []servers.Option{servers.WithLimit(-1)}, + wantCnt: testLimit + 1, + wantErr: false, + }, + { + name: "default-limit", + createCnt: testLimit + 1, + reqScopes: []string{scope.Global.String()}, + wantCnt: testLimit, + wantErr: false, + }, + { + name: "custom-limit", + createCnt: testLimit + 1, + reqScopes: []string{scope.Global.String()}, + opts: []servers.Option{servers.WithLimit(3)}, + wantCnt: 3, + wantErr: false, + }, + { + name: "no-scope", + createCnt: 1, + reqScopes: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := servers.NewWorker(scope.Global.String()) + db.TestDeleteWhere(t, conn, w, "true") + for i := 0; i < tt.createCnt; i++ { + servers.TestKmsWorker(t, conn, wrapper) + } + // the purpose of these tests isn't to check liveness, so disable + // liveness checking. + opts := append(tt.opts, servers.WithLiveness(-1)) + got, err := repo.ListWorkers(ctx, tt.reqScopes, opts...) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Len(t, got, tt.wantCnt) + }) + } +} + +func TestListWorkers_WithLiveness(t *testing.T) { + t.Parallel() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + serversRepo, err := servers.NewRepository(rw, rw, kms) + require.NoError(err) + ctx := context.Background() + + worker1 := servers.TestKmsWorker(t, conn, wrapper) + worker2 := servers.TestKmsWorker(t, conn, wrapper) + worker3 := servers.TestKmsWorker(t, conn, wrapper) + + // Sleep the default liveness time (15sec currently) +1s + time.Sleep(time.Second * 16) + + // Push an upsert to the first worker so that its status has been + // updated. + _, err = serversRepo.UpsertWorkerStatus(ctx, + servers.NewWorker(scope.Global.String(), + servers.WithName(worker1.GetName()), + servers.WithAddress(worker1.GetAddress())), + servers.WithPublicId(worker1.GetPublicId())) + require.NoError(err) + + requireIds := func(expected []string, actual []*servers.Worker) { + require.Len(expected, len(actual)) + want := make(map[string]struct{}) + for _, v := range expected { + want[v] = struct{}{} + } + + got := make(map[string]struct{}) + for _, v := range actual { + got[v.PublicId] = struct{}{} + } + + require.Equal(want, got) + } + + // Default liveness, should only list 1 + result, err := serversRepo.ListWorkers(ctx, []string{scope.Global.String()}) + require.NoError(err) + require.Len(result, 1) + requireIds([]string{worker1.GetPublicId()}, result) + + // Upsert second server. + _, err = serversRepo.UpsertWorkerStatus(ctx, + servers.NewWorker(scope.Global.String(), + servers.WithName(worker2.GetName()), + servers.WithAddress(worker2.GetAddress())), + servers.WithPublicId(worker2.GetPublicId())) + require.NoError(err) + + // Static liveness. Should get two, so long as this did not take + // more than 5s to execute. + result, err = serversRepo.ListWorkers(ctx, []string{scope.Global.String()}, servers.WithLiveness(time.Second*5)) + require.NoError(err) + require.Len(result, 2) + requireIds([]string{worker1.GetPublicId(), worker2.GetPublicId()}, result) + + // Liveness disabled, should get all three workers. + result, err = serversRepo.ListWorkers(ctx, []string{scope.Global.String()}, servers.WithLiveness(-1)) + require.NoError(err) + require.Len(result, 3) + requireIds([]string{worker1.GetPublicId(), worker2.GetPublicId(), worker3.GetPublicId()}, result) +} + +func TestRepository_CreateWorker(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + testRepo, err := servers.NewRepository(rw, rw, testKms) + require.NoError(t, err) + + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + testNewIdFn := func(context.Context) (string, error) { + return "", errors.New(testCtx, errors.Internal, "test", "testNewIdFn-error") + } + + kmsWorker := servers.TestKmsWorker(t, conn, wrapper) + + rootStorage, err := servers.NewRepositoryStorage(testCtx, rw, rw, testKms) + require.NoError(t, err) + _, err = rotation.RotateRootCertificates(testCtx, rootStorage) + require.NoError(t, err) + + tests := []struct { + name string + setup func() *servers.Worker + repo *servers.Repository + fetchReq *types.FetchNodeCredentialsRequest + reader db.Reader + opt []servers.Option + wantErr bool + wantErrIs errors.Code + wantErrContains string + }{ + { + name: "missing-worker", + setup: func() *servers.Worker { + return nil + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "missing worker", + }, + { + name: "public-id-not-empty", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + var err error + w.PublicId, err = db.NewPublicId(servers.WorkerPrefix) + require.NoError(t, err) + return w + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "public id is not empty", + }, + { + name: "empty-scope-id", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.ScopeId = "" + return w + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "missing scope id", + }, + { + name: "org-scope-id", + setup: func() *servers.Worker { + w := servers.NewWorker(org.PublicId) + return w + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "scope id must be \"global\"", + }, + { + name: "new-id-error", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + return w + }, + repo: testRepo, + opt: []servers.Option{servers.WithNewIdFunc(testNewIdFn)}, + wantErr: true, + wantErrIs: errors.Internal, + wantErrContains: "testNewIdFn-error", + }, + { + name: "create-error", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + return w + }, + repo: func() *servers.Repository { + conn, mock := db.TestSetupWithMock(t) + writer := db.New(conn) + mock.ExpectBegin() + mock.ExpectQuery(`INSERT`).WillReturnError(errors.New(testCtx, errors.Internal, "test", "create-error")) + mock.ExpectRollback() + r, err := servers.NewRepository(rw, writer, testKms) + require.NoError(t, err) + return r + }(), + wantErr: true, + wantErrContains: "unable to create worker", + }, + { + name: "no worker reported address", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.Address = "foo" + return w + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "address is not empty", + }, + { + name: "no last status update", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.LastStatusTime = timestamp.Now() + return w + }, + repo: testRepo, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "last status time is not nil", + }, + { + name: "no-database-key", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + return w + }, + reader: rw, + fetchReq: func() *types.FetchNodeCredentialsRequest { + // This happens on the worker + fileStorage, err := file.New(testCtx) + require.NoError(t, err) + defer fileStorage.Cleanup() + + nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage) + require.NoError(t, err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx) + require.NoError(t, err) + return fetchReq + }(), + repo: func() *servers.Repository { + mockConn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now())) + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "no-database-key")) + k := kms.TestKms(t, mockConn, wrapper) + r, err := servers.NewRepository(rw, rw, k) + require.NoError(t, err) + return r + }(), + wantErr: true, + wantErrContains: "unable to get wrapper", + }, + { + name: "bad-fetch-node-req", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + return w + }, + reader: rw, + fetchReq: &types.FetchNodeCredentialsRequest{}, + repo: testRepo, + wantErr: true, + wantErrContains: "unable to authorize node", + }, + { + name: "unique name violation with kms worker", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.Name = kmsWorker.GetName() + return w + }, + reader: rw, + repo: testRepo, + wantErr: true, + wantErrIs: errors.NotUnique, + }, + { + name: "success", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.Name = "success" + return w + }, + reader: rw, + repo: testRepo, + }, + // This case must follow the one above it. + { + name: "unique name violation with pki worker", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.Name = "success" + return w + }, + reader: rw, + repo: testRepo, + wantErr: true, + wantErrIs: errors.NotUnique, + }, + { + name: "success-with-fetch-node-req", + setup: func() *servers.Worker { + w := servers.NewWorker(scope.Global.String()) + w.Name = "success-with-fetch-node-req" + return w + }, + reader: rw, + fetchReq: func() *types.FetchNodeCredentialsRequest { + // This happens on the worker + fileStorage, err := file.New(testCtx) + require.NoError(t, err) + defer fileStorage.Cleanup() + + nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage) + require.NoError(t, err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx) + require.NoError(t, err) + return fetchReq + }(), + repo: testRepo, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + testWorker := tc.setup() + + tc.opt = append(tc.opt, servers.WithFetchNodeCredentialsRequest(tc.fetchReq)) + + got, err := tc.repo.CreateWorker(testCtx, testWorker, tc.opt...) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrIs != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrIs), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotNil(got) + assert.NotEmpty(got.PublicId) + assert.NotEmpty(got.UpdateTime) + assert.NotEmpty(got.CreateTime) + + found := &servers.Worker{ + Worker: &store.Worker{ + PublicId: got.PublicId, + }, + } + err = rw.LookupByPublicId(testCtx, found) + require.NoError(err) + assert.Equal(got, found) + + if tc.fetchReq != nil { + worker := &servers.WorkerAuth{ + WorkerAuth: &store.WorkerAuth{}, + } + require.NoError(tc.reader.LookupWhere(testCtx, worker, "worker_id = ?", []any{found.PublicId})) + } + }) + } +} + +func TestRepository_UpdateWorker(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + repo, err := servers.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + pkiCases := []struct { + name string + modifyWorker func(*testing.T, *servers.Worker) + path []string + assertGot func(*testing.T, *servers.Worker) + wantErr bool + }{ + { + name: "update name", + modifyWorker: func(t *testing.T, w *servers.Worker) { + t.Helper() + w.Name = "foo" + }, + path: []string{"Name"}, + assertGot: func(t *testing.T, w *servers.Worker) { + t.Helper() + assert.Equal(t, "foo", w.GetName()) + assert.Equal(t, uint32(2), w.GetVersion()) + assert.Nil(t, w.GetLastStatusTime()) + assert.Greater(t, w.GetUpdateTime().AsTime(), w.GetCreateTime().AsTime()) + }, + }, + { + name: "update description", + modifyWorker: func(t *testing.T, w *servers.Worker) { + t.Helper() + w.Description = "foo" + }, + path: []string{"Description"}, + assertGot: func(t *testing.T, w *servers.Worker) { + t.Helper() + assert.Equal(t, "foo", w.GetDescription()) + assert.Equal(t, uint32(2), w.GetVersion()) + assert.Nil(t, w.GetLastStatusTime()) + assert.Greater(t, w.GetUpdateTime().AsTime(), w.GetCreateTime().AsTime()) + }, + }, + } + for _, tt := range pkiCases { + t.Run(tt.name, func(t *testing.T) { + wkr := servers.TestPkiWorker(t, conn, wrapper) + defer func() { + _, err := repo.DeleteWorker(ctx, wkr.GetPublicId()) + require.NoError(t, err) + }() + tt.modifyWorker(t, wkr) + got, _, err := repo.UpdateWorker(ctx, wkr, 1, tt.path) + if tt.wantErr { + assert.Error(t, err) + return + } else { + require.NoError(t, err) + } + tt.assertGot(t, got) + }) + } + + // Clear cases + clearCases := []struct { + name string + modifyWorker func(*testing.T, *servers.Worker) + path []string + assertGot func(*testing.T, *servers.Worker) + wantErr bool + }{ + { + name: "clear name", + modifyWorker: func(t *testing.T, w *servers.Worker) { + t.Helper() + w.Name = "" + }, + path: []string{"Name"}, + assertGot: func(t *testing.T, w *servers.Worker) { + t.Helper() + assert.Empty(t, w.GetName()) + assert.Nil(t, w.GetLastStatusTime()) + assert.Greater(t, w.GetUpdateTime().AsTime(), w.GetCreateTime().AsTime()) + }, + }, + { + name: "clear description", + modifyWorker: func(t *testing.T, w *servers.Worker) { + t.Helper() + w.Description = "" + }, + path: []string{"Description"}, + assertGot: func(t *testing.T, w *servers.Worker) { + t.Helper() + assert.Empty(t, w.GetDescription()) + assert.Nil(t, w.GetLastStatusTime()) + assert.Greater(t, w.GetUpdateTime().AsTime(), w.GetCreateTime().AsTime()) + }, + }, + } + for _, tt := range clearCases { + t.Run(tt.name, func(t *testing.T) { + wkr := servers.TestPkiWorker(t, conn, wrapper) + wkr.Name = tt.name + wkr.Description = tt.name + wkr, _, err := repo.UpdateWorker(ctx, wkr, 1, []string{"name", "description"}) + require.NoError(t, err) + + tt.modifyWorker(t, wkr) + got, _, err := repo.UpdateWorker(ctx, wkr, 2, tt.path) + if tt.wantErr { + assert.Error(t, err) + return + } else { + require.NoError(t, err) + } + tt.assertGot(t, got) + }) + } + + t.Run("version is wrong", func(t *testing.T) { + wkr := servers.TestPkiWorker(t, conn, wrapper) + wkr.Name = "version is wrong" + result, numUpdated, err := repo.UpdateWorker(ctx, wkr, 2, []string{"name"}) + require.NoError(t, err) + assert.Zero(t, numUpdated) + assert.Equal(t, wkr.GetUpdateTime().AsTime(), result.GetUpdateTime().AsTime()) + }) + + errorCases := []struct { + name string + input *servers.Worker + path []string + version uint32 + wantErr *errors.Template + }{ + { + name: "nil worker", + path: []string{"name"}, + version: 1, + wantErr: errors.T(errors.InvalidParameter), + }, + { + name: "empty path", + input: servers.TestPkiWorker(t, conn, wrapper), + version: 1, + wantErr: errors.T(errors.EmptyFieldMask), + }, + { + name: "0 version", + input: servers.TestPkiWorker(t, conn, wrapper), + path: []string{"name"}, + version: 0, + wantErr: errors.T(errors.InvalidParameter), + }, + { + name: "changing kms name", + input: func() *servers.Worker { + w := servers.TestKmsWorker(t, conn, wrapper) + w.Name = "some change" + return w + }(), path: []string{"name"}, + version: 1, + wantErr: errors.T(errors.NotSpecificIntegrity), + }, + { + name: "clearing kms name", + input: func() *servers.Worker { + w := servers.TestKmsWorker(t, conn, wrapper) + w.Name = "" + return w + }(), path: []string{"name"}, + version: 1, + wantErr: errors.T(errors.NotSpecificIntegrity), + }, + { + name: "no public id", + input: func() *servers.Worker { + w := servers.TestPkiWorker(t, conn, wrapper) + w.PublicId = "" + return w + }(), + path: []string{"name"}, + version: 0, + wantErr: errors.T(errors.InvalidParameter), + }, + { + name: "unrecognized path", + input: servers.TestPkiWorker(t, conn, wrapper), + path: []string{"UnrecognizedField"}, + version: 1, + wantErr: errors.T(errors.InvalidFieldMask), + }, + { + name: "not found worker", + input: func() *servers.Worker { + w := servers.TestPkiWorker(t, conn, wrapper) + w.PublicId = "w_notfoundworker" + return w + }(), + path: []string{"name"}, + version: 1, + wantErr: errors.T(errors.RecordNotFound), + }, + { + name: "duplicate name", + input: func() *servers.Worker { + w1 := servers.TestPkiWorker(t, conn, wrapper) + w1.Name = "somenamethatijustmadeup" + w1, _, err := repo.UpdateWorker(ctx, w1, w1.Version, []string{"name"}, nil) + require.NoError(t, err) + w2 := servers.TestPkiWorker(t, conn, wrapper) + w2.Name = w1.Name + return w2 + }(), + path: []string{"name"}, + version: 1, + wantErr: errors.T("worker with name \"somenamethatijustmadeup\" already exists"), + }, + } + for _, tt := range errorCases { + t.Run(tt.name, func(t *testing.T) { + _, updated, err := repo.UpdateWorker(ctx, tt.input, tt.version, tt.path) + assert.Equal(t, 0, updated) + assert.Truef(t, errors.Match(tt.wantErr, err), "Didn't match error %v", err) + }) + } +} diff --git a/internal/servers/repository_workerauth.go b/internal/servers/repository_workerauth.go new file mode 100644 index 000000000..9c94b4bb0 --- /dev/null +++ b/internal/servers/repository_workerauth.go @@ -0,0 +1,781 @@ +package servers + +import ( + "context" + "crypto/x509" + "database/sql" + "fmt" + "strconv" + + "github.com/fatih/structs" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-dbw" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/nodeenrollment" + nodee "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/proto" +) + +// Ensure we implement the Storage interfaces +var ( + _ nodee.Storage = (*WorkerAuthRepositoryStorage)(nil) +) + +type rootCertificatesVersion struct { + Version uint32 `mapstructure:"version"` +} + +// WorkerAuthRepositoryStorage is the Worker Auth database repository +type WorkerAuthRepositoryStorage struct { + reader db.Reader + writer db.Writer + kms *kms.Kms +} + +// NewRepositoryStorage creates a new WorkerAuthRepositoryStorage that implements the Storage interface +func NewRepositoryStorage(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms) (*WorkerAuthRepositoryStorage, error) { + const op = "servers.(WorkerAuthRepositoryStorage).NewRepositoryStorage" + switch { + case isNil(r): + return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil") + case isNil(w): + return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil") + case isNil(kms): + return nil, errors.New(ctx, errors.InvalidParameter, op, "kms is nil") + } + + workerAuthRepoStorage := &WorkerAuthRepositoryStorage{ + reader: r, + writer: w, + kms: kms, + } + + return workerAuthRepoStorage, nil +} + +// Store implements the Storage interface +func (r *WorkerAuthRepositoryStorage) Store(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Store" + if err := types.ValidateMessage(msg); err != nil { + return errors.Wrap(ctx, err, op) + } + if msg.GetId() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be stored as it has no ID") + } + + // Determine type of message to store + switch t := msg.(type) { + case *types.NodeInformation: + // Encrypt the private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(read db.Reader, w db.Writer) error { + return StoreNodeInformationTx(ctx, w, databaseWrapper, t) + }); err != nil { + return errors.Wrap(ctx, err, op) + } + case *types.RootCertificates: + err := r.storeRootCertificates(ctx, t) + if err != nil { + return errors.Wrap(ctx, err, op) + } + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Store", msg)) + } + + return nil +} + +// StoreNodeInformationTx stores NodeInformation. No options are currently +// supported. +// +// This function encapsulates all the work required within a dbw.TxHandler and +// allows this capability to be shared with other repositories or just called +// within a transaction. To be clear, this repository function doesn't include +// its own transaction and is intended to be used within a transaction provided +// by the caller. +// +// Node information is stored in two parts: +// * the workerAuth record is stored with a reference to a worker +// * certificate bundles are stored with a reference to the workerAuth record and issuing root certificate +func StoreNodeInformationTx(ctx context.Context, writer db.Writer, databaseWrapper wrapping.Wrapper, node *types.NodeInformation, _ ...Option) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeNodeInformation" + if isNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + if isNil(databaseWrapper) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database wrapper") + } + if node == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing NodeInformation") + } + + nodeAuth := allocWorkerAuth() + nodeAuth.WorkerKeyIdentifier = node.Id + nodeAuth.WorkerEncryptionPubKey = node.EncryptionPublicKeyBytes + nodeAuth.WorkerSigningPubKey = node.CertificatePublicKeyPkix + nodeAuth.Nonce = node.RegistrationNonce + + var err error + nodeAuth.KeyId, err = databaseWrapper.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.ControllerEncryptionPrivKey, err = encrypt(ctx, node.ServerEncryptionPrivateKeyBytes, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + // Get workerId from state passed in + var result workerAuthWorkerId + err = mapstructure.Decode(node.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + nodeAuth.WorkerId = result.WorkerId + + if err := nodeAuth.ValidateNewWorkerAuth(ctx); err != nil { + return errors.Wrap(ctx, err, op) + } + + // Store WorkerAuth + if err := writer.Create(ctx, &nodeAuth); err != nil { + return errors.Wrap(ctx, err, op) + } + // Then store cert bundles associated with this WorkerAuth + for _, c := range node.CertificateBundles { + if err := storeWorkerCertBundle(ctx, c, nodeAuth.WorkerKeyIdentifier, writer); err != nil { + return errors.Wrap(ctx, err, op) + } + } + + return nil +} + +func storeWorkerCertBundle( + ctx context.Context, + bundle *types.CertificateBundle, + workerKeyIdentifier string, + writer db.Writer, +) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeWorkerCertBundle" + if bundle == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing CertificateBundle") + } + if workerKeyIdentifier == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty workerKeyIdentifier") + } + if writer == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + + workerCertBundle := allocWorkerCertBundle() + bundleBytes, err := proto.Marshal(bundle) + if err != nil { + return errors.New(ctx, errors.Encode, op, "error marshaling nodetypes.CertificateBundle", errors.WithWrap(err)) + } + + // Extract serial number from CA cert + caCert := bundle.CaCertificateDer + parsedCert, err := x509.ParseCertificate(caCert) + if err != nil { + return errors.Wrap(ctx, err, op) + } + workerCertBundle.RootCertificatePublicKey = parsedCert.AuthorityKeyId + workerCertBundle.CertBundle = bundleBytes + workerCertBundle.WorkerKeyIdentifier = workerKeyIdentifier + + err = workerCertBundle.ValidateNewWorkerCertBundle(ctx) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + err = writer.Create(ctx, &workerCertBundle) + + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func (r *WorkerAuthRepositoryStorage) convertRootCertificate(ctx context.Context, cert *types.RootCertificate) (*RootCertificate, error) { + const op = "servers.(WorkerAuthRepositoryStorage).convertRootCertificate" + if cert == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing RootCertificate") + } + + rootCert := allocRootCertificate() + + parsedCert, err := x509.ParseCertificate(cert.CertificateDer) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.SerialNumber = parsedCert.SerialNumber.Uint64() + rootCert.Certificate = cert.CertificateDer + rootCert.NotValidAfter = timestamp.New(cert.NotAfter.AsTime()) + rootCert.NotValidBefore = timestamp.New(cert.NotBefore.AsTime()) + rootCert.PublicKey = cert.PublicKeyPkix + rootCert.State = cert.Id + rootCert.IssuingCa = ca_id + + // Encrypt the private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.KeyId, err = databaseWrapper.KeyId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rootCert.PrivateKey, err = encrypt(ctx, cert.PrivateKeyPkcs8, databaseWrapper) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + err = rootCert.ValidateNewRootCertificate(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return rootCert, nil +} + +func (r *WorkerAuthRepositoryStorage) storeRootCertificates(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).storeRootCertificates" + if cert == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing RootCertificate") + } + + nextCert, err := r.convertRootCertificate(ctx, cert.Next) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert next root certificate")) + } + currentCert, err := r.convertRootCertificate(ctx, cert.Current) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert current root certificate")) + } + // Use passed version + var result rootCertificatesVersion + err = mapstructure.Decode(cert.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + version := uint32(1) + if result.Version != 0 { + version = result.Version + } + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: ca_id, + Version: version + 1, + }, + } + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + // Delete the old certs + if err = r.removeRootCertificateWithWriter(ctx, string(CurrentState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + if err = r.removeRootCertificateWithWriter(ctx, string(NextState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + + // Update the certAuthority's version column + rowsUpdated, err := w.Update(ctx, certAuthority, []string{"Version"}, nil, db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated != 1 { + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated CertificateAuthority and %d rows updated", rowsUpdated)) + } + + // Then insert the new certs + if nextCert != nil { + if err = w.Create(ctx, &nextCert); err != nil { + return errors.Wrap(ctx, err, op) + } + } + if currentCert != nil { + if err = w.Create(ctx, ¤tCert); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +// Load implements the Storage interface. +// Load loads values into the given message. The message must be populated +// with the ID value. If not found, the returned error should be ErrNotFound. +func (r *WorkerAuthRepositoryStorage) Load(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Load" + if err := types.ValidateMessage(msg); err != nil { + return errors.Wrap(ctx, err, op) + } + if msg.GetId() == "" { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be loaded as it has no ID") + } + + var err error + switch t := msg.(type) { + case *types.NodeInformation: + err = r.loadNodeInformation(ctx, t) + + case *types.RootCertificates: + err = r.loadRootCertificates(ctx, t) + + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Load", t)) + } + + if err != nil { + if err == nodeenrollment.ErrNotFound { + // Don't wrap as this will confuse things + return err + } + return errors.Wrap(ctx, err, op) + } + + return nil +} + +// Node information is loaded in two parts: +// * the workerAuth record +// * its certificate bundles +func (r *WorkerAuthRepositoryStorage) loadNodeInformation(ctx context.Context, node *types.NodeInformation) error { + const op = "servers.(WorkerAuthRepositoryStorage).loadNodeInformation" + if node == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing NodeInformation") + } + + authorizedWorker, err := r.findWorkerAuth(ctx, node) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if authorizedWorker == nil { + return nodee.ErrNotFound + } + + node.EncryptionPublicKeyBytes = authorizedWorker.WorkerEncryptionPubKey + node.CertificatePublicKeyPkix = authorizedWorker.WorkerSigningPubKey + node.RegistrationNonce = authorizedWorker.Nonce + + // Default values are used for key types + node.EncryptionPublicKeyType = types.KEYTYPE_X25519 + node.CertificatePublicKeyType = types.KEYTYPE_ED25519 + node.ServerEncryptionPrivateKeyType = types.KEYTYPE_X25519 + + // Decrypt private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(authorizedWorker.KeyId)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.ServerEncryptionPrivateKeyBytes, err = decrypt(ctx, authorizedWorker.ControllerEncryptionPrivKey, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + workerIdInfo := workerAuthWorkerId{WorkerId: authorizedWorker.GetWorkerId()} + s := structs.New(workerIdInfo) + s.TagName = "mapstructure" + state, err := structpb.NewStruct(s.Map()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.State = state + + // Get cert bundles from the other table + certBundles, err := r.findCertBundles(ctx, node.Id) + if err != nil { + return errors.Wrap(ctx, err, op) + } + node.CertificateBundles = certBundles + + return nil +} + +func (r *WorkerAuthRepositoryStorage) findCertBundles(ctx context.Context, workerKeyId string) ([]*types.CertificateBundle, error) { + const op = "servers.(WorkerAuthRepositoryStorage).findCertBundles" + if workerKeyId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty workerKeyId") + } + + var bundles []*WorkerCertBundle + err := r.reader.SearchWhere(ctx, &bundles, "worker_key_identifier = ?", []interface{}{workerKeyId}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + certBundle := []*types.CertificateBundle{} + for _, bundle := range bundles { + thisBundle := &types.CertificateBundle{} + if err := proto.Unmarshal(bundle.WorkerCertBundle.CertBundle, thisBundle); err != nil { + return nil, errors.New(ctx, errors.Decode, op, "error unmarshaling message", errors.WithWrap(err)) + } + certBundle = append(certBundle, thisBundle) + } + + return certBundle, nil +} + +func (r *WorkerAuthRepositoryStorage) findWorkerAuth(ctx context.Context, node *types.NodeInformation) (*WorkerAuth, error) { + const op = "servers.(WorkerAuthRepositoryStorage).findWorkerAuth" + if node == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "node is nil") + } + + worker := allocWorkerAuth() + worker.WorkerKeyIdentifier = node.Id + + err := r.reader.LookupById(ctx, worker) + if err != nil { + if errors.Is(err, dbw.ErrRecordNotFound) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + + return worker, nil +} + +func (r *WorkerAuthRepositoryStorage) loadRootCertificates(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).loadRootCertificates" + if cert == nil { + return errors.New(ctx, errors.InvalidParameter, op, "cert is nil") + } + + // See if we have the CertificateAuthority first + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: cert.Id, + }, + } + err := r.reader.LookupById(ctx, certAuthority) + if err != nil { + return nodee.ErrNotFound + } + + // Add version to state field of cert + versionMap := &rootCertificatesVersion{Version: certAuthority.Version} + stateOpt := structs.Map(versionMap) + state, err := structpb.NewStruct(stateOpt) + if err != nil { + return errors.Wrap(ctx, err, op) + } + cert.State = state + + // Load current and next certificates + certsToFind := []string{string(NextState), string(CurrentState)} + for _, c := range certsToFind { + + rootCertificate := allocRootCertificate() + rootCert := &types.RootCertificate{} + + if err := r.reader.SearchWhere(ctx, &rootCertificate, "state = ?", []interface{}{c}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op) + } + + if rootCertificate.Certificate == nil { + return nodee.ErrNotFound + } + + rootCert.CertificateDer = rootCertificate.Certificate + rootCert.NotAfter = rootCertificate.NotValidAfter.Timestamp + rootCert.NotBefore = rootCertificate.NotValidBefore.Timestamp + rootCert.PublicKeyPkix = rootCertificate.PublicKey + rootCert.PrivateKeyType = types.KEYTYPE_ED25519 + + // decrypt private key + databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(rootCertificate.KeyId)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + rootCert.PrivateKeyPkcs8, err = decrypt(ctx, rootCertificate.PrivateKey, databaseWrapper) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + if c == string(NextState) { + cert.Next = rootCert + } + if c == string(CurrentState) { + cert.Current = rootCert + } + } + + return nil +} + +// Remove implements the Storage interface. +// Remove removes the given message. Only the ID field of the message is considered. +func (r *WorkerAuthRepositoryStorage) Remove(ctx context.Context, msg nodee.MessageWithId) error { + const op = "servers.(WorkerAuthRepositoryStorage).Remove" + if err := types.ValidateMessage(msg); err != nil { + return errors.New(ctx, errors.InvalidParameter, op, "given message cannot be removed as it has no ID") + } + + // Determine type of message to remove + switch msg.(type) { + case *types.NodeInformation: + err := r.removeNodeInformation(ctx, msg.GetId()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + case *types.RootCertificates: + err := r.removeCertificateAuthority(ctx, msg.(*types.RootCertificates)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for Remove", msg)) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context, id string) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeNodeInformation" + if id == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty id") + } + + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + var err error + _, err = w.Exec(ctx, deleteWorkerAuthQuery, []interface{}{sql.Named("worker_key_identifier", id)}) + if err != nil { + return errors.Wrap(ctx, err, op) + } + _, err = w.Exec(ctx, deleteWorkerCertBundlesQuery, []interface{}{sql.Named("worker_key_identifier", id)}) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete workerAuth")) + } + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeCertificateAuthority(ctx context.Context, cert *types.RootCertificates) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeCertificateAuthority" + + var result rootCertificatesVersion + err := mapstructure.Decode(cert.State.AsMap(), &result) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + version := uint32(1) + if result.Version != 0 { + version = result.Version + } + certAuthority := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: ca_id, + Version: version + 1, + }, + } + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + // Update the certAuthority's version column first + rowsUpdated, err := w.Update(ctx, certAuthority, []string{"Version"}, nil, db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated != 1 { + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated CertificateAuthority and %d rows updated", rowsUpdated)) + } + + if err := r.removeRootCertificateWithWriter(ctx, string(CurrentState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + if err := r.removeRootCertificateWithWriter(ctx, string(NextState), w); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} + +func (r *WorkerAuthRepositoryStorage) removeRootCertificateWithWriter(ctx context.Context, id string, writer db.Writer) error { + const op = "servers.(WorkerAuthRepositoryStorage).removeRootCertificateWithWriter" + if id == "" { + return errors.New(ctx, errors.InvalidParameter, op, "empty id") + } + if writer == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing writer") + } + + rows, err := writer.Exec(ctx, deleteRootCertificateQuery, []interface{}{ + sql.Named("state", id), + }) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete root certificate")) + } + if rows > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 root certificate would have been deleted") + } + return nil +} + +// List implements the Storage interface. +// List returns a list of IDs; the type of the message is used to disambiguate what to list. +func (r *WorkerAuthRepositoryStorage) List(ctx context.Context, msg proto.Message) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).List" + + var err error + var ids []string + // Determine type of message to store + switch msg.(type) { + case *types.NodeInformation: + ids, err = r.listNodeInformation(ctx) + case *types.RootCertificate: + ids, err = r.listRootCertificates(ctx) + case *types.RootCertificates: + ids, err = r.listCertificateAuthority(ctx) + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("message type %T not supported for List", msg)) + } + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return ids, nil +} + +// Returns a list of node auth IDs +func (r *WorkerAuthRepositoryStorage) listNodeInformation(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listNodeCertificates" + + var where string + var nodeAuths []*WorkerAuth + err := r.reader.SearchWhere(ctx, &nodeAuths, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var nodeIds []string + for _, auth := range nodeAuths { + nodeIds = append(nodeIds, auth.WorkerKeyIdentifier) + } + return nodeIds, nil +} + +// Returns a list of root certificates +func (r *WorkerAuthRepositoryStorage) listRootCertificates(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listRootCertificates" + + var where string + var rootCertificates []*RootCertificate + err := r.reader.SearchWhere(ctx, &rootCertificates, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var certIds []string + for _, cert := range rootCertificates { + certIds = append(certIds, strconv.FormatUint(cert.SerialNumber, 10)) + } + + return certIds, nil +} + +// Returns a list of certificate authorities +func (r *WorkerAuthRepositoryStorage) listCertificateAuthority(ctx context.Context) ([]string, error) { + const op = "servers.(WorkerAuthRepositoryStorage).listCertificateAuthority" + + var where string + var rootCertificates []*CertificateAuthority + err := r.reader.SearchWhere(ctx, &rootCertificates, where, []interface{}{}, db.WithLimit(-1)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var certIds []string + for _, cert := range rootCertificates { + certIds = append(certIds, cert.PrivateId) + } + + return certIds, nil +} + +// encrypt value before writing it to the db +func encrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { + const op = "servers.(WorkerAuthRepositoryStorage).encrypt" + if value == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") + } + if wrapper == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") + } + + blobInfo, err := wrapper.Encrypt(ctx, value) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error encrypting recovery info", errors.WithWrap(err)) + } + marshaledBlob, err := proto.Marshal(blobInfo) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error marshaling encrypted blob", errors.WithWrap(err)) + } + return marshaledBlob, nil +} + +func decrypt(ctx context.Context, value []byte, wrapper wrapping.Wrapper) ([]byte, error) { + const op = "servers.(WorkerAuthRepositoryStorage).decrypt" + if value == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing value") + } + if wrapper == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapper") + } + + blobInfo := new(wrapping.BlobInfo) + if err := proto.Unmarshal(value, blobInfo); err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error decoding encrypted blob", errors.WithWrap(err)) + } + + marshaledInfo, err := wrapper.Decrypt(ctx, blobInfo) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "error decrypting recovery info", errors.WithWrap(err)) + } + + return marshaledInfo, nil +} diff --git a/internal/servers/repository_workerauth_test.go b/internal/servers/repository_workerauth_test.go new file mode 100644 index 000000000..162eca4b9 --- /dev/null +++ b/internal/servers/repository_workerauth_test.go @@ -0,0 +1,424 @@ +package servers + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/fatih/structs" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/registration" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/curve25519" + "google.golang.org/protobuf/types/known/structpb" +) + +// Test RootCertificate storage +func TestStoreRootCertificates(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + // Ensures the global scope contains a valid root key + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase) + require.NoError(err) + require.NotNil(wrapper) + + rw := db.New(conn) + workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + // Fail to find root certificates prior to rotation/ creation + noCertAuth := &types.RootCertificates{Id: ca_id} + err = workerAuthRepo.Load(ctx, noCertAuth) + require.Error(err) + rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 0) + + // Rotate will generate and store next and current, as we have none + roots, err := rotation.RotateRootCertificates(ctx, workerAuthRepo) + require.NoError(err) + rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 2) + + // Load stored roots + certAuthority := &types.RootCertificates{Id: ca_id} + err = workerAuthRepo.Load(ctx, certAuthority) + require.NoError(err) + require.NotNil(certAuthority.GetNext()) + require.NotNil(certAuthority.GetCurrent()) + + // Read the next cert and validate the stored values are valid after encrypt and decrypt + assert.Equal(roots.Next.PrivateKeyPkcs8, certAuthority.Next.PrivateKeyPkcs8) + + // Read the current cert and validate the stored values are valid after encrypt and decrypt + assert.Equal(roots.Current.PrivateKeyPkcs8, certAuthority.Current.PrivateKeyPkcs8) + + // Fail to find root certificates under bogus id + bogusCertAuth := &types.RootCertificates{Id: "bogus"} + err = workerAuthRepo.Load(ctx, bogusCertAuth) + require.Error(err) +} + +func TestStoreCertAuthorityVersioning(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + // Ensures the global scope contains a valid root key + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase) + require.NoError(err) + require.NotNil(wrapper) + + rw := db.New(conn) + workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + // Store CA and check that initial version updates + roots, err := rotation.RotateRootCertificates(ctx, workerAuthRepo) + require.NoError(err) + + cAuth2 := &types.RootCertificates{Id: ca_id} + err = workerAuthRepo.Load(ctx, cAuth2) + require.NoError(err) + var result2 rootCertificatesVersion + err = mapstructure.Decode(cAuth2.State.AsMap(), &result2) + require.NoError(err) + assert.Equal(uint32(2), result2.Version) + + // Store CA with old version, expect error + badVersion := &rootCertificatesVersion{Version: uint32(1)} + s := structs.New(badVersion) + s.TagName = "mapstructure" + badState, err := structpb.NewStruct(s.Map()) + require.NoError(err) + newRoots2 := &types.RootCertificates{ + Id: ca_id, + Next: roots.Next, + Current: roots.Current, + } + newRoots2.State = badState + err = workerAuthRepo.Store(ctx, newRoots2) + require.Error(err) + + // Remove CA, expect updated version + cAuthRemove := &types.RootCertificates{Id: ca_id} + removeVersion := &rootCertificatesVersion{Version: uint32(2)} + s = structs.New(removeVersion) + s.TagName = "mapstructure" + removeState, err := structpb.NewStruct(s.Map()) + require.NoError(err) + cAuthRemove.State = removeState + err = workerAuthRepo.Remove(ctx, cAuthRemove) + require.NoError(err) +} + +// Test WorkerAuth storage +func TestStoreWorkerAuth(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + + // Ensures the global scope contains a valid root key + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase) + require.NoError(err) + require.NotNil(wrapper) + + worker := TestPkiWorker(t, conn, wrapper) + + rw := db.New(conn) + rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + _, err = rotation.RotateRootCertificates(ctx, rootStorage) + require.NoError(err) + + // Create struct to pass in with workerId that will be passed along to storage + state, err := AttachWorkerIdToState(ctx, worker.PublicId) + require.NoError(err) + + // This happens on the worker + fileStorage, err := file.New(ctx) + require.NoError(err) + nodeCreds, err := types.NewNodeCredentials(ctx, fileStorage) + require.NoError(err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(ctx) + require.NoError(err) + keyId, err := nodeenrollment.KeyIdFromPkix(nodeCreds.CertificatePublicKeyPkix) + require.NoError(err) + nodePubKey, err := curve25519.X25519(nodeCreds.EncryptionPrivateKeyBytes, curve25519.Basepoint) + require.NoError(err) + + // Add in node information to storage so we have a key to use + nodeInfo := &types.NodeInformation{ + Id: keyId, + CertificatePublicKeyPkix: nodeCreds.CertificatePublicKeyPkix, + CertificatePublicKeyType: nodeCreds.CertificatePrivateKeyType, + EncryptionPublicKeyBytes: nodePubKey, + EncryptionPublicKeyType: nodeCreds.EncryptionPrivateKeyType, + RegistrationNonce: nodeCreds.RegistrationNonce, + State: state, + } + + // Create storage for authentication and pass it the worker + storage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + // The AuthorizeNode request will result in a WorkerAuth record being stored + _, err = registration.AuthorizeNode(ctx, storage, fetchReq, nodeenrollment.WithState(state)) + require.NoError(err) + + // We should now look for a node information value in storage and validate that it's populated + nodeInfos, err := storage.List(ctx, (*types.NodeInformation)(nil)) + require.NoError(err) + require.NotEmpty(nodeInfos) + assert.Len(nodeInfos, 1) + + // Validate the stored fields match those from the worker + nodeLookup := &types.NodeInformation{ + Id: keyId, + } + err = storage.Load(ctx, nodeLookup) + require.NoError(err) + assert.NotEmpty(nodeLookup) + assert.Equal(nodeInfo.EncryptionPublicKeyBytes, nodeLookup.EncryptionPublicKeyBytes) + assert.Equal(nodeInfo.RegistrationNonce, nodeLookup.RegistrationNonce) + assert.Equal(nodeInfo.CertificatePublicKeyPkix, nodeLookup.CertificatePublicKeyPkix) + assert.Equal(nodeInfo.State.AsMap(), nodeLookup.State.AsMap()) + + // Remove node + err = storage.Remove(ctx, nodeLookup) + require.NoError(err) + err = storage.Load(ctx, nodeLookup) + require.Error(err) +} + +func TestUnsupportedMessages(t *testing.T) { + require := require.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + + rw := db.New(conn) + storage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + err = storage.Store(ctx, &types.NodeCredentials{}) + require.Error(err) + + err = storage.Load(ctx, &types.NodeCredentials{Id: "bogus"}) + require.Error(err) + + _, err = storage.List(ctx, (*types.NodeCredentials)(nil)) + require.Error(err) + + err = storage.Remove(ctx, &types.NodeCredentials{Id: "bogus"}) + require.Error(err) +} + +func TestStoreNodeInformationTx(t *testing.T) { + t.Parallel() + testCtx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + testWrapper := db.TestWrapper(t) + testKeyId, err := testWrapper.KeyId(testCtx) + require.NoError(t, err) + + kmsCache := kms.TestKms(t, conn, testWrapper) + // Ensures the global scope contains a valid root key + err = kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(t, err) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase) + require.NoError(t, err) + + testRootStorage, err := NewRepositoryStorage(testCtx, rw, rw, kmsCache) + require.NoError(t, err) + + _, err = rotation.RotateRootCertificates(testCtx, testRootStorage) + require.NoError(t, err) + + // Create struct to pass in with workerId that will be passed along to + // storage + testWorker := TestPkiWorker(t, conn, testWrapper) + + testState, err := AttachWorkerIdToState(testCtx, testWorker.PublicId) + require.NoError(t, err) + + testNodeInfoFn := func() *types.NodeInformation { + // This happens on the worker + fileStorage, err := file.New(testCtx) + require.NoError(t, err) + nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage) + require.NoError(t, err) + + nodePubKey, err := curve25519.X25519(nodeCreds.EncryptionPrivateKeyBytes, curve25519.Basepoint) + require.NoError(t, err) + // Add in node information to storage so we have a key to use + nodeInfo := &types.NodeInformation{ + Id: testKeyId, + CertificatePublicKeyPkix: nodeCreds.CertificatePublicKeyPkix, + CertificatePublicKeyType: nodeCreds.CertificatePrivateKeyType, + EncryptionPublicKeyBytes: nodePubKey, + EncryptionPublicKeyType: nodeCreds.EncryptionPrivateKeyType, + ServerEncryptionPrivateKeyBytes: []byte("whatever"), + RegistrationNonce: nodeCreds.RegistrationNonce, + State: testState, + } + return nodeInfo + } + + tests := []struct { + name string + writer db.Writer + databaseWrapper wrapping.Wrapper + node *types.NodeInformation + wantErr bool + wantErrIs errors.Code + wantErrContains string + }{ + { + name: "missing-writer", + databaseWrapper: databaseWrapper, + node: testNodeInfoFn(), + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "missing writer", + }, + { + name: "missing-wrapper", + writer: rw, + node: testNodeInfoFn(), + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "missing database wrapper", + }, + { + name: "missing-node", + writer: rw, + databaseWrapper: databaseWrapper, + wantErr: true, + wantErrIs: errors.InvalidParameter, + wantErrContains: "missing NodeInformation", + }, + { + name: "key-id-error", + writer: rw, + databaseWrapper: &mockTestWrapper{err: errors.New(testCtx, errors.Internal, "testing", "key-id-error")}, + node: testNodeInfoFn(), + wantErr: true, + wantErrIs: errors.Internal, + wantErrContains: "key-id-error", + }, + { + name: "encrypt-error", + writer: rw, + databaseWrapper: &mockTestWrapper{ + encryptError: true, + err: errors.New(testCtx, errors.Encrypt, "testing", "encrypt-error"), + }, + node: testNodeInfoFn(), + wantErr: true, + wantErrIs: errors.Encrypt, + wantErrContains: "encrypt-error", + }, + { + name: "create-error-no-db", + writer: &db.Db{}, + databaseWrapper: databaseWrapper, + node: testNodeInfoFn(), + wantErr: true, + wantErrContains: "db.Create", + }, + { + name: "create-error-validation", + writer: rw, + databaseWrapper: databaseWrapper, + node: func() *types.NodeInformation { + ni := testNodeInfoFn() + ni.State = nil + return ni + }(), + wantErr: true, + wantErrContains: "missing WorkerId", + }, + { + name: "success", + writer: rw, + databaseWrapper: databaseWrapper, + node: testNodeInfoFn(), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + err := StoreNodeInformationTx(testCtx, tc.writer, tc.databaseWrapper, tc.node) + if tc.wantErr { + require.Error(err) + if tc.wantErrIs != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrIs), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + }) + } +} + +type mockTestWrapper struct { + wrapping.Wrapper + decryptError bool + encryptError bool + err error + keyId string +} + +func (m *mockTestWrapper) KeyId(context.Context) (string, error) { + if m.err != nil { + return "", m.err + } + return m.keyId, nil +} + +func (m *mockTestWrapper) Encrypt(ctx context.Context, plaintext []byte, options ...wrapping.Option) (*wrapping.BlobInfo, error) { + if m.err != nil && m.encryptError { + return nil, m.err + } + panic("todo") +} + +func (m *mockTestWrapper) Decrypt(ctx context.Context, ciphertext *wrapping.BlobInfo, options ...wrapping.Option) ([]byte, error) { + if m.err != nil && m.decryptError { + return nil, m.err + } + return []byte("decrypted"), nil +} diff --git a/internal/servers/root_certificate.go b/internal/servers/root_certificate.go new file mode 100644 index 000000000..f944c7e68 --- /dev/null +++ b/internal/servers/root_certificate.go @@ -0,0 +1,160 @@ +package servers + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/errors" + + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/servers/store" + "google.golang.org/protobuf/proto" +) + +// The CertificateAuthority id will always be set to "roots". +// The const ca_id contains this value +const ca_id = "roots" + +// CertificateAuthority is a versioned entity used to lock the database when rotation RootCertificates +type CertificateAuthority struct { + *store.CertificateAuthority + tableName string `gorm:"-"` +} + +func newCertificateAuthority() *CertificateAuthority { + ca := &CertificateAuthority{ + CertificateAuthority: &store.CertificateAuthority{ + PrivateId: ca_id, + }, + } + return ca +} + +// TableName returns the table name. +func (r *CertificateAuthority) TableName() string { + if r.tableName != "" { + return r.tableName + } + return "worker_auth_ca" +} + +// SetTableName sets the table name. +func (r *CertificateAuthority) SetTableName(n string) { + r.tableName = n +} + +// RootCertificate contains fields related to a RootCertificate resource +// This includes public/ private keys, the PEM encoded certificate, and the certificate validity period +type RootCertificate struct { + *store.RootCertificate + tableName string `gorm:"-"` +} + +// RootCertificateKeys contains the public and private keys for use in constructing a RootCertificate +type RootCertificateKeys struct { + publicKey []byte + privateKey []byte +} + +func newRootCertificate(ctx context.Context, serialNumber uint64, certificate []byte, notValidBefore, notValidAfter *timestamp.Timestamp, + rootCertificateKeys RootCertificateKeys, keyId string, state CertificateState, +) (*RootCertificate, error) { + const op = "servers.newRootCertificate" + + if &serialNumber == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no serialNumber") + } + if certificate == nil || len(certificate) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty certificate") + } + if notValidAfter == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no notValidAfter") + } + if notValidBefore == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no notValidBefore") + } + if rootCertificateKeys.publicKey == nil || len(rootCertificateKeys.publicKey) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty publicKey") + } + if rootCertificateKeys.privateKey == nil || len(rootCertificateKeys.privateKey) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty privateKey") + } + if keyId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no keyId") + } + if !validState(state) { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is not a valid certificate state", state)) + } + + l := &RootCertificate{ + RootCertificate: &store.RootCertificate{ + SerialNumber: serialNumber, + Certificate: certificate, + NotValidAfter: notValidAfter, + NotValidBefore: notValidBefore, + PublicKey: rootCertificateKeys.publicKey, + PrivateKey: rootCertificateKeys.privateKey, + KeyId: keyId, + State: string(state), + IssuingCa: ca_id, + }, + } + return l, nil +} + +func allocRootCertificate() *RootCertificate { + return &RootCertificate{ + RootCertificate: &store.RootCertificate{}, + } +} + +func (r *RootCertificate) clone() *RootCertificate { + cp := proto.Clone(r.RootCertificate) + return &RootCertificate{ + RootCertificate: cp.(*store.RootCertificate), + } +} + +// Validate the RootCertificate. On success, return nil +func (r *RootCertificate) ValidateNewRootCertificate(ctx context.Context) error { + const op = "servers.(RootCertificate).ValidateNewRootCertificate" + if &r.SerialNumber == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing SerialNumber") + } + if r.Certificate == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing Certificate") + } + if r.NotValidBefore.GetTimestamp().AsTime().IsZero() { + return errors.New(ctx, errors.InvalidParameter, op, "missing not valid before timestamp") + } + if r.NotValidAfter.GetTimestamp().AsTime().IsZero() { + return errors.New(ctx, errors.InvalidParameter, op, "missing not valid after timestamp") + } + if r.PublicKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing public key") + } + if r.PrivateKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing private key") + } + if r.KeyId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing key id") + } + if r.State == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing state") + } + + return nil +} + +// TableName returns the table name. +func (r *RootCertificate) TableName() string { + if r.tableName != "" { + return r.tableName + } + return "worker_auth_ca_certificate" +} + +// SetTableName sets the table name. +func (r *RootCertificate) SetTableName(n string) { + r.tableName = n +} diff --git a/internal/servers/servers.pb.go b/internal/servers/servers.pb.go index 22d492e8a..edf1bd10d 100644 --- a/internal/servers/servers.pb.go +++ b/internal/servers/servers.pb.go @@ -7,7 +7,6 @@ package servers import ( - timestamp "github.com/hashicorp/boundary/internal/db/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -21,39 +20,18 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Server contains all fields related to a Controller or Worker resource -type Server struct { +// TagPair matches a key to a value. +type TagPair struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Private ID of the resource - PrivateId string `protobuf:"bytes,10,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty"` - // Type of the resource (controller, worker) - Type string `protobuf:"bytes,20,opt,name=type,proto3" json:"type,omitempty"` - // Name of the resource. This is deprecated because we ended up setting the - // private ID and name to be identical, and there is currently no reason we - // can think of to allow a given server's name to match another within the - // same cluster. So we simply use the private ID and deprecate this. - // @inject_tag: `gorm:"-"` - // - // Deprecated: Do not use. - Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"-"` - // Description of the resource - Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty"` - // Address for the server - Address string `protobuf:"bytes,50,opt,name=address,proto3" json:"address,omitempty"` - // First seen time from the RDBMS - CreateTime *timestamp.Timestamp `protobuf:"bytes,60,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - // Last time there was an update - UpdateTime *timestamp.Timestamp `protobuf:"bytes,70,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` - // Tags for workers - // @inject_tag: `gorm:"-"` - Tags map[string]*TagValues `protobuf:"bytes,80,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3" gorm:"-"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty" class:"public"` // @gotags: `class:"public"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty" class:"public"` // @gotags: `class:"public"` } -func (x *Server) Reset() { - *x = Server{} +func (x *TagPair) Reset() { + *x = TagPair{} if protoimpl.UnsafeEnabled { mi := &file_controller_servers_v1_servers_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -61,13 +39,13 @@ func (x *Server) Reset() { } } -func (x *Server) String() string { +func (x *TagPair) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Server) ProtoMessage() {} +func (*TagPair) ProtoMessage() {} -func (x *Server) ProtoReflect() protoreflect.Message { +func (x *TagPair) ProtoReflect() protoreflect.Message { mi := &file_controller_servers_v1_servers_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -79,80 +57,47 @@ func (x *Server) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Server.ProtoReflect.Descriptor instead. -func (*Server) Descriptor() ([]byte, []int) { +// Deprecated: Use TagPair.ProtoReflect.Descriptor instead. +func (*TagPair) Descriptor() ([]byte, []int) { return file_controller_servers_v1_servers_proto_rawDescGZIP(), []int{0} } -func (x *Server) GetPrivateId() string { +func (x *TagPair) GetKey() string { if x != nil { - return x.PrivateId + return x.Key } return "" } -func (x *Server) GetType() string { +func (x *TagPair) GetValue() string { if x != nil { - return x.Type + return x.Value } return "" } -// Deprecated: Do not use. -func (x *Server) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Server) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *Server) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - -func (x *Server) GetCreateTime() *timestamp.Timestamp { - if x != nil { - return x.CreateTime - } - return nil -} - -func (x *Server) GetUpdateTime() *timestamp.Timestamp { - if x != nil { - return x.UpdateTime - } - return nil -} - -func (x *Server) GetTags() map[string]*TagValues { - if x != nil { - return x.Tags - } - return nil -} - -// TagValues is used because map fields cannot be repeated but can be a -// message -type TagValues struct { +// ServerWorkerStatus is the new message used in place of Server to relay status request info. +type ServerWorkerStatus struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Values []string `protobuf:"bytes,10,rep,name=values,proto3" json:"values,omitempty"` + // Id of the worker. + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" class:"public"` // @gotags: `class:"public"` + // Name of the resource (optional) + Name string `protobuf:"bytes,20,opt,name=name,proto3" json:"name,omitempty" class:"public"` // @gotags: `class:"public"` + // Description of the resource (optional) + Description string `protobuf:"bytes,25,opt,name=description,proto3" json:"description,omitempty" class:"public"` // @gotags: `class:"public"` + // Address for the worker. This must be set. + Address string `protobuf:"bytes,30,opt,name=address,proto3" json:"address,omitempty" class:"public"` // @gotags: `class:"public"` + // Tags for workers + Tags []*TagPair `protobuf:"bytes,40,rep,name=tags,proto3" json:"tags,omitempty"` + // The key id for this worker, if applicable (optional) + KeyId string `protobuf:"bytes,50,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" class:"public"` // @gotags: `class:"public"` } -func (x *TagValues) Reset() { - *x = TagValues{} +func (x *ServerWorkerStatus) Reset() { + *x = ServerWorkerStatus{} if protoimpl.UnsafeEnabled { mi := &file_controller_servers_v1_servers_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -160,13 +105,13 @@ func (x *TagValues) Reset() { } } -func (x *TagValues) String() string { +func (x *ServerWorkerStatus) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TagValues) ProtoMessage() {} +func (*ServerWorkerStatus) ProtoMessage() {} -func (x *TagValues) ProtoReflect() protoreflect.Message { +func (x *ServerWorkerStatus) ProtoReflect() protoreflect.Message { mi := &file_controller_servers_v1_servers_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -178,63 +123,80 @@ func (x *TagValues) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TagValues.ProtoReflect.Descriptor instead. -func (*TagValues) Descriptor() ([]byte, []int) { +// Deprecated: Use ServerWorkerStatus.ProtoReflect.Descriptor instead. +func (*ServerWorkerStatus) Descriptor() ([]byte, []int) { return file_controller_servers_v1_servers_proto_rawDescGZIP(), []int{1} } -func (x *TagValues) GetValues() []string { +func (x *ServerWorkerStatus) GetPublicId() string { if x != nil { - return x.Values + return x.PublicId + } + return "" +} + +func (x *ServerWorkerStatus) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ServerWorkerStatus) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ServerWorkerStatus) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *ServerWorkerStatus) GetTags() []*TagPair { + if x != nil { + return x.Tags } return nil } +func (x *ServerWorkerStatus) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + var File_controller_servers_v1_servers_proto protoreflect.FileDescriptor var file_controller_servers_v1_servers_proto_rawDesc = []byte{ 0x0a, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc1, 0x03, - 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x74, 0x61, 0x67, - 0x73, 0x18, 0x50, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x1a, 0x59, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x36, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x23, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, - 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x31, 0x0a, 0x07, + 0x54, 0x61, 0x67, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0xcc, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x32, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x28, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67, 0x50, 0x61, 0x69, + 0x72, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x38, + 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, + 0x3b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -249,23 +211,18 @@ func file_controller_servers_v1_servers_proto_rawDescGZIP() []byte { return file_controller_servers_v1_servers_proto_rawDescData } -var file_controller_servers_v1_servers_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_servers_v1_servers_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_controller_servers_v1_servers_proto_goTypes = []interface{}{ - (*Server)(nil), // 0: controller.servers.v1.Server - (*TagValues)(nil), // 1: controller.servers.v1.TagValues - nil, // 2: controller.servers.v1.Server.TagsEntry - (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp + (*TagPair)(nil), // 0: controller.servers.v1.TagPair + (*ServerWorkerStatus)(nil), // 1: controller.servers.v1.ServerWorkerStatus } var file_controller_servers_v1_servers_proto_depIdxs = []int32{ - 3, // 0: controller.servers.v1.Server.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 1: controller.servers.v1.Server.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 2, // 2: controller.servers.v1.Server.tags:type_name -> controller.servers.v1.Server.TagsEntry - 1, // 3: controller.servers.v1.Server.TagsEntry.value:type_name -> controller.servers.v1.TagValues - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 0, // 0: controller.servers.v1.ServerWorkerStatus.tags:type_name -> controller.servers.v1.TagPair + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_controller_servers_v1_servers_proto_init() } @@ -275,7 +232,7 @@ func file_controller_servers_v1_servers_proto_init() { } if !protoimpl.UnsafeEnabled { file_controller_servers_v1_servers_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Server); i { + switch v := v.(*TagPair); i { case 0: return &v.state case 1: @@ -287,7 +244,7 @@ func file_controller_servers_v1_servers_proto_init() { } } file_controller_servers_v1_servers_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TagValues); i { + switch v := v.(*ServerWorkerStatus); i { case 0: return &v.state case 1: @@ -305,7 +262,7 @@ func file_controller_servers_v1_servers_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_servers_v1_servers_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/servers/service_rotate_roots.go b/internal/servers/service_rotate_roots.go new file mode 100644 index 000000000..119962b10 --- /dev/null +++ b/internal/servers/service_rotate_roots.go @@ -0,0 +1,27 @@ +package servers + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/rotation" +) + +// RotateRoots is a domain service function that initiates a rotation of root certificates +// via a call to the nodenenrollment RotateRootCertificates function +// Accepts the nodeenrollment option, WithCertificateLifetime(time.Duration) to specify the lifetime +// of the generated cert(s) +func RotateRoots(ctx context.Context, workerAuthRepo *WorkerAuthRepositoryStorage, opt ...nodeenrollment.Option) error { + const op = "servers.RotateRoots" + if workerAuthRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing workerAuthRepo") + } + + _, err := rotation.RotateRootCertificates(ctx, workerAuthRepo, opt...) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} diff --git a/internal/servers/service_rotate_roots_test.go b/internal/servers/service_rotate_roots_test.go new file mode 100644 index 000000000..7df8e488b --- /dev/null +++ b/internal/servers/service_rotate_roots_test.go @@ -0,0 +1,88 @@ +package servers + +import ( + "context" + "crypto/rand" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRotateRoots(t *testing.T) { + require, assert := require.New(t), assert.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + kmsCache := kms.TestKms(t, conn, wrapper) + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + + rw := db.New(conn) + workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(err) + + // Check that we have no roots first + rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 0) + + // Generate roots + err = RotateRoots(ctx, workerAuthRepo, nodeenrollment.WithCertificateLifetime(time.Second*5)) + require.NoError(err) + + // Check that we have roots now + rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil)) + require.NoError(err) + assert.Len(rootIds, 2) + certAuthority := &types.RootCertificates{Id: ca_id} + err = workerAuthRepo.Load(ctx, certAuthority) + require.NoError(err) + require.NotNil(certAuthority.GetNext()) + require.NotNil(certAuthority.GetCurrent()) + + initialNext := certAuthority.GetNext() + initialCurrent := certAuthority.GetCurrent() + + // Rotate roots and assert that they've rotated + err = RotateRoots(ctx, workerAuthRepo, nodeenrollment.WithCertificateLifetime(time.Second*5)) + require.NoError(err) + + certAuthority2 := &types.RootCertificates{Id: ca_id} + err = workerAuthRepo.Load(ctx, certAuthority2) + require.NoError(err) + require.NotNil(certAuthority2.GetNext()) + require.NotNil(certAuthority2.GetCurrent()) + rotatedNext := certAuthority2.GetNext() + rotatedCurrent := certAuthority2.GetCurrent() + + // Next and current should have changed + assert.NotEqual(initialNext, rotatedNext) + assert.NotEqual(initialCurrent, rotatedCurrent) + + // And the old next root should now be current + assert.Equal(initialNext, rotatedCurrent) +} + +func TestRotateRootsFailure(t *testing.T) { + require := require.New(t) + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + + kmsCache := kms.TestKms(t, conn, wrapper) + err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + require.NoError(err) + + workerAuthRepo, err := NewRepositoryStorage(ctx, &db.Db{}, &db.Db{}, kmsCache) + require.NoError(err) + + err = RotateRoots(ctx, workerAuthRepo) + require.Error(err) +} diff --git a/internal/servers/state.go b/internal/servers/state.go new file mode 100644 index 000000000..2cbf6c295 --- /dev/null +++ b/internal/servers/state.go @@ -0,0 +1,20 @@ +package servers + +// CertificateState defines the possible states for a workerauth certificate +type CertificateState string + +const ( + UnknownState CertificateState = "unknown" + CurrentState CertificateState = "current" + NextState CertificateState = "next" +) + +func validState(s CertificateState) bool { + st := CertificateState(s) + switch st { + case CurrentState, NextState: + return true + default: + return false + } +} diff --git a/internal/servers/store/controller.pb.go b/internal/servers/store/controller.pb.go new file mode 100644 index 000000000..c0d684a89 --- /dev/null +++ b/internal/servers/store/controller.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/storage/servers/store/v1/controller.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Controller contains all fields related to a Controller resource +type Controller struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Private ID of the resource + PrivateId string `protobuf:"bytes,10,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty"` + // Address for the controller + Address string `protobuf:"bytes,20,opt,name=address,proto3" json:"address,omitempty"` + // Description of the resource (optional) + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,30,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // First seen time from the RDBMS + CreateTime *timestamp.Timestamp `protobuf:"bytes,40,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Last time there was an update + UpdateTime *timestamp.Timestamp `protobuf:"bytes,50,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` +} + +func (x *Controller) Reset() { + *x = Controller{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_controller_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Controller) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Controller) ProtoMessage() {} + +func (x *Controller) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_controller_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Controller.ProtoReflect.Descriptor instead. +func (*Controller) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_controller_proto_rawDescGZIP(), []int{0} +} + +func (x *Controller) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *Controller) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Controller) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Controller) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Controller) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +var File_controller_storage_servers_store_v1_controller_proto protoreflect.FileDescriptor + +var file_controller_storage_servers_store_v1_controller_proto_rawDesc = []byte{ + 0x0a, 0x34, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x81, 0x02, 0x0a, + 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_servers_store_v1_controller_proto_rawDescOnce sync.Once + file_controller_storage_servers_store_v1_controller_proto_rawDescData = file_controller_storage_servers_store_v1_controller_proto_rawDesc +) + +func file_controller_storage_servers_store_v1_controller_proto_rawDescGZIP() []byte { + file_controller_storage_servers_store_v1_controller_proto_rawDescOnce.Do(func() { + file_controller_storage_servers_store_v1_controller_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_servers_store_v1_controller_proto_rawDescData) + }) + return file_controller_storage_servers_store_v1_controller_proto_rawDescData +} + +var file_controller_storage_servers_store_v1_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_servers_store_v1_controller_proto_goTypes = []interface{}{ + (*Controller)(nil), // 0: controller.storage.servers.store.v1.Controller + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_servers_store_v1_controller_proto_depIdxs = []int32{ + 1, // 0: controller.storage.servers.store.v1.Controller.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.servers.store.v1.Controller.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_servers_store_v1_controller_proto_init() } +func file_controller_storage_servers_store_v1_controller_proto_init() { + if File_controller_storage_servers_store_v1_controller_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_servers_store_v1_controller_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Controller); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_servers_store_v1_controller_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_servers_store_v1_controller_proto_goTypes, + DependencyIndexes: file_controller_storage_servers_store_v1_controller_proto_depIdxs, + MessageInfos: file_controller_storage_servers_store_v1_controller_proto_msgTypes, + }.Build() + File_controller_storage_servers_store_v1_controller_proto = out.File + file_controller_storage_servers_store_v1_controller_proto_rawDesc = nil + file_controller_storage_servers_store_v1_controller_proto_goTypes = nil + file_controller_storage_servers_store_v1_controller_proto_depIdxs = nil +} diff --git a/internal/servers/store/gorm.go b/internal/servers/store/gorm.go new file mode 100644 index 000000000..2da1ceae9 --- /dev/null +++ b/internal/servers/store/gorm.go @@ -0,0 +1,11 @@ +package store + +// TableName overrides the table name used by Controller to `server_controller` +func (Controller) TableName() string { + return "server_controller" +} + +// TableName overrides the table name used by WorkerTag to `worker_tag` +func (WorkerTag) TableName() string { + return "server_worker_tag" +} diff --git a/internal/servers/store/root_certificate.pb.go b/internal/servers/store/root_certificate.pb.go new file mode 100644 index 000000000..f3a32e013 --- /dev/null +++ b/internal/servers/store/root_certificate.pb.go @@ -0,0 +1,340 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/storage/servers/store/v1/root_certificate.proto + +// Package store provides protobufs for storing types in the pki package. + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// CertificateAuthority is a versioned entity used to lock the database when rotation RootCertificates +type CertificateAuthority struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PrivateId string `protobuf:"bytes,10,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,20,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` +} + +func (x *CertificateAuthority) Reset() { + *x = CertificateAuthority{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificateAuthority) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateAuthority) ProtoMessage() {} + +func (x *CertificateAuthority) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateAuthority.ProtoReflect.Descriptor instead. +func (*CertificateAuthority) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_root_certificate_proto_rawDescGZIP(), []int{0} +} + +func (x *CertificateAuthority) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *CertificateAuthority) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +// RootCertificate contains all fields related to a RootCertificate resource +type RootCertificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The serial number of the root certificate + // @inject_tag: `gorm:"not_null"` + SerialNumber uint64 `protobuf:"varint,10,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty" gorm:"not_null"` + // Certificate is the PEM encoded certificate. + // @inject_tag: `gorm:"not_null"` + Certificate []byte `protobuf:"bytes,20,opt,name=certificate,proto3" json:"certificate,omitempty" gorm:"not_null"` + // Not valid before is the timestamp at which this certificate's validity period starts + NotValidBefore *timestamp.Timestamp `protobuf:"bytes,30,opt,name=not_valid_before,json=notValidBefore,proto3" json:"not_valid_before,omitempty"` + // Not valid after is the timestamp at which this certificate's validity period ends + NotValidAfter *timestamp.Timestamp `protobuf:"bytes,40,opt,name=not_valid_after,json=notValidAfter,proto3" json:"not_valid_after,omitempty"` + // The public key associated with this certificate + // @inject_tag: `gorm:"not_null"` + PublicKey []byte `protobuf:"bytes,50,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" gorm:"not_null"` + // The private key associated with this certificate + // This is a ciphertext field + // @inject_tag: `gorm:"not_null"` + PrivateKey []byte `protobuf:"bytes,60,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" gorm:"not_null"` + // The id of the kms database key used for encrypting this entry. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,70,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // State is an enum value indicating if this is the next or current root cert + // @inject_tag: `gorm:"not_null"` + State string `protobuf:"bytes,80,opt,name=state,proto3" json:"state,omitempty" gorm:"not_null"` + // A reference to the CertificateAuthority + // @inject_tag: `gorm:"not_null"` + IssuingCa string `protobuf:"bytes,90,opt,name=issuing_ca,json=issuingCa,proto3" json:"issuing_ca,omitempty" gorm:"not_null"` +} + +func (x *RootCertificate) Reset() { + *x = RootCertificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RootCertificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RootCertificate) ProtoMessage() {} + +func (x *RootCertificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RootCertificate.ProtoReflect.Descriptor instead. +func (*RootCertificate) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_root_certificate_proto_rawDescGZIP(), []int{1} +} + +func (x *RootCertificate) GetSerialNumber() uint64 { + if x != nil { + return x.SerialNumber + } + return 0 +} + +func (x *RootCertificate) GetCertificate() []byte { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *RootCertificate) GetNotValidBefore() *timestamp.Timestamp { + if x != nil { + return x.NotValidBefore + } + return nil +} + +func (x *RootCertificate) GetNotValidAfter() *timestamp.Timestamp { + if x != nil { + return x.NotValidAfter + } + return nil +} + +func (x *RootCertificate) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *RootCertificate) GetPrivateKey() []byte { + if x != nil { + return x.PrivateKey + } + return nil +} + +func (x *RootCertificate) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *RootCertificate) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *RootCertificate) GetIssuingCa() string { + if x != nil { + return x.IssuingCa + } + return "" +} + +var File_controller_storage_servers_store_v1_root_certificate_proto protoreflect.FileDescriptor + +var file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc = []byte{ + 0x0a, 0x3a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, + 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, + 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x4f, 0x0a, 0x14, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x03, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x74, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x54, + 0x0a, 0x10, 0x6e, 0x6f, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x62, 0x65, 0x66, 0x6f, + 0x72, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, + 0x66, 0x6f, 0x72, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, + 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, + 0x5f, 0x63, 0x61, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x73, 0x73, 0x75, 0x69, + 0x6e, 0x67, 0x43, 0x61, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_servers_store_v1_root_certificate_proto_rawDescOnce sync.Once + file_controller_storage_servers_store_v1_root_certificate_proto_rawDescData = file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc +) + +func file_controller_storage_servers_store_v1_root_certificate_proto_rawDescGZIP() []byte { + file_controller_storage_servers_store_v1_root_certificate_proto_rawDescOnce.Do(func() { + file_controller_storage_servers_store_v1_root_certificate_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_servers_store_v1_root_certificate_proto_rawDescData) + }) + return file_controller_storage_servers_store_v1_root_certificate_proto_rawDescData +} + +var file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_servers_store_v1_root_certificate_proto_goTypes = []interface{}{ + (*CertificateAuthority)(nil), // 0: controller.storage.servers.store.v1.CertificateAuthority + (*RootCertificate)(nil), // 1: controller.storage.servers.store.v1.RootCertificate + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_servers_store_v1_root_certificate_proto_depIdxs = []int32{ + 2, // 0: controller.storage.servers.store.v1.RootCertificate.not_valid_before:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.servers.store.v1.RootCertificate.not_valid_after:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_servers_store_v1_root_certificate_proto_init() } +func file_controller_storage_servers_store_v1_root_certificate_proto_init() { + if File_controller_storage_servers_store_v1_root_certificate_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificateAuthority); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RootCertificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_servers_store_v1_root_certificate_proto_goTypes, + DependencyIndexes: file_controller_storage_servers_store_v1_root_certificate_proto_depIdxs, + MessageInfos: file_controller_storage_servers_store_v1_root_certificate_proto_msgTypes, + }.Build() + File_controller_storage_servers_store_v1_root_certificate_proto = out.File + file_controller_storage_servers_store_v1_root_certificate_proto_rawDesc = nil + file_controller_storage_servers_store_v1_root_certificate_proto_goTypes = nil + file_controller_storage_servers_store_v1_root_certificate_proto_depIdxs = nil +} diff --git a/internal/servers/store/worker.pb.go b/internal/servers/store/worker.pb.go new file mode 100644 index 000000000..9ab200250 --- /dev/null +++ b/internal/servers/store/worker.pb.go @@ -0,0 +1,382 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/storage/servers/store/v1/worker.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Worker contains all fields related to a Worker resource +type Worker struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // Name of the resource (optional) + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,20,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // Description of the resource (optional) + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,30,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // Address for the worker. This is optional. + // @inject_tag: `gorm:"default:null"` + Address string `protobuf:"bytes,40,opt,name=address,proto3" json:"address,omitempty" gorm:"default:null"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,50,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,60,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,70,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,80,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // The last_status_time is the last time a worker daemon sent a status update. + // @inject_tag: `gorm:"default:null"` + LastStatusTime *timestamp.Timestamp `protobuf:"bytes,110,opt,name=last_status_time,json=lastStatusTime,proto3" json:"last_status_time,omitempty" gorm:"default:null"` + // The type of the worker, denoted by how it authenticates: pki or kms. + // @inject_tag: `gorm:"not_null"` + Type string `protobuf:"bytes,130,opt,name=type,proto3" json:"type,omitempty" gorm:"not_null"` +} + +func (x *Worker) Reset() { + *x = Worker{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_worker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Worker) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Worker) ProtoMessage() {} + +func (x *Worker) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_worker_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Worker.ProtoReflect.Descriptor instead. +func (*Worker) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_worker_proto_rawDescGZIP(), []int{0} +} + +func (x *Worker) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Worker) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Worker) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Worker) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Worker) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Worker) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Worker) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Worker) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Worker) GetLastStatusTime() *timestamp.Timestamp { + if x != nil { + return x.LastStatusTime + } + return nil +} + +func (x *Worker) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +// WorkerTag is a tag for a worker. The primary key is comprised of the +// worker_id, key, value, and source. +type WorkerTag struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // worker_id is the public key that key of the worker this tag is for. + // @inject_tag: `gorm:"primary_key"` + WorkerId string `protobuf:"bytes,10,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty" gorm:"primary_key"` + // key is the key of the tag. This must be set. + // @inject_tag: `gorm:"primary_key"` + Key string `protobuf:"bytes,20,opt,name=key,proto3" json:"key,omitempty" gorm:"primary_key"` + // value is the value + // @inject_tag: `gorm:"primary_key"` + Value string `protobuf:"bytes,30,opt,name=value,proto3" json:"value,omitempty" gorm:"primary_key"` + // source is the source of the tag. Either 'configuration' or 'api'. + // @inject_tag: `gorm:"default:not_null"` + Source string `protobuf:"bytes,40,opt,name=source,proto3" json:"source,omitempty" gorm:"default:not_null"` +} + +func (x *WorkerTag) Reset() { + *x = WorkerTag{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_worker_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkerTag) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkerTag) ProtoMessage() {} + +func (x *WorkerTag) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_worker_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkerTag.ProtoReflect.Descriptor instead. +func (*WorkerTag) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_worker_proto_rawDescGZIP(), []int{1} +} + +func (x *WorkerTag) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *WorkerTag) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *WorkerTag) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *WorkerTag) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +var File_controller_storage_servers_store_v1_worker_proto protoreflect.FileDescriptor + +var file_controller_storage_servers_store_v1_worker_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe1, 0x03, 0x0a, 0x06, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x4b, + 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x32, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, + 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x13, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x82, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x68, 0x0a, 0x09, 0x57, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x54, 0x61, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_servers_store_v1_worker_proto_rawDescOnce sync.Once + file_controller_storage_servers_store_v1_worker_proto_rawDescData = file_controller_storage_servers_store_v1_worker_proto_rawDesc +) + +func file_controller_storage_servers_store_v1_worker_proto_rawDescGZIP() []byte { + file_controller_storage_servers_store_v1_worker_proto_rawDescOnce.Do(func() { + file_controller_storage_servers_store_v1_worker_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_servers_store_v1_worker_proto_rawDescData) + }) + return file_controller_storage_servers_store_v1_worker_proto_rawDescData +} + +var file_controller_storage_servers_store_v1_worker_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_servers_store_v1_worker_proto_goTypes = []interface{}{ + (*Worker)(nil), // 0: controller.storage.servers.store.v1.Worker + (*WorkerTag)(nil), // 1: controller.storage.servers.store.v1.WorkerTag + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_servers_store_v1_worker_proto_depIdxs = []int32{ + 2, // 0: controller.storage.servers.store.v1.Worker.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.servers.store.v1.Worker.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 2: controller.storage.servers.store.v1.Worker.last_status_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_controller_storage_servers_store_v1_worker_proto_init() } +func file_controller_storage_servers_store_v1_worker_proto_init() { + if File_controller_storage_servers_store_v1_worker_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_servers_store_v1_worker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Worker); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_servers_store_v1_worker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkerTag); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_servers_store_v1_worker_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_servers_store_v1_worker_proto_goTypes, + DependencyIndexes: file_controller_storage_servers_store_v1_worker_proto_depIdxs, + MessageInfos: file_controller_storage_servers_store_v1_worker_proto_msgTypes, + }.Build() + File_controller_storage_servers_store_v1_worker_proto = out.File + file_controller_storage_servers_store_v1_worker_proto_rawDesc = nil + file_controller_storage_servers_store_v1_worker_proto_goTypes = nil + file_controller_storage_servers_store_v1_worker_proto_depIdxs = nil +} diff --git a/internal/servers/store/worker_auth.pb.go b/internal/servers/store/worker_auth.pb.go new file mode 100644 index 000000000..88c48b18b --- /dev/null +++ b/internal/servers/store/worker_auth.pb.go @@ -0,0 +1,326 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/storage/servers/store/v1/worker_auth.proto + +// Package store provides protobufs for storing types in the pki package. + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// WorkerAuth contains all fields related to an authorized Worker resource +type WorkerAuth struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The key id for this worker auth record, generated from the signing pub key + // @inject_tag: `gorm:"primary_key"` + WorkerKeyIdentifier string `protobuf:"bytes,10,opt,name=worker_key_identifier,json=workerKeyIdentifier,proto3" json:"worker_key_identifier,omitempty" gorm:"primary_key"` + // The worker id this worker authentication record is for + // @inject_tag: `gorm:"not_null"` + WorkerId string `protobuf:"bytes,20,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty" gorm:"not_null"` + // The worker's public signing key + // @inject_tag: `gorm:"not_null"` + WorkerSigningPubKey []byte `protobuf:"bytes,30,opt,name=worker_signing_pub_key,json=workerSigningPubKey,proto3" json:"worker_signing_pub_key,omitempty" gorm:"not_null"` + // The worker's public encryption key + // @inject_tag: `gorm:"not_null"` + WorkerEncryptionPubKey []byte `protobuf:"bytes,40,opt,name=worker_encryption_pub_key,json=workerEncryptionPubKey,proto3" json:"worker_encryption_pub_key,omitempty" gorm:"not_null"` + // The private key associated with this certificate + // This is a ciphertext field + // @inject_tag: `gorm:"not_null"` + ControllerEncryptionPrivKey []byte `protobuf:"bytes,50,opt,name=controller_encryption_priv_key,json=controllerEncryptionPrivKey,proto3" json:"controller_encryption_priv_key,omitempty" gorm:"not_null"` + // The id of the kms database key used for encrypting this entry. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,60,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // Nonce used by a worker in authenticating + // @inject_tag: `gorm:"default:null"` + Nonce []byte `protobuf:"bytes,70,opt,name=nonce,proto3" json:"nonce,omitempty" gorm:"default:null"` +} + +func (x *WorkerAuth) Reset() { + *x = WorkerAuth{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkerAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkerAuth) ProtoMessage() {} + +func (x *WorkerAuth) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkerAuth.ProtoReflect.Descriptor instead. +func (*WorkerAuth) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *WorkerAuth) GetWorkerKeyIdentifier() string { + if x != nil { + return x.WorkerKeyIdentifier + } + return "" +} + +func (x *WorkerAuth) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *WorkerAuth) GetWorkerSigningPubKey() []byte { + if x != nil { + return x.WorkerSigningPubKey + } + return nil +} + +func (x *WorkerAuth) GetWorkerEncryptionPubKey() []byte { + if x != nil { + return x.WorkerEncryptionPubKey + } + return nil +} + +func (x *WorkerAuth) GetControllerEncryptionPrivKey() []byte { + if x != nil { + return x.ControllerEncryptionPrivKey + } + return nil +} + +func (x *WorkerAuth) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *WorkerAuth) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +// WorkerCertBundle contains all fields related to a WorkerCertBundle resource +type WorkerCertBundle struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The public key of the issuing root certificate + // @inject_tag: `gorm:"primary_key"` + RootCertificatePublicKey []byte `protobuf:"bytes,10,opt,name=root_certificate_public_key,json=rootCertificatePublicKey,proto3" json:"root_certificate_public_key,omitempty" gorm:"primary_key"` + // The WorkerAuth worker_key_identifier this cert bundle record is for + // @inject_tag: `gorm:"primary_key"` + WorkerKeyIdentifier string `protobuf:"bytes,20,opt,name=worker_key_identifier,json=workerKeyIdentifier,proto3" json:"worker_key_identifier,omitempty" gorm:"primary_key"` + // CertBundle is the marshaled protobuf certificate bundle for a WorkerAuth + // @inject_tag: `gorm:"not_null"` + CertBundle []byte `protobuf:"bytes,30,opt,name=cert_bundle,json=certBundle,proto3" json:"cert_bundle,omitempty" gorm:"not_null"` +} + +func (x *WorkerCertBundle) Reset() { + *x = WorkerCertBundle{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkerCertBundle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkerCertBundle) ProtoMessage() {} + +func (x *WorkerCertBundle) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkerCertBundle.ProtoReflect.Descriptor instead. +func (*WorkerCertBundle) Descriptor() ([]byte, []int) { + return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *WorkerCertBundle) GetRootCertificatePublicKey() []byte { + if x != nil { + return x.RootCertificatePublicKey + } + return nil +} + +func (x *WorkerCertBundle) GetWorkerKeyIdentifier() string { + if x != nil { + return x.WorkerKeyIdentifier + } + return "" +} + +func (x *WorkerCertBundle) GetCertBundle() []byte { + if x != nil { + return x.CertBundle + } + return nil +} + +var File_controller_storage_servers_store_v1_worker_auth_proto protoreflect.FileDescriptor + +var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{ + 0x0a, 0x35, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x61, 0x75, 0x74, + 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x22, 0xbf, 0x02, 0x0a, + 0x0a, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x12, 0x32, 0x0a, 0x15, 0x77, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, + 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x16, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x70, + 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x12, 0x39, 0x0a, 0x19, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x28, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x16, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x43, 0x0a, 0x1e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x69, 0x76, 0x4b, 0x65, + 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, + 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0xa6, + 0x01, 0x0a, 0x10, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x62, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, 0x72, + 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_servers_store_v1_worker_auth_proto_rawDescOnce sync.Once + file_controller_storage_servers_store_v1_worker_auth_proto_rawDescData = file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc +) + +func file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP() []byte { + file_controller_storage_servers_store_v1_worker_auth_proto_rawDescOnce.Do(func() { + file_controller_storage_servers_store_v1_worker_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_servers_store_v1_worker_auth_proto_rawDescData) + }) + return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescData +} + +var file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_servers_store_v1_worker_auth_proto_goTypes = []interface{}{ + (*WorkerAuth)(nil), // 0: controller.storage.servers.store.v1.WorkerAuth + (*WorkerCertBundle)(nil), // 1: controller.storage.servers.store.v1.WorkerCertBundle +} +var file_controller_storage_servers_store_v1_worker_auth_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_storage_servers_store_v1_worker_auth_proto_init() } +func file_controller_storage_servers_store_v1_worker_auth_proto_init() { + if File_controller_storage_servers_store_v1_worker_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkerAuth); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkerCertBundle); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_servers_store_v1_worker_auth_proto_goTypes, + DependencyIndexes: file_controller_storage_servers_store_v1_worker_auth_proto_depIdxs, + MessageInfos: file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes, + }.Build() + File_controller_storage_servers_store_v1_worker_auth_proto = out.File + file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = nil + file_controller_storage_servers_store_v1_worker_auth_proto_goTypes = nil + file_controller_storage_servers_store_v1_worker_auth_proto_depIdxs = nil +} diff --git a/internal/servers/testing.go b/internal/servers/testing.go new file mode 100644 index 000000000..8e5a6ee05 --- /dev/null +++ b/internal/servers/testing.go @@ -0,0 +1,213 @@ +package servers + +import ( + "context" + "crypto/rand" + mathRand "math/rand" + "strings" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/nodeenrollment" + "github.com/hashicorp/nodeenrollment/registration" + "github.com/hashicorp/nodeenrollment/rotation" + "github.com/hashicorp/nodeenrollment/storage/file" + "github.com/hashicorp/nodeenrollment/types" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const defaultLength = 20 + +// Generate random bytes for byte fields +func populateBytes(length int) []byte { + fieldBytes := make([]byte, length) + rand.Read(fieldBytes) + return fieldBytes +} + +func TestKmsKey(ctx context.Context, t *testing.T, conn *db.DB, wrapper wrapping.Wrapper) string { + t.Helper() + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsCache := kms.TestKms(t, conn, wrapper) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testKey, err := databaseWrapper.KeyId(ctx) + require.NoError(t, err) + + return testKey +} + +func TestRootCertificate(ctx context.Context, t *testing.T, conn *db.DB, kmsKey string) *RootCertificate { + t.Helper() + rw := db.New(conn) + + beforeTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(-1 * time.Hour))} + afterTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(1 * time.Hour))} + + rootCertKeys := RootCertificateKeys{ + publicKey: populateBytes(defaultLength), + privateKey: populateBytes(defaultLength), + } + + cert, err := newRootCertificate(ctx, mathRand.Uint64(), populateBytes(defaultLength), beforeTimestamp, afterTimestamp, + rootCertKeys, kmsKey, CurrentState) + err = rw.Create(ctx, cert) + require.NoError(t, err) + return cert +} + +func TestWorkerAuth(t *testing.T, conn *db.DB, worker *Worker, kmsKey string) *WorkerAuth { + t.Helper() + ctx := context.Background() + rw := db.New(conn) + wSignPubKey := populateBytes(defaultLength) + wEncPubKey := populateBytes(defaultLength) + workerKeys := WorkerKeys{workerSigningPubKey: wSignPubKey, workerEncryptionPubKey: wEncPubKey} + controllerKey := populateBytes(defaultLength) + nonce := populateBytes(defaultLength) + opt := []Option{ + WithKeyId(kmsKey), + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + } + + workerAuth, err := newWorkerAuth(ctx, "worker-key-identifier", worker.PublicId, opt...) + require.NoError(t, err) + err = rw.Create(ctx, workerAuth) + require.NoError(t, err) + + return workerAuth +} + +// TestKmsWorker inserts a worker into the db to satisfy foreign key constraints. +// The worker provided fields are auto generated. if WithName is not present a +// random name will be generated and assigned to the worker. +func TestKmsWorker(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper, opt ...Option) *Worker { + t.Helper() + rw := db.New(conn) + kms := kms.TestKms(t, conn, wrapper) + serversRepo, err := NewRepository(rw, rw, kms) + require.NoError(t, err) + ctx := context.Background() + opts := getOpts(opt...) + + namePart, err := newWorkerId(ctx) + require.NoError(t, err) + name := "test-worker-" + strings.ToLower(namePart) + if opts.withName != "" { + name = opts.withName + } + address := "127.0.0.1" + if opts.withAddress != "" { + address = opts.withAddress + } + wrk := NewWorker(scope.Global.String(), + WithName(name), + WithAddress(address)) + wrk, err = serversRepo.UpsertWorkerStatus(ctx, wrk) + require.NoError(t, err) + require.NotNil(t, wrk) + + if len(opts.withWorkerTags) > 0 { + var tags []interface{} + for _, t := range opts.withWorkerTags { + tags = append(tags, &store.WorkerTag{ + WorkerId: wrk.GetPublicId(), + Key: t.Key, + Value: t.Value, + Source: "configuration", + }) + } + require.NoError(t, rw.CreateItems(ctx, tags)) + } + var mask []string + if opts.withDescription != "" { + wrk.Description = opts.withDescription + mask = append(mask, "description") + } + if len(mask) > 0 { + var n int + wrk, n, err = serversRepo.UpdateWorker(ctx, wrk, wrk.Version, mask) + require.NoError(t, err) + require.Equal(t, 1, n) + require.NotNil(t, wrk) + } + wrk, err = serversRepo.LookupWorker(ctx, wrk.GetPublicId()) + require.NoError(t, err) + return wrk +} + +// TestPkiWorker inserts a worker into the db to satisfy foreign key constraints. +// The worker provided fields are auto generated. WithName and WithDescription, +// are applied to the resource name, description if present. WithTestPkiWorkerAuthorizedKeyId +// can be used to make the PkiWorker authorized in which case the string pointer +// passed to WithTestPkiWorkerAuthorizedKeyId is set to the key id. +func TestPkiWorker(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper, opt ...Option) *Worker { + t.Helper() + rw := db.New(conn) + kmsCache := kms.TestKms(t, conn, wrapper) + serversRepo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + ctx := context.Background() + opts := getOpts(opt...) + + require.NoError(t, err) + wrk := NewWorker(scope.Global.String(), + opt...) + wrk, err = serversRepo.CreateWorker(ctx, wrk, opt...) + require.NoError(t, err) + require.NotNil(t, wrk) + + if len(opts.withWorkerTags) > 0 { + var tags []interface{} + for _, t := range opts.withWorkerTags { + tags = append(tags, &store.WorkerTag{ + WorkerId: wrk.GetPublicId(), + Key: t.Key, + Value: t.Value, + Source: "configuration", + }) + } + require.NoError(t, rw.CreateItems(ctx, tags)) + } + if opts.withTestPkiWorkerAuthorized { + err = kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)) + // We always try to create root global scope keys, but if it already exists, just continue... + require.True(t, err == nil || errors.IsUniqueError(err), err) + rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache) + require.NoError(t, err) + _, err = rotation.RotateRootCertificates(ctx, rootStorage) + require.NoError(t, err) + // Create struct to pass in with workerId that will be passed along to storage + state, err := AttachWorkerIdToState(ctx, wrk.PublicId) + require.NoError(t, err) + + // This happens on the worker + fileStorage, err := file.New(ctx) + require.NoError(t, err) + nodeCreds, err := types.NewNodeCredentials(ctx, fileStorage) + require.NoError(t, err) + // Create request using worker id + fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(ctx) + require.NoError(t, err) + registeredNode, err := registration.AuthorizeNode(ctx, rootStorage, fetchReq, nodeenrollment.WithState(state)) + require.NoError(t, err) + + if opts.withTestPkiWorkerKeyId != nil { + *opts.withTestPkiWorkerKeyId = registeredNode.Id + } + } + wrk, err = serversRepo.LookupWorker(ctx, wrk.GetPublicId()) + require.NoError(t, err) + return wrk +} diff --git a/internal/servers/testing_test.go b/internal/servers/testing_test.go new file mode 100644 index 000000000..bcbcb7364 --- /dev/null +++ b/internal/servers/testing_test.go @@ -0,0 +1,64 @@ +package servers + +import ( + "context" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTestKmsWorker(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + const ( + name = "test name" + description = "test description" + address = "test address" + ) + tWorker := TestKmsWorker(t, conn, wrapper, WithName(name), WithDescription(description), WithAddress(address)) + assert.NotNil(t, tWorker) + assert.True(t, strings.HasPrefix(tWorker.GetPublicId(), WorkerPrefix)) + + lkpWorker := NewWorker(scope.Global.String()) + lkpWorker.PublicId = tWorker.GetPublicId() + rw := db.New(conn) + require.NoError(t, rw.LookupById(context.Background(), lkpWorker)) + assert.NotNil(t, lkpWorker) + assert.NotNil(t, lkpWorker.GetLastStatusTime()) + assert.Equal(t, KmsWorkerType.String(), lkpWorker.GetType()) + assert.Equal(t, name, lkpWorker.GetName()) + assert.Equal(t, description, lkpWorker.GetDescription()) + assert.Equal(t, address, lkpWorker.GetAddress()) +} + +func TestTestPkiWorker(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + const ( + name = "test name" + description = "test description" + ) + tWorker := TestPkiWorker(t, conn, wrapper, WithName(name), WithDescription(description)) + assert.NotNil(t, tWorker) + assert.True(t, strings.HasPrefix(tWorker.GetPublicId(), WorkerPrefix)) + + lkpWorker := NewWorker(scope.Global.String()) + lkpWorker.PublicId = tWorker.GetPublicId() + rw := db.New(conn) + require.NoError(t, rw.LookupById(context.Background(), lkpWorker)) + assert.NotNil(t, lkpWorker) + assert.Equal(t, PkiWorkerType.String(), lkpWorker.GetType()) + assert.Equal(t, name, lkpWorker.GetName()) + assert.Equal(t, description, lkpWorker.GetDescription()) + assert.Nil(t, lkpWorker.GetLastStatusTime()) + + var keyId string + authorizedWorker := TestPkiWorker(t, conn, wrapper, WithTestPkiWorkerAuthorizedKeyId(&keyId)) + assert.NotNil(t, authorizedWorker) + assert.True(t, strings.HasPrefix(authorizedWorker.GetPublicId(), WorkerPrefix)) + assert.NotEmpty(t, keyId) +} diff --git a/internal/servers/worker.go b/internal/servers/worker.go new file mode 100644 index 000000000..8f7986e19 --- /dev/null +++ b/internal/servers/worker.go @@ -0,0 +1,253 @@ +package servers + +import ( + "context" + "strings" + + "github.com/fatih/structs" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/servers/store" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +type WorkerType string + +const ( + UnknownWorkerType WorkerType = "unknown" + KmsWorkerType WorkerType = "kms" + PkiWorkerType WorkerType = "pki" +) + +func (t WorkerType) Valid() bool { + switch t { + case KmsWorkerType, PkiWorkerType: + return true + } + return false +} + +func (t WorkerType) String() string { + return string(t) +} + +type workerAuthWorkerId struct { + WorkerId string `mapstructure:"worker_id"` +} + +// AttachWorkerIdToState accepts a workerId and creates a struct for use with the Nodeenrollment lib +// This is intended for use in worker authorization; AuthorizeNode in the lib accepts the option WithState +// so that the workerId is passed through to storage and associated with a WorkerAuth record +func AttachWorkerIdToState(ctx context.Context, workerId string) (*structpb.Struct, error) { + const op = "servers.(Worker).AttachWorkerIdToState" + if workerId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing workerId") + } + + workerMap := &workerAuthWorkerId{WorkerId: workerId} + s := structs.New(workerMap) + s.TagName = "mapstructure" + return structpb.NewStruct(s.Map()) +} + +// A Worker is a server that provides an address which can be used to proxy +// session connections. It can be tagged with custom tags and is used when +// authorizing and establishing a session. It is owned by a scope. +type Worker struct { + *store.Worker + + activeConnectionCount uint32 `gorm:"-"` + apiTags []*Tag `gorm:"-"` + configTags []*Tag `gorm:"-"` + + // inputTags is not specified to be api or config tags and is not intended + // to be read by clients. Since config tags and api tags are applied in + // mutually exclusive contexts, inputTags is interpreted to be one or the + // other based on the context in which the worker is passed. As such + // inputTags should only be read when performing mutations on the database. + inputTags []*Tag `gorm:"-"` +} + +// NewWorker returns a new Worker. Valid options are WithName, WithDescription +// WithAddress, and WithWorkerTags. All other options are ignored. This does +// not set any of the worker reported values. +func NewWorker(scopeId string, opt ...Option) *Worker { + opts := getOpts(opt...) + return &Worker{ + Worker: &store.Worker{ + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + Address: opts.withAddress, + }, + inputTags: opts.withWorkerTags, + } +} + +// allocWorker will allocate a Worker +func allocWorker() Worker { + return Worker{Worker: &store.Worker{}} +} + +func (w *Worker) clone() *Worker { + if w == nil { + return nil + } + cw := proto.Clone(w.Worker) + cWorker := &Worker{ + Worker: cw.(*store.Worker), + } + if w.apiTags != nil { + cWorker.apiTags = make([]*Tag, 0, len(w.apiTags)) + for _, t := range w.apiTags { + cWorker.apiTags = append(cWorker.apiTags, &Tag{Key: t.Key, Value: t.Value}) + } + } + if w.configTags != nil { + cWorker.configTags = make([]*Tag, 0, len(w.configTags)) + for _, t := range w.configTags { + cWorker.configTags = append(cWorker.configTags, &Tag{Key: t.Key, Value: t.Value}) + } + } + if w.inputTags != nil { + cWorker.inputTags = make([]*Tag, 0, len(w.inputTags)) + for _, t := range w.inputTags { + cWorker.inputTags = append(cWorker.inputTags, &Tag{Key: t.Key, Value: t.Value}) + } + } + return cWorker +} + +// ActiveConnectionCount is the current number of sessions this worker is handling +// according to the controllers. +func (w *Worker) ActiveConnectionCount() uint32 { + return w.activeConnectionCount +} + +// CanonicalTags is the deduplicated set of tags contained on both the resource +// set over the API as well as the tags reported by the worker itself. +func (w *Worker) CanonicalTags() map[string][]string { + dedupedTags := make(map[Tag]struct{}) + for _, t := range w.apiTags { + dedupedTags[*t] = struct{}{} + } + for _, t := range w.configTags { + dedupedTags[*t] = struct{}{} + } + tags := make(map[string][]string) + for t := range dedupedTags { + tags[t.Key] = append(tags[t.Key], t.Value) + } + return tags +} + +// GetConfigTags returns the tags for this worker which has been set through +// the worker daemon's configuration file. +func (w *Worker) GetConfigTags() map[string][]string { + tags := make(map[string][]string) + for _, t := range w.configTags { + tags[t.Key] = append(tags[t.Key], t.Value) + } + return tags +} + +// GetLastStatusTime contains the last time the worker has reported to the +// controller its connection status. If the worker has never reported to a +// controller then nil is returned. +func (w *Worker) GetLastStatusTime() *timestamp.Timestamp { + if w == nil || w.Worker == nil || w.Worker.GetLastStatusTime().AsTime() == timestamp.NegativeInfinityTS { + return nil + } + return w.Worker.GetLastStatusTime() +} + +// TableName overrides the table name used by Worker to `server_worker` +func (Worker) TableName() string { + return "server_worker" +} + +// workerAggregate contains an aggregated view of the values associated with +// a single worker. +type workerAggregate struct { + PublicId string `gorm:"primary_key"` + ScopeId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Address string + Version uint32 + Type string + ApiTags string + ActiveConnectionCount uint32 + // Config Fields + LastStatusTime *timestamp.Timestamp + WorkerConfigTags string +} + +func (a *workerAggregate) toWorker(ctx context.Context) (*Worker, error) { + const op = "servers.(workerAggregate).toWorker" + worker := &Worker{ + Worker: &store.Worker{ + PublicId: a.PublicId, + Name: a.Name, + Description: a.Description, + Address: a.Address, + CreateTime: a.CreateTime, + UpdateTime: a.UpdateTime, + ScopeId: a.ScopeId, + Version: a.Version, + LastStatusTime: a.LastStatusTime, + Type: a.Type, + }, + activeConnectionCount: a.ActiveConnectionCount, + } + tags, err := tagsFromAggregatedTagString(ctx, a.ApiTags) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing config tag string")) + } + worker.apiTags = tags + + tags, err = tagsFromAggregatedTagString(ctx, a.WorkerConfigTags) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing config tag string")) + } + worker.configTags = tags + + return worker, nil +} + +// tagsForAggregatedTagString parses a deliminated string in the format returned +// by the database for the server_worker_aggregate view and returns []*Tag. +// The string is in the format of key1Yvalue1Zkey2Yvalue2Zkey3Yvalue3. Y and Z +// ares chosen for deliminators since tag keys and values are restricted from +// having capitalized letters in them. +func tagsFromAggregatedTagString(ctx context.Context, s string) ([]*Tag, error) { + if s == "" { + return nil, nil + } + const op = "servers.tagsFromAggregatedTagString" + const aggregateDelimiter = "Z" + const pairDelimiter = "Y" + var tags []*Tag + for _, kv := range strings.Split(s, aggregateDelimiter) { + res := strings.SplitN(kv, pairDelimiter, 3) + if len(res) != 2 { + return nil, errors.New(ctx, errors.Internal, op, "invalid aggregated tag pairs") + } + tags = append(tags, &Tag{ + Key: res[0], + Value: res[1], + }) + } + return tags, nil +} + +func (a *workerAggregate) GetPublicId() string { + return a.PublicId +} + +func (workerAggregate) TableName() string { + return "server_worker_aggregate" +} diff --git a/internal/servers/worker_auth.go b/internal/servers/worker_auth.go new file mode 100644 index 000000000..3c7c11b56 --- /dev/null +++ b/internal/servers/worker_auth.go @@ -0,0 +1,183 @@ +package servers + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/servers/store" + "google.golang.org/protobuf/proto" +) + +// WorkerAuth contains all fields related to an authorized Worker resource +// This includes worker public keys, the controller encryption key, +// and certificate bundles issued by the Boundary CA +type WorkerAuth struct { + *store.WorkerAuth + tableName string `gorm:"-"` +} + +// WorkerKeys contain the signing and encryption keys for a WorkerAuth resource +type WorkerKeys struct { + workerSigningPubKey []byte + workerEncryptionPubKey []byte +} + +func newWorkerAuth(ctx context.Context, workerKeyIdentifier, workerId string, opt ...Option) (*WorkerAuth, error) { + const op = "servers.newWorkerAuth" + opts := getOpts(opt...) + + if workerKeyIdentifier == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no workerKeyIdentifier") + } + if workerId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no workerId") + } + + l := &WorkerAuth{ + WorkerAuth: &store.WorkerAuth{ + WorkerKeyIdentifier: workerKeyIdentifier, + WorkerId: workerId, + }, + } + + if &opts.withWorkerKeys != nil { + l.WorkerSigningPubKey = opts.withWorkerKeys.workerSigningPubKey + l.WorkerEncryptionPubKey = opts.withWorkerKeys.workerEncryptionPubKey + } + if &opts.withControllerEncryptionPrivateKey != nil { + l.ControllerEncryptionPrivKey = opts.withControllerEncryptionPrivateKey + } + if opts.withKeyId != "" { + l.KeyId = opts.withKeyId + } + if opts.withNonce != nil { + l.Nonce = opts.withNonce + } + return l, nil +} + +func allocWorkerAuth() *WorkerAuth { + return &WorkerAuth{ + WorkerAuth: &store.WorkerAuth{}, + } +} + +func (w *WorkerAuth) clone() *WorkerAuth { + cp := proto.Clone(w.WorkerAuth) + return &WorkerAuth{ + WorkerAuth: cp.(*store.WorkerAuth), + } +} + +// Validate is called before storing a WorkerAuth in the db +func (w *WorkerAuth) ValidateNewWorkerAuth(ctx context.Context) error { + const op = "servers.(WorkerAuth).validateNewWorkerAuth" + if w.WorkerKeyIdentifier == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerKeyIdentifier") + } + if w.WorkerId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerId") + } + if w.WorkerSigningPubKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerSigningPubKey") + } + if w.WorkerEncryptionPubKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerEncryptionPubKey") + } + if w.ControllerEncryptionPrivKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing ControllerEncryptionPrivKey") + } + if w.KeyId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing KeyId") + } + if w.Nonce == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing Nonce") + } + + return nil +} + +// TableName returns the table name. +func (w *WorkerAuth) TableName() string { + if w.tableName != "" { + return w.tableName + } + return "worker_auth_authorized" +} + +// SetTableName sets the table name. +func (w *WorkerAuth) SetTableName(n string) { + w.tableName = n +} + +// WorkerCertBundle contains all fields related to a WorkerCertBundle resource +// This includes the serial number of the issuing CA, the worker id, and the certificate bundles issued by the CA +type WorkerCertBundle struct { + *store.WorkerCertBundle + tableName string `gorm:"-"` +} + +func newWorkerCertBundle(ctx context.Context, certificatePublicKey []byte, workerKeyIdentifier string, certBundle []byte) (*WorkerCertBundle, error) { + const op = "servers.newWorkerCertBundle" + + if certificatePublicKey == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no certificatePublicKey") + } + if workerKeyIdentifier == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no workerKeyIdentifier") + } + if certBundle == nil || len(certBundle) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty certBundle") + } + + l := &WorkerCertBundle{ + WorkerCertBundle: &store.WorkerCertBundle{ + RootCertificatePublicKey: certificatePublicKey, + WorkerKeyIdentifier: workerKeyIdentifier, + CertBundle: certBundle, + }, + } + return l, nil +} + +func allocWorkerCertBundle() *WorkerCertBundle { + return &WorkerCertBundle{ + WorkerCertBundle: &store.WorkerCertBundle{}, + } +} + +func (w *WorkerCertBundle) clone() *WorkerCertBundle { + cp := proto.Clone(w.WorkerCertBundle) + return &WorkerCertBundle{ + WorkerCertBundle: cp.(*store.WorkerCertBundle), + } +} + +// Validate is called before storing a WorkerCertBundle in the db +func (w *WorkerCertBundle) ValidateNewWorkerCertBundle(ctx context.Context) error { + const op = "servers.(WorkerAuth).validateNewWorkerCertBundle" + if w.RootCertificatePublicKey == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing CertificatePublicKey") + } + if w.WorkerKeyIdentifier == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing WorkerKeyIdentifier") + } + if w.CertBundle == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing certificate bundle") + } + + return nil +} + +// TableName returns the table name. +func (w *WorkerCertBundle) TableName() string { + if w.tableName != "" { + return w.tableName + } + return "worker_auth_certificate_bundle" +} + +// SetTableName sets the table name. +func (w *WorkerCertBundle) SetTableName(n string) { + w.tableName = n +} diff --git a/internal/servers/worker_tag.go b/internal/servers/worker_tag.go new file mode 100644 index 000000000..75afc0fe6 --- /dev/null +++ b/internal/servers/worker_tag.go @@ -0,0 +1,26 @@ +package servers + +// A Tag is a custom key/value pair which can be attached to a Worker. +// Multiple Tags may contain the same key and different values in which +// case both key/value pairs are valid. Tags can be sourced from either the +// worker's configuration or the api. key/value pairs can be the same from +// different sources. +type Tag struct { + Key string + Value string +} + +type TagSource string + +const ( + ConfigurationTagSource TagSource = "configuration" + ApiTagSource TagSource = "api" +) + +func (t TagSource) isValid() bool { + return t == ConfigurationTagSource || t == ApiTagSource +} + +func (t TagSource) String() string { + return string(t) +} diff --git a/internal/servers/worker_tags_test.go b/internal/servers/worker_tags_test.go new file mode 100644 index 000000000..db371cd43 --- /dev/null +++ b/internal/servers/worker_tags_test.go @@ -0,0 +1,108 @@ +package servers + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/stretchr/testify/assert" +) + +func TestWorkerTags_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + rw := db.New(conn) + + worker := TestKmsWorker(t, conn, wrapper) + + tests := []struct { + name string + want *store.WorkerTag + wantCreateErr bool + }{ + { + name: "success api source", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Key: "key", + Value: "value", + Source: ApiTagSource.String(), + }, + }, + { + name: "success config source", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Key: "key", + Value: "value", + Source: ConfigurationTagSource.String(), + }, + }, + { + name: "unknown source", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Key: "key", + Value: "value", + Source: "unknown", + }, + wantCreateErr: true, + }, + { + name: "no source", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Key: "key", + Value: "value", + }, + wantCreateErr: true, + }, + { + name: "bad worker id", + want: &store.WorkerTag{ + WorkerId: "w_badworkeridthatdoesntexist", + Key: "key", + Value: "value", + }, + wantCreateErr: true, + }, + { + name: "missing worker id", + want: &store.WorkerTag{ + Key: "key", + Value: "value", + }, + wantCreateErr: true, + }, + { + name: "missing key", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Value: "value", + }, + wantCreateErr: true, + }, + { + name: "missing value", + want: &store.WorkerTag{ + WorkerId: worker.GetPublicId(), + Key: "key", + }, + wantCreateErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + err := rw.Create(context.Background(), tt.want) + if tt.wantCreateErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/servers/worker_test.go b/internal/servers/worker_test.go new file mode 100644 index 000000000..7c80297f5 --- /dev/null +++ b/internal/servers/worker_test.go @@ -0,0 +1,785 @@ +package servers + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestWorkerCanonicalTags(t *testing.T) { + w := NewWorker(scope.Global.String()) + w.apiTags = []*Tag{ + {Key: "key", Value: "apis unique"}, + {Key: "key", Value: "shared"}, + {Key: "key2", Value: "apis key2 unique"}, + } + w.configTags = []*Tag{ + {Key: "key", Value: "configs unique"}, + {Key: "key", Value: "shared"}, + {Key: "key3", Value: "configs key3 unique"}, + } + + got := w.CanonicalTags() + assert.Len(t, got, 3, "2 keys expected, 'key' and 'key2'") + assert.ElementsMatch(t, got["key"], []string{"shared", "apis unique", "configs unique"}) + assert.ElementsMatch(t, got["key2"], []string{"apis key2 unique"}) + assert.ElementsMatch(t, got["key3"], []string{"configs key3 unique"}) +} + +func TestWorkerAggregate(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + ctx := context.Background() + + getAggWorker := func(id string) *Worker { + agg := &workerAggregate{PublicId: id} + require.NoError(t, rw.LookupById(ctx, agg)) + got, err := agg.toWorker(ctx) + assert.NoError(t, err) + return got + } + + t.Run("kms worker", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + id = strings.ToLower(id) + w := NewWorker(scope.Global.String(), + WithName(id), + WithAddress("address")) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + + got := getAggWorker(id) + assert.Equal(t, KmsWorkerType.String(), got.GetType()) + assert.Equal(t, id, got.GetPublicId()) + assert.Equal(t, scope.Global.String(), got.GetScopeId()) + assert.Equal(t, id, got.GetName()) + assert.Equal(t, "address", got.GetAddress()) + assert.Equal(t, uint32(1), got.GetVersion()) + assert.NotNil(t, got.GetLastStatusTime()) + assert.Empty(t, got.CanonicalTags()) + }) + + // Worker with status + t.Run("Worker with status", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + w := NewWorker(scope.Global.String(), + WithAddress("address"), + WithName(strings.ToLower(id))) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + + got := getAggWorker(id) + assert.Equal(t, id, got.GetPublicId()) + assert.Equal(t, uint32(1), got.GetVersion()) + assert.NotNil(t, got.GetLastStatusTime()) + assert.Equal(t, strings.ToLower(id), got.GetName()) + assert.Equal(t, "address", got.GetAddress()) + }) + + t.Run("Worker with a config tag", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + w := NewWorker(scope.Global.String(), + WithAddress("address"), + WithName(strings.ToLower(id))) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + require.NoError(t, rw.Create(ctx, + &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val", + Source: ConfigurationTagSource.String(), + })) + + got := getAggWorker(id) + assert.Equal(t, id, got.GetPublicId()) + assert.Equal(t, uint32(1), got.GetVersion()) + assert.NotNil(t, got.GetLastStatusTime()) + assert.Empty(t, got.apiTags) + assert.Equal(t, got.configTags, []*Tag{{Key: "key", Value: "val"}}) + }) + + t.Run("Worker with many config tag", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + w := NewWorker(scope.Global.String(), + WithAddress("address"), + WithName(strings.ToLower(id))) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val", + Source: ConfigurationTagSource.String(), + })) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val2", + Source: ConfigurationTagSource.String(), + })) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key2", + Value: "val2", + Source: ConfigurationTagSource.String(), + })) + + got := getAggWorker(id) + require.NotNil(t, got.GetLastStatusTime()) + assert.Empty(t, got.apiTags) + assert.ElementsMatch(t, got.configTags, []*Tag{ + {Key: "key", Value: "val"}, + {Key: "key", Value: "val2"}, + {Key: "key2", Value: "val2"}, + }) + }) + + t.Run("Worker with an api tag", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + w := NewWorker(scope.Global.String(), + WithAddress("address"), + WithName(strings.ToLower(id))) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + require.NoError(t, rw.Create(ctx, + &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val", + Source: ApiTagSource.String(), + })) + + got := getAggWorker(id) + assert.Equal(t, id, got.GetPublicId()) + assert.Equal(t, uint32(1), got.GetVersion()) + assert.NotNil(t, got.GetLastStatusTime()) + assert.Empty(t, got.GetConfigTags()) + assert.Equal(t, got.apiTags, []*Tag{{Key: "key", Value: "val"}}) + }) + + // Worker with mix of tag sources + t.Run("Worker with mix of tag sources", func(t *testing.T) { + id, err := newWorkerId(ctx) + require.NoError(t, err) + w := NewWorker(scope.Global.String(), + WithAddress("address"), + WithName(strings.ToLower(id))) + w.Type = KmsWorkerType.String() + w.PublicId = id + require.NoError(t, rw.Create(ctx, w)) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val", + Source: ConfigurationTagSource.String(), + })) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key", + Value: "val2", + Source: ApiTagSource.String(), + })) + require.NoError(t, rw.Create(ctx, &store.WorkerTag{ + WorkerId: id, + Key: "key2", + Value: "val2", + Source: ApiTagSource.String(), + })) + + got := getAggWorker(id) + require.NotNil(t, got.GetLastStatusTime()) + assert.ElementsMatch(t, got.apiTags, []*Tag{ + {Key: "key", Value: "val2"}, + {Key: "key2", Value: "val2"}, + }) + assert.ElementsMatch(t, got.configTags, []*Tag{ + {Key: "key", Value: "val"}, + }) + }) +} + +func TestWorker_Update(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + ctx := context.Background() + + newId := func() string { + id, err := newWorkerId(ctx) + require.NoError(t, err) + return id + } + + cases := []struct { + name string + initial Worker + update Worker + mask []string + nullMask []string + assert func(t *testing.T, init, up *Worker) + wantUpdateError bool + }{ + { + name: "kms update address", + initial: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "kms update address", + Address: "kms update address", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Address: "updated kms update address", + }, + }, + mask: []string{"Address"}, + assert: func(t *testing.T, init, up *Worker) { + t.Helper() + assert.Equal(t, "kms update address", up.Name) + assert.Equal(t, "updated kms update address", up.Address) + assert.Greater(t, up.GetUpdateTime().AsTime(), up.GetCreateTime().AsTime()) + assert.Equal(t, up.GetLastStatusTime().AsTime(), up.GetUpdateTime().AsTime()) + }, + }, + { + name: "kms update name", + initial: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "kms update name", + Address: "kms update name", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Name: "updated kms update name", + }, + }, + mask: []string{"Name"}, + wantUpdateError: true, + }, + { + name: "kms clear name", + initial: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "kms clear name", + Address: "kms clear name", + }, + }, + update: Worker{ + Worker: &store.Worker{}, + }, + nullMask: []string{"Name"}, + wantUpdateError: true, + }, + { + name: "pki update address", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki update address", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Address: "updated pki update address", + }, + }, + mask: []string{"Address"}, + assert: func(t *testing.T, init, up *Worker) { + t.Helper() + assert.Equal(t, "pki update address", up.Name) + assert.Equal(t, "updated pki update address", up.Address) + assert.Greater(t, up.GetUpdateTime().AsTime(), up.GetCreateTime().AsTime()) + assert.Equal(t, up.GetLastStatusTime().AsTime(), up.GetUpdateTime().AsTime()) + }, + }, + { + name: "pki update name", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki update name", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Name: "updated pki update name", + }, + }, + mask: []string{"Name"}, + assert: func(t *testing.T, init, up *Worker) { + t.Helper() + assert.Equal(t, "updated pki update name", up.Name) + assert.Greater(t, up.GetUpdateTime().AsTime(), up.GetCreateTime().AsTime()) + assert.Nil(t, up.GetLastStatusTime()) + }, + }, + { + name: "pki update description", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki update description", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Description: "updated pki update description", + }, + }, + mask: []string{"Description"}, + assert: func(t *testing.T, init, up *Worker) { + t.Helper() + assert.Equal(t, "pki update description", up.Name) + assert.Equal(t, "updated pki update description", up.Description) + assert.Greater(t, up.GetUpdateTime().AsTime(), up.GetCreateTime().AsTime()) + assert.Nil(t, up.GetLastStatusTime()) + }, + }, + { + name: "pki clear address", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki clear address", + Address: "pki clear address", + LastStatusTime: ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(time.Hour))}, + }, + }, + update: Worker{ + Worker: &store.Worker{}, + }, + nullMask: []string{"Address"}, + wantUpdateError: true, + }, + { + name: "pki clear name", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki clear name", + Address: "pki clear name", + LastStatusTime: ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(time.Hour))}, + }, + }, + update: Worker{ + Worker: &store.Worker{}, + }, + nullMask: []string{"Name"}, + assert: func(t *testing.T, init, up *Worker) { + t.Helper() + assert.Empty(t, up.Name) + assert.Greater(t, up.GetUpdateTime().AsTime(), up.GetCreateTime().AsTime()) + assert.Greater(t, up.GetLastStatusTime().AsTime(), up.GetUpdateTime().AsTime()) + assert.Less(t, up.GetLastStatusTime().AsTime(), time.Now().Add(time.Hour)) + }, + }, + { + name: "pki update name to capital", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki update name to capital", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Name: "PKI Update Name To Capital", + }, + }, + mask: []string{"Name"}, + wantUpdateError: true, + }, + { + name: "pki update name to unprintable", + initial: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "worker reported update name to unprintable", + }, + }, + update: Worker{ + Worker: &store.Worker{ + Name: "unprintable \u0008 name", + }, + }, + mask: []string{"Name"}, + wantUpdateError: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + init := tc.initial.clone() + require.NoError(t, rw.Create(ctx, init)) + up := tc.update.clone() + up.PublicId = init.PublicId + _, err := rw.Update(ctx, up, tc.mask, tc.nullMask) + if tc.wantUpdateError { + fmt.Println(err) + assert.Error(t, err) + return + } else { + require.NoError(t, err) + } + tc.assert(t, init.clone(), up.clone()) + }) + } +} + +func TestWorker_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + ctx := context.Background() + + newId := func() string { + id, err := newWorkerId(ctx) + require.NoError(t, err) + return id + } + + type wanted struct { + lastStatusUpdated bool + createError bool + } + + cases := []struct { + name string + in Worker + want wanted + }{ + { + name: "kms base", + in: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Address: "address", + Name: "kms base", + Description: "kms base", + }, + }, + want: wanted{lastStatusUpdated: true}, + }, + { + name: "kms with no name field", + in: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + { + name: "kms with no address field", + in: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "kms with no address field", + }, + }, + want: wanted{createError: true}, + }, + { + name: "kms with upper case name", + in: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "With Upper Case Worker Reported Name", + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + { + name: "kms with unprintable worker reported name", + in: Worker{ + Worker: &store.Worker{ + Type: KmsWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "unprintable \u0008 worker reported name", + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + // PKI workers are not created in a way that should set the address + // or the last_status_time. + { + name: "pki base", + in: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "pki base", + }, + }, + want: wanted{}, + }, + { + name: "pki with no name field", + in: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + }, + }, + want: wanted{}, + }, + { + name: "pki with address field", + in: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + { + name: "pki with upper case name", + in: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "PKI With Upper Case Worker Reported Name", + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + { + name: "pki with unprintable worker reported name", + in: Worker{ + Worker: &store.Worker{ + Type: PkiWorkerType.String(), + ScopeId: scope.Global.String(), + PublicId: newId(), + Name: "unprintable \u0008 worker reported name", + Address: "address", + }, + }, + want: wanted{createError: true}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + w := tc.in.clone() + err := rw.Create(ctx, w) + if tc.want.createError { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + if tc.want.lastStatusUpdated { + assert.NotNil(t, w.GetLastStatusTime()) + } else { + assert.Nil(t, w.GetLastStatusTime()) + } + + assert.NotNil(t, w.GetCreateTime()) + assert.Equal(t, w.GetCreateTime(), w.GetUpdateTime()) + }) + } +} + +func TestWorker_New(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + type args struct { + scopeId string + opts []Option + } + + tests := []struct { + name string + args args + want *Worker + }{ + { + name: "missing-scope-id", + args: args{}, + want: &Worker{ + Worker: &store.Worker{}, + }, + }, + { + name: "global-scope-id", + args: args{ + scopeId: scope.Global.String(), + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + }, + }, + }, + { + name: "org-scope-id", + args: args{ + scopeId: org.GetPublicId(), + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: org.GetPublicId(), + }, + }, + }, + { + name: "project-scope-id", + args: args{ + scopeId: prj.GetPublicId(), + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: prj.GetPublicId(), + }, + }, + }, + { + name: "with name", + args: args{ + scopeId: scope.Global.String(), + opts: []Option{ + WithName("foo"), + }, + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + Name: "foo", + }, + }, + }, + { + name: "with description", + args: args{ + scopeId: scope.Global.String(), + opts: []Option{ + WithDescription("foo"), + }, + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + Description: "foo", + }, + }, + }, + { + name: "with address", + args: args{ + scopeId: scope.Global.String(), + opts: []Option{ + WithAddress("foo"), + }, + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + Address: "foo", + }, + }, + }, + { + name: "with Tags", + args: args{ + scopeId: scope.Global.String(), + opts: []Option{ + WithWorkerTags(&Tag{ + Key: "key", + Value: "val", + }), + }, + }, + want: &Worker{ + Worker: &store.Worker{ + ScopeId: scope.Global.String(), + }, + inputTags: []*Tag{ + { + Key: "key", + Value: "val", + }, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + got := NewWorker(tt.args.scopeId, tt.args.opts...) + assert.Empty(cmp.Diff(tt.want, got.clone(), protocmp.Transform())) + }) + } +} diff --git a/internal/servers/workerauth_store_test.go b/internal/servers/workerauth_store_test.go new file mode 100644 index 000000000..4bddd2ca4 --- /dev/null +++ b/internal/servers/workerauth_store_test.go @@ -0,0 +1,532 @@ +package servers + +import ( + "context" + "math/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestRootCertStore(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsCache := kms.TestKms(t, conn, wrapper) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testKey, err := databaseWrapper.KeyId(ctx) + require.NoError(t, err) + + beforeTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(-1 * time.Hour))} + afterTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(1 * time.Hour))} + publicKey := populateBytes(defaultLength) + privateKey := populateBytes(defaultLength) + certificate := populateBytes(defaultLength) + + type args struct { + serialNumber uint64 + certificate []byte + rootCertKeys RootCertificateKeys + notValidBefore *timestamp.Timestamp + notValidAfter *timestamp.Timestamp + keyId string + state CertificateState + } + + tests := []struct { + name string + args args + expectedRootCert *store.RootCertificate + wantErr bool + wantCreateErr bool + }{ + { + name: "rootcert-store-next", + args: args{ + serialNumber: 1234567890, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: NextState, + }, + expectedRootCert: &store.RootCertificate{ + SerialNumber: 1234567890, + PublicKey: publicKey, + PrivateKey: privateKey, + Certificate: certificate, + NotValidBefore: beforeTimestamp, + NotValidAfter: afterTimestamp, + KeyId: testKey, + State: "next", + IssuingCa: ca_id, + }, + wantErr: false, + }, + { + name: "rootcert-store-current", + args: args{ + serialNumber: 9876543210, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: CurrentState, + }, + expectedRootCert: &store.RootCertificate{ + SerialNumber: 9876543210, + PublicKey: publicKey, + PrivateKey: privateKey, + Certificate: certificate, + NotValidBefore: beforeTimestamp, + NotValidAfter: afterTimestamp, + KeyId: testKey, + State: "current", + IssuingCa: ca_id, + }, + wantErr: false, + }, + { + name: "rootcert-store-empty-key", + args: args{ + serialNumber: 9876543210, + rootCertKeys: RootCertificateKeys{publicKey: populateBytes(0), privateKey: privateKey}, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: NextState, + }, + wantErr: true, + }, + { + name: "rootcert-no-keys", + args: args{ + serialNumber: 1011121314, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: NextState, + }, + wantErr: true, + }, + { + name: "rootcert-no-cert", + args: args{ + serialNumber: 15161718, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: NextState, + }, + wantErr: true, + }, + { + name: "rootcert-invalid-timestamps", + args: args{ + serialNumber: 1920212223, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: afterTimestamp, + notValidAfter: beforeTimestamp, + keyId: testKey, + state: NextState, + }, + wantCreateErr: true, + }, + { + name: "rootcert-invalid-timestamps-2", + args: args{ + serialNumber: 2425262728, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: afterTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: NextState, + }, + wantCreateErr: true, + }, + { + name: "rootcert-invalid-state", + args: args{ + serialNumber: 3031323334, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + keyId: testKey, + state: UnknownState, + }, + wantErr: true, + }, + { + name: "rootcert-no-key-id", + args: args{ + serialNumber: 4041424344, + rootCertKeys: RootCertificateKeys{publicKey: publicKey, privateKey: privateKey}, + certificate: certificate, + notValidBefore: beforeTimestamp, + notValidAfter: afterTimestamp, + state: NextState, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + cert, err := newRootCertificate(ctx, tt.args.serialNumber, tt.args.certificate, tt.args.notValidBefore, tt.args.notValidAfter, + tt.args.rootCertKeys, tt.args.keyId, tt.args.state) + + if tt.wantErr { + assert.Error(err) + return + } + require.NoError(err) + require.NotNil(cert) + + err = rw.Create(ctx, cert) + if tt.wantCreateErr { + assert.Error(err) + return + } else { + assert.NoError(err) + assert.Equal(tt.expectedRootCert, cert.RootCertificate) + assert.Empty(cmp.Diff(tt.expectedRootCert, cert, protocmp.Transform())) + } + + deleteWhere := `state = ?` + deleted, err := rw.Delete(ctx, &RootCertificate{}, db.WithWhere(deleteWhere, cert.State)) + assert.NoError(err) + assert.Equal(1, deleted) + }) + } +} + +func TestDuplicateRootCert(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsCache := kms.TestKms(t, conn, wrapper) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testKey, err := databaseWrapper.KeyId(ctx) + require.NoError(t, err) + + beforeTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(-1 * time.Hour))} + afterTimestamp := ×tamp.Timestamp{Timestamp: timestamppb.New(time.Now().Add(1 * time.Hour))} + + // Attempt to create a duplicate CA and expect a failure + certAuthority := newCertificateAuthority() + err = rw.Create(ctx, certAuthority) + require.Error(t, err) + + // Make two current and next root certs + current1, err := newRootCertificate(ctx, rand.Uint64(), populateBytes(defaultLength), beforeTimestamp, afterTimestamp, + RootCertificateKeys{publicKey: populateBytes(defaultLength), privateKey: populateBytes(defaultLength)}, + testKey, CurrentState) + require.NoError(t, err) + current2, err := newRootCertificate(ctx, rand.Uint64(), populateBytes(defaultLength), beforeTimestamp, afterTimestamp, + RootCertificateKeys{publicKey: populateBytes(defaultLength), privateKey: populateBytes(defaultLength)}, + testKey, CurrentState) + require.NoError(t, err) + next1, err := newRootCertificate(ctx, rand.Uint64(), populateBytes(defaultLength), beforeTimestamp, afterTimestamp, + RootCertificateKeys{publicKey: populateBytes(defaultLength), privateKey: populateBytes(defaultLength)}, + testKey, NextState) + require.NoError(t, err) + next2, err := newRootCertificate(ctx, rand.Uint64(), populateBytes(defaultLength), beforeTimestamp, afterTimestamp, + RootCertificateKeys{publicKey: populateBytes(defaultLength), privateKey: populateBytes(defaultLength)}, + testKey, NextState) + require.NoError(t, err) + + // Insert first set of certs + err = rw.Create(ctx, current1) + require.NoError(t, err) + err = rw.Create(ctx, next1) + require.NoError(t, err) + + // Insert second current cert and expect an error + err = rw.Create(ctx, current2) + require.Error(t, err) + err = rw.Create(ctx, next2) + require.Error(t, err) +} + +func TestWorkerAuthStore(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + kmsCache := kms.TestKms(t, conn, wrapper) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testKey, err := databaseWrapper.KeyId(ctx) + require.NoError(t, err) + + worker := TestPkiWorker(t, conn, wrapper) + + wSignPubKey := populateBytes(defaultLength) + wEncPubKey := populateBytes(defaultLength) + workerKeys := WorkerKeys{workerSigningPubKey: wSignPubKey, workerEncryptionPubKey: wEncPubKey} + controllerKey := populateBytes(defaultLength) + nonce := populateBytes(defaultLength) + + type args struct { + workerKeyIdentifier string + workerId string + } + tests := []struct { + name string + args args + expectedWorkerAuth *store.WorkerAuth + opt []Option + wantErr bool + wantCreateErr bool + }{ + { + name: "workerauth-store", + args: args{ + workerKeyIdentifier: "worker-auth-id-123", + workerId: worker.PublicId, + }, + opt: []Option{ + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + WithKeyId(testKey), + }, + expectedWorkerAuth: &store.WorkerAuth{ + WorkerKeyIdentifier: "worker-auth-id-123", + WorkerId: worker.PublicId, + WorkerSigningPubKey: wSignPubKey, + WorkerEncryptionPubKey: wEncPubKey, + ControllerEncryptionPrivKey: controllerKey, + KeyId: testKey, + Nonce: nonce, + }, + wantErr: false, + }, + { + name: "workerauth-store-invalid-key-id", + args: args{ + workerKeyIdentifier: "worker-auth-id-456", + workerId: worker.PublicId, + }, + opt: []Option{ + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + WithKeyId("fakeyityfake"), + }, + wantCreateErr: true, + }, + { + name: "workerauth-no-pkey", + args: args{ + workerId: worker.PublicId, + }, + opt: []Option{ + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + WithKeyId(testKey), + }, + wantErr: true, + }, + { + name: "workerauth-empty-pkey", + args: args{ + workerKeyIdentifier: "", + workerId: worker.PublicId, + }, + opt: []Option{ + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + WithKeyId(testKey), + }, + wantErr: true, + }, + { + name: "workerauth-invalid-workerid", + args: args{ + workerKeyIdentifier: "worker-auth-id-101", + workerId: "bogus-worker", + }, + opt: []Option{ + WithWorkerKeys(workerKeys), + WithControllerEncryptionPrivateKey(controllerKey), + WithNonce(nonce), + WithKeyId(testKey), + }, + wantCreateErr: true, + }, + { + name: "empty-workerauth", + args: args{}, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + wAuth, err := newWorkerAuth(ctx, tt.args.workerKeyIdentifier, tt.args.workerId, tt.opt...) + + if tt.wantErr { + assert.Error(err) + return + } + require.NoError(err) + require.NotNil(wAuth) + + err = rw.Create(ctx, wAuth) + if tt.wantCreateErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tt.expectedWorkerAuth, wAuth.WorkerAuth) + assert.Empty(cmp.Diff(tt.expectedWorkerAuth, wAuth, protocmp.Transform())) + } + }) + } +} + +func TestWorkerCertBundle(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKey := TestKmsKey(ctx, t, conn, wrapper) + + worker := TestPkiWorker(t, conn, wrapper) + workerAuth := TestWorkerAuth(t, conn, worker, testKey) + rootCA := TestRootCertificate(ctx, t, conn, testKey) + certBundle := populateBytes(defaultLength) + + type args struct { + certificatePubKey []byte + workerAuthId string + certificateBundle []byte + } + tests := []struct { + name string + args args + expected *store.WorkerCertBundle + wantErr bool + wantCreateErr bool + }{ + { + name: "workercertbundle-store", + args: args{ + certificatePubKey: rootCA.PublicKey, + workerAuthId: workerAuth.WorkerKeyIdentifier, + certificateBundle: certBundle, + }, + expected: &store.WorkerCertBundle{ + RootCertificatePublicKey: rootCA.PublicKey, + WorkerKeyIdentifier: workerAuth.WorkerKeyIdentifier, + CertBundle: certBundle, + }, + wantErr: false, + }, + { + name: "workercertbundle-empty-cert-bundle", + args: args{ + certificatePubKey: rootCA.PublicKey, + workerAuthId: workerAuth.WorkerKeyIdentifier, + }, + wantErr: true, + }, + { + name: "workercertbundle-no-worker-id", + args: args{ + certificatePubKey: rootCA.PublicKey, + certificateBundle: certBundle, + }, + wantErr: true, + }, + { + name: "workercertbundle-invalid-worker-id", + args: args{ + certificatePubKey: rootCA.PublicKey, + workerAuthId: "bogus-worker", + certificateBundle: certBundle, + }, + wantCreateErr: true, + }, + { + name: "workercertbundle-empty-publickey", + args: args{ + workerAuthId: workerAuth.WorkerKeyIdentifier, + certificateBundle: certBundle, + }, + wantErr: true, + }, + { + name: "workercertbundle-invalid-publickey", + args: args{ + certificatePubKey: populateBytes(defaultLength), + workerAuthId: workerAuth.WorkerKeyIdentifier, + certificateBundle: certBundle, + }, + wantCreateErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + workerCertBundle, err := newWorkerCertBundle(ctx, tt.args.certificatePubKey, tt.args.workerAuthId, tt.args.certificateBundle) + + if tt.wantErr { + assert.Error(err) + return + } + require.NoError(err) + require.NotNil(workerCertBundle) + + err = rw.Create(ctx, workerCertBundle) + if tt.wantCreateErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tt.expected, workerCertBundle.WorkerCertBundle) + assert.Empty(cmp.Diff(tt.expected, workerCertBundle, protocmp.Transform())) + } + }) + } +} diff --git a/internal/session/job_session_cleanup.go b/internal/session/job_session_cleanup.go index c684ed1af..3947cc6b2 100644 --- a/internal/session/job_session_cleanup.go +++ b/internal/session/job_session_cleanup.go @@ -20,7 +20,7 @@ import ( const deadWorkerConnCloseMinGrace = servers.DefaultLiveness type closeConnectionsForDeadWorkersResult struct { - ServerId string + WorkerId string LastUpdateTime time.Time NumberConnectionsClosed int } @@ -107,7 +107,7 @@ func (j *sessionConnectionCleanupJob) Run(ctx context.Context) error { for _, result := range results { event.WriteError(ctx, op, stderrors.New("worker has not reported status within acceptable grace period, all connections closed"), event.WithInfo( - "private_id", result.ServerId, + "public_id", result.WorkerId, "update_time", result.LastUpdateTime, "grace_period_seconds", j.gracePeriod.Seconds(), "number_connections_closed", result.NumberConnectionsClosed, @@ -116,9 +116,48 @@ func (j *sessionConnectionCleanupJob) Run(ctx context.Context) error { j.totalClosed += result.NumberConnectionsClosed } + { + count, err := j.closeWorkerlessConnections(ctx) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if count > 0 { + event.WriteError(ctx, op, stderrors.New("connections with no worker closed"), + event.WithInfo( + "number_connections_closed", count, + )) + j.totalClosed += count + } + } + return nil } +// closeWorkerlessConnections will close all connections which do not have a +// worker id associated with them. +func (j *sessionConnectionCleanupJob) closeWorkerlessConnections(ctx context.Context) (int, error) { + const op = "session.(sessionConnectionCleanupJob).closeWorkerlessConnections" + count := 0 + _, err := j.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + var err error + count, err = w.Exec(ctx, closeWorkerlessConnections, []interface{}{}) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil + }, + ) + if err != nil { + return count, errors.Wrap(ctx, err, op) + } + + return count, nil +} + // closeConnectionsForDeadWorkers will run // closeConnectionsForDeadServersCte to look for connections that // should be marked because they are on a server that is no longer diff --git a/internal/session/job_session_cleanup_test.go b/internal/session/job_session_cleanup_test.go index b9ff65d35..837567c74 100644 --- a/internal/session/job_session_cleanup_test.go +++ b/internal/session/job_session_cleanup_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -44,19 +45,19 @@ func TestSessionConnectionCleanupJob(t *testing.T) { // Create two "workers". One will remain untouched while the other "goes // away and comes back" (worker 2). - worker1 := TestWorker(t, conn, wrapper, WithServerId("worker1")) - worker2 := TestWorker(t, conn, wrapper, WithServerId("worker2")) + worker1 := servers.TestKmsWorker(t, conn, wrapper) + worker2 := servers.TestKmsWorker(t, conn, wrapper) // Create a few sessions on each, activate, and authorize a connection var connIds []string connIdsByWorker := make(map[string][]string) for i := 0; i < numConns; i++ { - serverId := worker1.PrivateId + serverId := worker1.PublicId if i%2 == 0 { - serverId = worker2.PrivateId + serverId = worker2.PublicId } - sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithServerId(serverId), WithDbOpts(db.WithSkipVetForWrite(true))) - sess, _, err = sessionRepo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, serverId, "worker", []byte("foo")) + sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithDbOpts(db.WithSkipVetForWrite(true))) + sess, _, err = sessionRepo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) require.NoError(err) c, cs, _, err := AuthorizeConnection(ctx, sessionRepo, connectionRepo, sess.GetPublicId(), serverId) require.NoError(err) @@ -64,9 +65,9 @@ func TestSessionConnectionCleanupJob(t *testing.T) { require.Equal(StatusAuthorized, cs[0].Status) connIds = append(connIds, c.GetPublicId()) if i%2 == 0 { - connIdsByWorker[worker2.PrivateId] = append(connIdsByWorker[worker2.PrivateId], c.GetPublicId()) + connIdsByWorker[worker2.PublicId] = append(connIdsByWorker[worker2.PublicId], c.GetPublicId()) } else { - connIdsByWorker[worker1.PrivateId] = append(connIdsByWorker[worker1.PrivateId], c.GetPublicId()) + connIdsByWorker[worker1.PublicId] = append(connIdsByWorker[worker1.PublicId], c.GetPublicId()) } } @@ -108,9 +109,11 @@ func TestSessionConnectionCleanupJob(t *testing.T) { // Push an upsert to the first worker so that its status has been // updated. - _, rowsUpdated, err := serversRepo.UpsertServer(ctx, worker1, []servers.Option{}...) + _, err = serversRepo.UpsertWorkerStatus(ctx, servers.NewWorker(scope.Global.String(), + servers.WithName(worker1.GetName()), + servers.WithAddress(worker1.GetAddress())), + servers.WithPublicId(worker1.GetPublicId())) require.NoError(err) - require.Equal(1, rowsUpdated) // Run the job. require.NoError(job.Run(ctx)) @@ -121,7 +124,7 @@ func TestSessionConnectionCleanupJob(t *testing.T) { require.True(ok) require.Len(connIds, 6) for _, connId := range connIds { - _, states, err := connectionRepo.LookupConnection(ctx, connId, nil) + _, states, err := connectionRepo.LookupConnection(ctx, connId) require.NoError(err) var foundClosed bool for _, state := range states { @@ -135,9 +138,9 @@ func TestSessionConnectionCleanupJob(t *testing.T) { } // Assert that all connections on the second worker are closed - assertConnections(worker2.PrivateId, true) + assertConnections(worker2.PublicId, true) // Assert that all connections on the first worker are still open - assertConnections(worker1.PrivateId, false) + assertConnections(worker1.PublicId, false) } func TestSessionConnectionCleanupJobNewJobErr(t *testing.T) { @@ -196,24 +199,24 @@ func TestCloseConnectionsForDeadWorkers(t *testing.T) { // firstly, the last worker will have no connections at all, and we will be // closing the others in stages to test multiple servers being closed at // once. - worker1 := TestWorker(t, conn, wrapper, WithServerId("worker1")) - worker2 := TestWorker(t, conn, wrapper, WithServerId("worker2")) - worker3 := TestWorker(t, conn, wrapper, WithServerId("worker3")) - worker4 := TestWorker(t, conn, wrapper, WithServerId("worker4")) + worker1 := servers.TestKmsWorker(t, conn, wrapper) + worker2 := servers.TestKmsWorker(t, conn, wrapper) + worker3 := servers.TestKmsWorker(t, conn, wrapper) + worker4 := servers.TestKmsWorker(t, conn, wrapper) // Create sessions on the first three, activate, and authorize connections var worker1ConnIds, worker2ConnIds, worker3ConnIds []string for i := 0; i < numConns; i++ { var serverId string if i%3 == 0 { - serverId = worker1.PrivateId + serverId = worker1.PublicId } else if i%3 == 1 { - serverId = worker2.PrivateId + serverId = worker2.PublicId } else { - serverId = worker3.PrivateId + serverId = worker3.PublicId } - sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithServerId(serverId), WithDbOpts(db.WithSkipVetForWrite(true))) - sess, _, err = repo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, serverId, "worker", []byte("foo")) + sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithDbOpts(db.WithSkipVetForWrite(true))) + sess, _, err = repo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) require.NoError(err) c, cs, err := connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), serverId) require.NoError(err) @@ -288,23 +291,13 @@ func TestCloseConnectionsForDeadWorkers(t *testing.T) { // updateServer is a helper for updating the update time for our // servers. The controller is read back so that we can reference // the most up-to-date fields. - updateServer := func(t *testing.T, w *servers.Server) *servers.Server { + updateServer := func(t *testing.T, w *servers.Worker) *servers.Worker { t.Helper() - _, rowsUpdated, err := serversRepo.UpsertServer(ctx, w) + pubId := w.GetPublicId() + w.PublicId = "" + wkr, err := serversRepo.UpsertWorkerStatus(ctx, w, servers.WithPublicId(pubId)) require.NoError(err) - require.Equal(1, rowsUpdated) - servers, err := serversRepo.ListServers(ctx, servers.ServerTypeWorker) - require.NoError(err) - for _, server := range servers { - if server.PrivateId == w.PrivateId { - return server - } - } - - require.FailNowf("server %q not found after updating", w.PrivateId) - // Looks weird but needed to build, as we fail in testify instead - // of returning an error - return nil + return wkr } // requireConnectionStatus is a helper expecting all connections on a worker @@ -398,8 +391,8 @@ func TestCloseConnectionsForDeadWorkers(t *testing.T) { // number of connections closed. Due to how things are require.Equal([]closeConnectionsForDeadWorkersResult{ { - ServerId: worker1.PrivateId, - LastUpdateTime: timestampPbAsUTC(t, worker1.UpdateTime.AsTime()), + WorkerId: worker1.PublicId, + LastUpdateTime: timestampPbAsUTC(t, worker1.GetLastStatusTime().AsTime()), NumberConnectionsClosed: 12, // 18 per server, with 6 closed already }, }, result) @@ -420,15 +413,15 @@ func TestCloseConnectionsForDeadWorkers(t *testing.T) { result, err := job.closeConnectionsForDeadWorkers(ctx, gracePeriod) require.NoError(err) // Assert that we have one result with the appropriate ID and number of connections closed. - require.Equal([]closeConnectionsForDeadWorkersResult{ + require.ElementsMatch([]closeConnectionsForDeadWorkersResult{ { - ServerId: worker2.PrivateId, - LastUpdateTime: timestampPbAsUTC(t, worker2.UpdateTime.AsTime()), + WorkerId: worker2.PublicId, + LastUpdateTime: timestampPbAsUTC(t, worker2.GetLastStatusTime().AsTime()), NumberConnectionsClosed: 12, // 18 per server, with 6 closed already }, { - ServerId: worker3.PrivateId, - LastUpdateTime: timestampPbAsUTC(t, worker3.UpdateTime.AsTime()), + WorkerId: worker3.PublicId, + LastUpdateTime: timestampPbAsUTC(t, worker3.GetLastStatusTime().AsTime()), NumberConnectionsClosed: 12, // 18 per server, with 6 closed already }, }, result) @@ -440,3 +433,95 @@ func TestCloseConnectionsForDeadWorkers(t *testing.T) { requireConnectionStatus(t, worker3ConnIds, true) } } + +func TestCloseWorkerlessConnections(t *testing.T) { + t.Parallel() + ctx := context.Background() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + connRepo, err := NewConnectionRepository(ctx, rw, rw, kms) + require.NoError(err) + + job, err := newSessionConnectionCleanupJob(rw, time.Hour) + require.NoError(err) + + createConnection := func(workerId string) *Connection { + sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithDbOpts(db.WithSkipVetForWrite(true))) + sess, _, err = repo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) + require.NoError(err) + + conn, cs, err := connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), workerId) + require.NoError(err) + require.Len(cs, 1) + require.Equal(StatusAuthorized, cs[0].Status) + return conn + } + + // Setup deleted worker connections + deletedWorker := servers.TestKmsWorker(t, conn, wrapper) + dActiveConn := createConnection(deletedWorker.GetPublicId()) + dClosedConn := createConnection(deletedWorker.GetPublicId()) + connRepo.closeConnections(ctx, []CloseWith{{ + ConnectionId: dClosedConn.PublicId, + BytesUp: 1, + BytesDown: 2, + ClosedReason: ConnectionClosedByUser, + }}) + _, err = rw.Delete(ctx, deletedWorker) + require.NoError(err) + + // Non deleted worker case + activeWorker := servers.TestKmsWorker(t, conn, wrapper) + activeConn := createConnection(activeWorker.GetPublicId()) + closedConn := createConnection(activeWorker.GetPublicId()) + connRepo.closeConnections(ctx, []CloseWith{{ + ConnectionId: closedConn.PublicId, + BytesUp: 1, + BytesDown: 2, + ClosedReason: ConnectionClosedByUser, + }}) + + _, st, err := connRepo.LookupConnection(ctx, dActiveConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusAuthorized, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, dClosedConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusClosed, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, activeConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusAuthorized, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, closedConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusClosed, st[0].Status) + + // Run the job + numClosed, err := job.closeWorkerlessConnections(ctx) + require.NoError(err) + assert.Equal(t, 1, numClosed) + + // This is the only one that the job should have actually closed. + _, st, err = connRepo.LookupConnection(ctx, dActiveConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusClosed, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, dClosedConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusClosed, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, activeConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusAuthorized, st[0].Status) + + _, st, err = connRepo.LookupConnection(ctx, closedConn.GetPublicId()) + require.NoError(err) + require.Equal(StatusClosed, st[0].Status) +} diff --git a/internal/session/options.go b/internal/session/options.go index e69ec2c7d..a46fe134d 100644 --- a/internal/session/options.go +++ b/internal/session/options.go @@ -29,7 +29,6 @@ type options struct { withTestTofu []byte withListingConvert bool withSessionIds []string - withServerId string withDbOpts []db.Option withWorkerStateDelay time.Duration withTerminated bool @@ -100,14 +99,6 @@ func withListingConvert(withListingConvert bool) Option { } } -// WithServerId allows the specification of the server id to use for the -// operation. -func WithServerId(id string) Option { - return func(o *options) { - o.withServerId = id - } -} - // WithDbOpts passes through given DB options to the DB layer func WithDbOpts(opts ...db.Option) Option { return func(o *options) { diff --git a/internal/session/options_test.go b/internal/session/options_test.go index 975a2f93f..a32c08772 100644 --- a/internal/session/options_test.go +++ b/internal/session/options_test.go @@ -75,11 +75,4 @@ func Test_GetOpts(t *testing.T) { testOpts.withSessionIds = []string{"s_1", "s_2", "s_3"} assert.Equal(opts, testOpts) }) - t.Run("WithServerId", func(t *testing.T) { - assert := assert.New(t) - opts := getOpts(WithServerId("worker1")) - testOpts := getDefaultOptions() - testOpts.withServerId = "worker1" - assert.Equal(opts, testOpts) - }) } diff --git a/internal/session/query.go b/internal/session/query.go index 5242ff9a1..4e086c37f 100644 --- a/internal/session/query.go +++ b/internal/session/query.go @@ -81,7 +81,7 @@ active_session as ( select ss.session_id as session_id, @public_id as public_id, - @worker_id as server_id + @worker_id as worker_id from session_state ss where @@ -92,7 +92,7 @@ active_session as ( insert into session_connection ( session_id, public_id, - server_id + worker_id ) select * from active_session; ` @@ -324,34 +324,48 @@ where // * not closed // * belong to servers that have not reported in within an acceptable // threshold of time + // * belong to servers where we do not know when they last reported. // // and marks them as closed. // // The query returns the set of servers that have had connections closed // along with their last update time and the number of connections closed on - // each. + // each. If we do not know the last update time, we use the current time. closeConnectionsForDeadServersCte = ` with - dead_servers (server_id, last_update_time) as ( - select private_id, update_time - from server - where update_time < wt_sub_seconds_from_now(@grace_period_seconds) + dead_workers (worker_id, last_update_time) as ( + select + w.public_id as worker_id, + w.last_status_time as last_update_time + from server_worker w + where + w.last_status_time < wt_sub_seconds_from_now(@grace_period_seconds) ), - closed_connections (connection_id, server_id) as ( + closed_connections (connection_id, worker_id) as ( update session_connection set closed_reason = 'system error' - where server_id in (select server_id from dead_servers) + where worker_id in (select worker_id from dead_workers) and closed_reason is null - returning public_id, server_id + returning public_id, worker_id ) - select closed_connections.server_id, - dead_servers.last_update_time, + select closed_connections.worker_id, + dead_workers.last_update_time as last_update_time, count(closed_connections.connection_id) as number_connections_closed from closed_connections - join dead_servers - on closed_connections.server_id = dead_servers.server_id - group by closed_connections.server_id, dead_servers.last_update_time - order by closed_connections.server_id; + join dead_workers + on closed_connections.worker_id = dead_workers.worker_id + group by closed_connections.worker_id, dead_workers.last_update_time + order by closed_connections.worker_id; +` + + // closeWorkerlessConnections closes any open connections which has the + // worker id set to null. + closeWorkerlessConnections = ` + update session_connection + set closed_reason = 'system error' + where worker_id is null + and closed_reason is null + returning public_id; ` orphanedConnectionsCte = ` @@ -375,7 +389,7 @@ with from session_connection where -- Related to the worker that just reported to us - server_id = @server_id + worker_id = @worker_id -- Only unclosed ones and public_id in (select connection_id from unclosed_connections) -- These are connection IDs that just got reported to us by the given diff --git a/internal/session/repository.go b/internal/session/repository.go index d9f4c860a..d35abfdb0 100644 --- a/internal/session/repository.go +++ b/internal/session/repository.go @@ -104,8 +104,6 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi PublicId: sv.PublicId, UserId: sv.UserId, HostId: sv.HostId, - ServerId: sv.ServerId, - ServerType: sv.ServerType, TargetId: sv.TargetId, HostSetId: sv.HostSetId, AuthTokenId: sv.AuthTokenId, diff --git a/internal/session/repository_connection.go b/internal/session/repository_connection.go index 808a2333a..6c8c6c4a8 100644 --- a/internal/session/repository_connection.go +++ b/internal/session/repository_connection.go @@ -352,7 +352,7 @@ func (r *ConnectionRepository) closeOrphanedConnections(ctx context.Context, wor var orphanedConns []string args := make([]interface{}, 0, len(reportedConnections)+2) - args = append(args, sql.Named("server_id", workerId)) + args = append(args, sql.Named("worker_id", workerId)) args = append(args, sql.Named("worker_state_delay_seconds", r.workerStateDelay.Seconds())) var notInClause string diff --git a/internal/session/repository_connection_test.go b/internal/session/repository_connection_test.go index 164d0155c..df43a59c9 100644 --- a/internal/session/repository_connection_test.go +++ b/internal/session/repository_connection_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/servers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -147,9 +148,8 @@ func TestRepository_ConnectConnection(t *testing.T) { setupFn := func() ConnectWith { s := TestDefaultSession(t, conn, wrapper, iamRepo) - srv := TestWorker(t, conn, wrapper) tofu := TestTofu(t) - _, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, srv.PrivateId, srv.Type, tofu) + _, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 2222, "127.0.0.1") return ConnectWith{ @@ -359,20 +359,20 @@ func TestRepository_orphanedConnections(t *testing.T) { // Create two "workers". One will remain untouched while the other "goes // away and comes back" (worker 2). - worker1 := TestWorker(t, conn, wrapper, WithServerId("worker1")) - worker2 := TestWorker(t, conn, wrapper, WithServerId("worker2")) + worker1 := servers.TestKmsWorker(t, conn, wrapper) + worker2 := servers.TestKmsWorker(t, conn, wrapper) // Create a few sessions on each, activate, and authorize a connection var connIds []string var worker1ConnIds []string var worker2ConnIds []string for i := 0; i < numConns; i++ { - serverId := worker1.PrivateId + serverId := worker1.PublicId if i%2 == 0 { - serverId = worker2.PrivateId + serverId = worker2.PublicId } - sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithServerId(serverId), WithDbOpts(db.WithSkipVetForWrite(true))) - sess, _, err = repo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, serverId, "worker", []byte("foo")) + sess := TestDefaultSession(t, conn, wrapper, iamRepo, WithDbOpts(db.WithSkipVetForWrite(true))) + sess, _, err = repo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) require.NoError(err) c, cs, err := connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), serverId) require.NoError(err) @@ -418,7 +418,7 @@ func TestRepository_orphanedConnections(t *testing.T) { // all connection IDs for worker 1 should be showing as non-closed, and // the ones for worker 2 not advertised should be closed. shouldStayOpen := worker2ConnIds[0:2] - found, err := connRepo.closeOrphanedConnections(ctx, worker2.GetPrivateId(), shouldStayOpen) + found, err := connRepo.closeOrphanedConnections(ctx, worker2.GetPublicId(), shouldStayOpen) require.NoError(err) fmt.Printf("shouldstate: %v\nfound: %v\n", shouldStayOpen, found) require.Equal(4, len(found)) @@ -432,7 +432,7 @@ func TestRepository_orphanedConnections(t *testing.T) { // Now, advertise none of the connection IDs for worker 2. This is mainly to // test that handling the case where we do not include IDs works properly as // it changes the where clause. - found, err = connRepo.closeOrphanedConnections(ctx, worker1.GetPrivateId(), nil) + found, err = connRepo.closeOrphanedConnections(ctx, worker1.GetPublicId(), nil) require.NoError(err) assert.Equal(6, len(found)) assert.ElementsMatch(found, worker1ConnIds) @@ -453,9 +453,8 @@ func TestRepository_CloseConnections(t *testing.T) { setupFn := func(cnt int) []CloseWith { s := TestDefaultSession(t, conn, wrapper, iamRepo) - srv := TestWorker(t, conn, wrapper) tofu := TestTofu(t) - s, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, srv.PrivateId, srv.Type, tofu) + s, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) cw := make([]CloseWith, 0, cnt) for i := 0; i < cnt; i++ { diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index b225409fc..cdd83b1d9 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -19,7 +19,7 @@ import ( // CreateSession inserts into the repository and returns the new Session with // its State of "Pending". The following fields must be empty when creating a -// session: ServerId, ServerType, and PublicId. No options are +// session: WorkerId, and PublicId. No options are // currently supported. func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping.Wrapper, newSession *Session, workerAddresses []string, _ ...Option) (*Session, ed25519.PrivateKey, error) { const op = "session.(Repository).CreateSession" @@ -50,12 +50,6 @@ func (r *Repository) CreateSession(ctx context.Context, sessionWrapper wrapping. if newSession.ScopeId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } - if newSession.ServerId != "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "server id is not empty") - } - if newSession.ServerType != "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "server type is not empty") - } if newSession.CtTofuToken != nil { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "ct is not empty") } @@ -279,7 +273,7 @@ func (r *Repository) fetchAuthzProtectedSessionsByScope( return sessionsMap, nil } -// ListSessions will sessions. Supports the WithLimit, WithScopeId, WithSessionIds, and WithServerId options. +// ListSessions lists sessions. Supports the WithLimit, WithScopeId, and WithSessionIds options. func (r *Repository) ListSessions(ctx context.Context, opt ...Option) ([]*Session, error) { const op = "session.(Repository).ListSessions" opts := getOpts(opt...) @@ -320,11 +314,6 @@ func (r *Repository) ListSessions(ctx context.Context, opt ...Option) ([]*Sessio where = append(where, fmt.Sprintf("s.public_id in (%s)", strings.Join(idsInClause, ","))) } - if opts.withServerId != "" { - inClauseCnt += 1 - where, args = append(where, fmt.Sprintf("server_id = @%d", inClauseCnt)), append(args, sql.Named(fmt.Sprintf("%d", inClauseCnt), opts.withServerId)) - } - var limit string switch { case opts.withLimit < 0: // any negative number signals unlimited results @@ -523,7 +512,7 @@ func (r *Repository) sessionAuthzSummary(ctx context.Context, sessionId string) // activated. States are ordered by start time descending. Returns an // InvalidSessionState error code if a connection cannot be made because the session // was canceled or terminated. -func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sessionVersion uint32, serverId, serverType string, tofuToken []byte) (*Session, []*State, error) { +func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sessionVersion uint32, tofuToken []byte) (*Session, []*State, error) { const op = "session.(Repository).ActivateSession" if sessionId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing session id") @@ -531,12 +520,6 @@ func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sess if sessionVersion == 0 { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - if serverId == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing server id") - } - if serverType == "" { - return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing server type") - } if len(tofuToken) == 0 { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing tofu token") } @@ -573,8 +556,6 @@ func (r *Repository) ActivateSession(ctx context.Context, sessionId string, sess } updatedSession.TofuToken = tofuToken - updatedSession.ServerId = serverId - updatedSession.ServerType = serverType if err := updatedSession.encrypt(ctx, databaseWrapper); err != nil { return errors.Wrap(ctx, err, op) } diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 3a259348c..61f6c6672 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -212,32 +212,6 @@ func TestRepository_ListSession(t *testing.T) { assert.Equal(StatusActive, got[0].States[0].Status) assert.Equal(StatusPending, got[0].States[1].Status) }) - t.Run("withServerId", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - db.TestDeleteWhere(t, conn, func() interface{} { i := AllocSession(); return &i }(), "1=1") - for i := 0; i < 6; i++ { - if i%2 == 0 { - TestWorker(t, conn, wrapper, WithServerId(fmt.Sprintf("server-%d", i/2))) - } - _ = TestSession(t, conn, wrapper, composedOf, - WithServerId(fmt.Sprintf("server-%d", i/2)), - WithDbOpts(db.WithSkipVetForWrite(true)), - ) - } - got, err := repo.ListSessions(context.Background()) - require.NoError(err) - assert.Equal(6, len(got)) - - for i := 0; i < 3; i++ { - serverId := fmt.Sprintf("server-%d", i) - got, err = repo.ListSessions(context.Background(), WithServerId(serverId)) - require.NoError(err) - assert.Equal(2, len(got)) - for _, item := range got { - assert.Equal(serverId, item.ServerId) - } - } - }) } func TestRepository_ListSessions_Multiple_Scopes(t *testing.T) { @@ -585,7 +559,6 @@ func TestRepository_transitionState(t *testing.T) { repo, err := NewRepository(rw, rw, kms) require.NoError(t, err) tofu := TestTofu(t) - srv := TestWorker(t, conn, wrapper) tests := []struct { name string @@ -653,7 +626,7 @@ func TestRepository_transitionState(t *testing.T) { var ss []*State var err error if status == StatusActive { - s, ss, err = repo.ActivateSession(context.Background(), id, version, srv.PrivateId, srv.Type, tofu) + s, ss, err = repo.ActivateSession(context.Background(), id, version, tofu) } else { s, ss, err = repo.updateState(context.Background(), id, version, status) } @@ -692,9 +665,8 @@ func TestRepository_TerminateCompletedSessions(t *testing.T) { composedOf.ExpirationTime = ×tamp.Timestamp{Timestamp: exp} s := TestSession(t, conn, wrapper, composedOf) - srv := TestWorker(t, conn, wrapper) tofu := TestTofu(t) - s, _, err = repo.ActivateSession(context.Background(), s.PublicId, s.Version, srv.PrivateId, srv.Type, tofu) + s, _, err = repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) c := TestConnection(t, conn, s.PublicId, "127.0.0.1", 22, "127.0.0.1", 222, "127.0.0.1") if !leaveOpen { @@ -1238,7 +1210,6 @@ func TestRepository_ActivateSession(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) repo, err := NewRepository(rw, rw, kms) require.NoError(t, err) - worker := TestWorker(t, conn, wrapper) tofu := TestTofu(t) tests := []struct { @@ -1258,7 +1229,7 @@ func TestRepository_ActivateSession(t *testing.T) { name: "already-active", session: func() *Session { s := TestDefaultSession(t, conn, wrapper, iamRepo) - activeSession, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, worker.PrivateId, worker.Type, tofu) + activeSession, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) return activeSession }(), @@ -1321,7 +1292,7 @@ func TestRepository_ActivateSession(t *testing.T) { default: version = tt.session.Version } - s, ss, err := repo.ActivateSession(context.Background(), id, version, worker.PrivateId, worker.Type, tofu) + s, ss, err := repo.ActivateSession(context.Background(), id, version, tofu) if tt.wantErr { require.Error(err) assert.Truef(errors.Match(errors.T(tt.wantIsError), err), "unexpected error %s", err.Error()) @@ -1337,17 +1308,17 @@ func TestRepository_ActivateSession(t *testing.T) { t.Run("already active", func(t *testing.T) { assert, require := assert.New(t), require.New(t) session := TestDefaultSession(t, conn, wrapper, iamRepo) - s, ss, err := repo.ActivateSession(context.Background(), session.PublicId, 1, worker.PrivateId, worker.Type, tofu) + s, ss, err := repo.ActivateSession(context.Background(), session.PublicId, 1, tofu) require.NoError(err) require.NotNil(s) require.NotNil(ss) assert.Equal(2, len(ss)) assert.Equal(StatusActive, ss[0].Status) - _, _, err = repo.ActivateSession(context.Background(), session.PublicId, 1, worker.PrivateId, worker.Type, tofu) + _, _, err = repo.ActivateSession(context.Background(), session.PublicId, 1, tofu) require.Error(err) - _, _, err = repo.ActivateSession(context.Background(), session.PublicId, 2, worker.PrivateId, worker.Type, tofu) + _, _, err = repo.ActivateSession(context.Background(), session.PublicId, 2, tofu) require.Error(err) }) } diff --git a/internal/session/service_authorize_connection_test.go b/internal/session/service_authorize_connection_test.go index 0cd6e86a2..e0d3cf739 100644 --- a/internal/session/service_authorize_connection_test.go +++ b/internal/session/service_authorize_connection_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/hashicorp/boundary/internal/servers" "google.golang.org/protobuf/types/known/timestamppb" "github.com/hashicorp/boundary/internal/db" @@ -35,10 +36,10 @@ func TestService_AuthorizeConnection(t *testing.T) { composedOf.ExpirationTime = exp } s := TestSession(t, conn, wrapper, composedOf) - srv := TestWorker(t, conn, wrapper) - testServer = srv.PrivateId + srv := servers.TestKmsWorker(t, conn, wrapper) + testServer = srv.PublicId tofu := TestTofu(t) - _, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, srv.PrivateId, srv.Type, tofu) + _, _, err := repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) return s } diff --git a/internal/session/service_close_connections_test.go b/internal/session/service_close_connections_test.go index 8eb5d3fa6..d54fafa92 100644 --- a/internal/session/service_close_connections_test.go +++ b/internal/session/service_close_connections_test.go @@ -30,9 +30,8 @@ func TestServiceCloseConnections(t *testing.T) { setupFn := func(cnt int, addtlConn int) sessionAndCloseWiths { s := TestDefaultSession(t, conn, wrapper, iamRepo) - srv := TestWorker(t, conn, wrapper) tofu := TestTofu(t) - s, _, err = repo.ActivateSession(context.Background(), s.PublicId, s.Version, srv.PrivateId, srv.Type, tofu) + s, _, err = repo.ActivateSession(context.Background(), s.PublicId, s.Version, tofu) require.NoError(t, err) require.NoError(t, err) diff --git a/internal/session/service_worker_status_report.go b/internal/session/service_worker_status_report.go index 0cc62a426..0ed4820a3 100644 --- a/internal/session/service_worker_status_report.go +++ b/internal/session/service_worker_status_report.go @@ -42,7 +42,7 @@ func WorkerStatusReport(ctx context.Context, repo *Repository, connRepo *Connect return notActive, errors.New(ctx, errors.Internal, op, fmt.Sprintf("Error closing orphaned connections for worker %s: %v", workerId, err)) } if len(closed) > 0 { - event.WriteSysEvent(ctx, op, "marked unclaimed connections as closed", "server_id", workerId, "count", len(closed)) + event.WriteSysEvent(ctx, op, "marked unclaimed connections as closed", "controller_id", workerId, "count", len(closed)) } return notActive, err } diff --git a/internal/session/service_worker_status_report_test.go b/internal/session/service_worker_status_report_test.go index 0589bdea3..4cf5a608b 100644 --- a/internal/session/service_worker_status_report_test.go +++ b/internal/session/service_worker_status_report_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/store" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/target/tcp" @@ -26,16 +27,11 @@ func TestWorkerStatusReport(t *testing.T) { org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) serverRepo, _ := servers.NewRepository(rw, rw, kms) - serverRepo.UpsertServer(ctx, &servers.Server{ + _, err := serverRepo.UpsertController(ctx, &store.Controller{ PrivateId: "test_controller1", - Type: "controller", - Address: "127.0.0.1", - }) - serverRepo.UpsertServer(ctx, &servers.Server{ - PrivateId: "test_worker1", - Type: "worker", Address: "127.0.0.1", }) + require.NoError(t, err) repo, err := session.NewRepository(rw, rw, kms) require.NoError(t, err) @@ -56,7 +52,7 @@ func TestWorkerStatusReport(t *testing.T) { ) type testCase struct { - worker *servers.Server + worker *servers.Worker req []session.StateReport want []session.StateReport orphanedConnections []string @@ -68,7 +64,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "No Sessions", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) return testCase{ worker: worker, req: []session.StateReport{}, @@ -79,7 +75,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "No Sessions already canceled", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -91,11 +87,11 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) require.NoError(t, err) - _, _, err = connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + _, _, err = connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess.PublicId, sess.Version) @@ -111,7 +107,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "Still Active", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -123,11 +119,11 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) require.NoError(t, err) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) return testCase{ worker: worker, @@ -145,7 +141,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "SessionClosed", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -157,9 +153,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess.PublicId, sess.Version) require.NoError(t, err) @@ -185,7 +181,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "MultipleSessionsClosed", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -197,9 +193,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess.PublicId, sess.Version) require.NoError(t, err) @@ -215,9 +211,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu2 := session.TestTofu(t) - sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, worker.PrivateId, worker.Type, tofu2) + sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, tofu2) require.NoError(t, err) - connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PrivateId) + connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess2.PublicId, sess2.Version) require.NoError(t, err) @@ -252,7 +248,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "OrphanedConnection", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -264,9 +260,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) sess2 := session.TestSession(t, conn, wrapper, session.ComposedOf{ @@ -280,9 +276,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu2 := session.TestTofu(t) - sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, worker.PrivateId, worker.Type, tofu2) + sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, tofu2) require.NoError(t, err) - connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PrivateId) + connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PublicId) require.NoError(t, err) require.NotEqual(t, connection.PublicId, connection2.PublicId) @@ -303,7 +299,7 @@ func TestWorkerStatusReport(t *testing.T) { { name: "MultipleSessionsAndOrphanedConnections", caseFn: func(t *testing.T) testCase { - worker := session.TestWorker(t, conn, wrapper) + worker := servers.TestKmsWorker(t, conn, wrapper) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -315,9 +311,9 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu := session.TestTofu(t) - sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, tofu) + sess, _, err = repo.ActivateSession(ctx, sess.PublicId, sess.Version, tofu) require.NoError(t, err) - connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PrivateId) + connection, _, err := connRepo.AuthorizeConnection(ctx, sess.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess.PublicId, sess.Version) require.NoError(t, err) @@ -333,11 +329,11 @@ func TestWorkerStatusReport(t *testing.T) { ConnectionLimit: 10, }) tofu2 := session.TestTofu(t) - sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, worker.PrivateId, worker.Type, tofu2) + sess2, _, err = repo.ActivateSession(ctx, sess2.PublicId, sess2.Version, tofu2) require.NoError(t, err) - connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PrivateId) + connection2, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PublicId) require.NoError(t, err) - connection3, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PrivateId) + connection3, _, err := connRepo.AuthorizeConnection(ctx, sess2.PublicId, worker.PublicId) require.NoError(t, err) _, err = repo.CancelSession(ctx, sess2.PublicId, sess2.Version) require.NoError(t, err) @@ -378,7 +374,7 @@ func TestWorkerStatusReport(t *testing.T) { tc := tt.caseFn(t) - got, err := session.WorkerStatusReport(ctx, repo, connRepo, tc.worker.PrivateId, tc.req) + got, err := session.WorkerStatusReport(ctx, repo, connRepo, tc.worker.PublicId, tc.req) require.NoError(err) assert.ElementsMatch(tc.want, got) for _, dc := range tc.orphanedConnections { diff --git a/internal/session/session.go b/internal/session/session.go index f497b3728..51af0b988 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -67,10 +67,6 @@ type Session struct { UserId string `json:"user_id,omitempty" gorm:"default:null"` // HostId of the session HostId string `json:"host_id,omitempty" gorm:"default:null"` - // ServerId that proxied the session - ServerId string `json:"server_id,omitempty" gorm:"default:null"` - // ServerType that proxied the session - ServerType string `json:"server_type,omitempty" gorm:"default:null"` // TargetId for the session TargetId string `json:"target_id,omitempty" gorm:"default:null"` // HostSetId for the session @@ -178,8 +174,6 @@ func (s *Session) Clone() interface{} { PublicId: s.PublicId, UserId: s.UserId, HostId: s.HostId, - ServerId: s.ServerId, - ServerType: s.ServerType, TargetId: s.TargetId, HostSetId: s.HostSetId, AuthTokenId: s.AuthTokenId, @@ -353,12 +347,6 @@ func (s *Session) validateNewSession() error { if s.TerminationReason != "" { return errors.NewDeprecated(errors.InvalidParameter, op, "termination reason must be empty") } - if s.ServerId != "" { - return errors.NewDeprecated(errors.InvalidParameter, op, "server id must be empty") - } - if s.ServerType != "" { - return errors.NewDeprecated(errors.InvalidParameter, op, "server type must be empty") - } if s.TofuToken != nil { return errors.NewDeprecated(errors.InvalidParameter, op, "tofu token must be empty") } @@ -465,8 +453,6 @@ type sessionListView struct { PublicId string `json:"public_id,omitempty" gorm:"primary_key"` UserId string `json:"user_id,omitempty" gorm:"default:null"` HostId string `json:"host_id,omitempty" gorm:"default:null"` - ServerId string `json:"server_id,omitempty" gorm:"default:null"` - ServerType string `json:"server_type,omitempty" gorm:"default:null"` TargetId string `json:"target_id,omitempty" gorm:"default:null"` HostSetId string `json:"host_set_id,omitempty" gorm:"default:null"` AuthTokenId string `json:"auth_token_id,omitempty" gorm:"default:null"` diff --git a/internal/session/testing.go b/internal/session/testing.go index 311a50b53..4d7b9aecf 100644 --- a/internal/session/testing.go +++ b/internal/session/testing.go @@ -14,12 +14,10 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/target/tcp" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-secure-stdlib/base62" - "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -88,8 +86,6 @@ func TestSession(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, c Composed _, certBytes, err := newCert(ctx, wrapper, c.UserId, id, []string{"127.0.0.1", "localhost"}, c.ExpirationTime.Timestamp.AsTime()) require.NoError(err) s.Certificate = certBytes - s.ServerId = opts.withServerId - if len(s.TofuToken) != 0 { err = s.encrypt(ctx, wrapper) } @@ -180,33 +176,6 @@ func TestTofu(t testing.TB) []byte { return []byte(tofu) } -// TestWorker inserts a worker into the db to satisfy foreign key constraints. -// Supports the WithServerId option. -func TestWorker(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, opt ...Option) *servers.Server { - t.Helper() - rw := db.New(conn) - kms := kms.TestKms(t, conn, wrapper) - serversRepo, err := servers.NewRepository(rw, rw, kms) - require.NoError(t, err) - - opts := getOpts(opt...) - id := opts.withServerId - if id == "" { - id, err = uuid.GenerateUUID() - require.NoError(t, err) - id = "test-session-worker-" + id - } - worker := &servers.Server{ - PrivateId: id, - Type: servers.ServerTypeWorker.String(), - Description: "Test Session Worker", - Address: "127.0.0.1", - } - _, _, err = serversRepo.UpsertServer(context.Background(), worker) - require.NoError(t, err) - return worker -} - // TestCert is a temporary test func that intentionally doesn't take testing.T // as a parameter. It's currently used in controller.jobTestingHandler() and // should be deprecated once that function is refactored to use sessions properly. diff --git a/internal/session/testing_test.go b/internal/session/testing_test.go index 8e18df86a..1403a3296 100644 --- a/internal/session/testing_test.go +++ b/internal/session/testing_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/servers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -73,7 +74,7 @@ func Test_TestWorker(t *testing.T) { require := require.New(t) conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) - w := TestWorker(t, conn, wrapper) + w := servers.TestKmsWorker(t, conn, wrapper) require.NotNil(w) } diff --git a/internal/tests/api/sessions/classification_test.go b/internal/tests/api/sessions/classification_test.go index 3136a44e9..8ee4e56d5 100644 --- a/internal/tests/api/sessions/classification_test.go +++ b/internal/tests/api/sessions/classification_test.go @@ -66,12 +66,7 @@ func TestSessions(t *testing.T) { EndTime: pbNow, }, }, - Status: "status", - WorkerInfo: []*pb.WorkerInfo{ - { - Address: "worker-address", - }, - }, + Status: "status", Certificate: []byte("certificate"), TerminationReason: "termination_reason", AuthorizedActions: []string{"action-1"}, @@ -119,12 +114,7 @@ func TestSessions(t *testing.T) { EndTime: pbNow, }, }, - Status: "status", - WorkerInfo: []*pb.WorkerInfo{ - { - Address: "worker-address", - }, - }, + Status: "status", Certificate: []byte("certificate"), TerminationReason: "termination_reason", AuthorizedActions: []string{"action-1"}, diff --git a/internal/tests/cluster/ipv6_listener_test.go b/internal/tests/cluster/ipv6_listener_test.go index 9de82f5d6..3cca5b714 100644 --- a/internal/tests/cluster/ipv6_listener_test.go +++ b/internal/tests/cluster/ipv6_listener_test.go @@ -45,10 +45,10 @@ func TestIPv6Listener(t *testing.T) { require.NoError(err) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: wconf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + Config: wconf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() diff --git a/internal/tests/cluster/multi_controller_worker_test.go b/internal/tests/cluster/multi_controller_worker_test.go index 98b040d63..df6c023b9 100644 --- a/internal/tests/cluster/multi_controller_worker_test.go +++ b/internal/tests/cluster/multi_controller_worker_test.go @@ -35,9 +35,9 @@ func TestMultiControllerMultiWorkerConnections(t *testing.T) { expectWorkers(t, c2) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() @@ -60,9 +60,9 @@ func TestMultiControllerMultiWorkerConnections(t *testing.T) { expectWorkers(t, c2, w2) w1 = worker.NewTestWorker(t, &worker.TestWorkerOpts{ - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() diff --git a/internal/tests/cluster/session_cleanup_test.go b/internal/tests/cluster/session_cleanup_test.go index 150f474ce..ad6739430 100644 --- a/internal/tests/cluster/session_cleanup_test.go +++ b/internal/tests/cluster/session_cleanup_test.go @@ -109,7 +109,7 @@ func testWorkerSessionCleanupSingle(burdenCase timeoutBurdenType) func(t *testin w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: []string{proxy.ListenerAddr()}, + InitialUpstreams: []string{proxy.ListenerAddr()}, Logger: logger.Named("w1"), StatusGracePeriodDuration: workerGracePeriod(burdenCase), }) @@ -269,7 +269,7 @@ func testWorkerSessionCleanupMulti(burdenCase timeoutBurdenType) func(t *testing // ************ w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: []string{p1.ListenerAddr(), p2.ListenerAddr()}, + InitialUpstreams: []string{p1.ListenerAddr(), p2.ListenerAddr()}, Logger: logger.Named("w1"), StatusGracePeriodDuration: workerGracePeriod(burdenCase), }) diff --git a/internal/tests/cluster/unix_listener_test.go b/internal/tests/cluster/unix_listener_test.go index 9fa104bf4..97a0b9f80 100644 --- a/internal/tests/cluster/unix_listener_test.go +++ b/internal/tests/cluster/unix_listener_test.go @@ -58,10 +58,10 @@ func TestUnixListener(t *testing.T) { require.NoError(err) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: wconf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + Config: wconf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() diff --git a/internal/tests/cluster/worker_replay_test.go b/internal/tests/cluster/worker_replay_test.go index ff2458e80..a5a1fd2c6 100644 --- a/internal/tests/cluster/worker_replay_test.go +++ b/internal/tests/cluster/worker_replay_test.go @@ -37,9 +37,9 @@ func TestWorkerReplay(t *testing.T) { conf.Eventing = &ec.EventerConfig require.NoError(t, err) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), NonceFn: func(length int) (string, error) { return "test_noncetest_nonce", nil }, @@ -63,9 +63,9 @@ func TestWorkerReplay(t *testing.T) { // Now, start up again with the same nonce w1 = worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), NonceFn: func(length int) (string, error) { return "test_noncetest_nonce", nil }, diff --git a/internal/tests/cluster/worker_tagging_test.go b/internal/tests/cluster/worker_tagging_test.go index 8c217b6e1..540f1d2f0 100644 --- a/internal/tests/cluster/worker_tagging_test.go +++ b/internal/tests/cluster/worker_tagging_test.go @@ -47,16 +47,16 @@ func TestWorkerTagging(t *testing.T) { // Worker 1 conf, err = config.DevWorker() require.NoError(t, err) - conf.Worker.Name = "w1" + conf.Worker.Name = "test_worker_1" conf.Worker.Tags = map[string][]string{ "region": {"east"}, "foo": {"bar"}, } w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) defer w1.Shutdown() w1Addr := w1.ProxyAddrs()[0] @@ -64,16 +64,16 @@ func TestWorkerTagging(t *testing.T) { // Worker 2 conf, err = config.DevWorker() require.NoError(t, err) - conf.Worker.Name = "w2" + conf.Worker.Name = "test_worker_2" conf.Worker.Tags = map[string][]string{ "region": {"west"}, "az": {"one", "two", "three"}, } w2 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w2"), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w2"), }) defer w2.Shutdown() w2Addr := w2.ProxyAddrs()[0] @@ -81,16 +81,16 @@ func TestWorkerTagging(t *testing.T) { // Worker 3 conf, err = config.DevWorker() require.NoError(t, err) - conf.Worker.Name = "w3" + conf.Worker.Name = "test_worker_3" conf.Worker.Tags = map[string][]string{ "region": {"west"}, "az": {"one", "three"}, } w3 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - Config: conf, - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w3"), + Config: conf, + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w3"), }) defer w3.Shutdown() w3Addr := w3.ProxyAddrs()[0] @@ -109,12 +109,12 @@ func TestWorkerTagging(t *testing.T) { }, { name: "name and region", - filter: `"/name" matches "w[13]" and "west" in "/tags/region"`, + filter: `"/name" matches "test_worker_[13]" and "west" in "/tags/region"`, expWorkers: []string{w3Addr}, }, { name: "name and az", - filter: `"/name" matches "w[23]" and "three" in "/tags/az"`, + filter: `"/name" matches "test_worker_[23]" and "three" in "/tags/az"`, expWorkers: []string{w2Addr, w3Addr}, }, { diff --git a/internal/tests/cluster/x509_verification_test.go b/internal/tests/cluster/x509_verification_test.go index bf6aeca78..2aa142e52 100644 --- a/internal/tests/cluster/x509_verification_test.go +++ b/internal/tests/cluster/x509_verification_test.go @@ -49,9 +49,9 @@ func TestCustomX509Verification_Client(t *testing.T) { conf.Eventing = &ec.EventerConfig req.NoError(err) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), - Logger: logger.Named("w1"), + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), + Logger: logger.Named("w1"), }) t.Cleanup(w1.Shutdown) @@ -188,8 +188,8 @@ func testCustomX509Verification_Server(ec event.TestConfig, certPool *x509.CertP conf.Eventing = &ec.EventerConfig req.NoError(err) w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{ - WorkerAuthKms: c1.Config().WorkerAuthKms, - InitialControllers: c1.ClusterAddrs(), + WorkerAuthKms: c1.Config().WorkerAuthKms, + InitialUpstreams: c1.ClusterAddrs(), }) w1.Worker().TestOverrideX509VerifyCertPool = certPool w1.Worker().TestOverrideX509VerifyDnsName = dnsName diff --git a/internal/tests/helper/testing_helper.go b/internal/tests/helper/testing_helper.go index 4fab08943..24b9cb602 100644 --- a/internal/tests/helper/testing_helper.go +++ b/internal/tests/helper/testing_helper.go @@ -34,7 +34,7 @@ import ( const ( DefaultGracePeriod = time.Second * 15 expectConnectionStateOnControllerTimeout = time.Minute * 2 - expectConnectionStateOnWorkerTimeout = DefaultGracePeriod * 2 + expectConnectionStateOnWorkerTimeout = DefaultGracePeriod * 3 // This is the interval that we check states on in the worker. It // needs to be particularly granular to ensure that we allow for @@ -70,7 +70,7 @@ func NewTestSession( ) *TestSession { t.Helper() require := require.New(t) - sar, err := tcl.AuthorizeSession(ctx, "ttcp_1234567890") + sar, err := tcl.AuthorizeSession(ctx, targetId) require.NoError(err) require.NotNil(sar) diff --git a/internal/types/action/action.go b/internal/types/action/action.go index f93b86c22..900da72f1 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -1,12 +1,12 @@ package action -import "strings" +import ( + "fmt" + "strings" +) -// Type defines a type for the Actions of Resources -// actions are also stored as a lookup db table named iam_action type Type uint -// not using iota intentionally, since the values are stored in the db as well. const ( Unknown Type = 0 List Type = 1 @@ -53,6 +53,7 @@ const ( AddHostSources Type = 42 SetHostSources Type = 43 RemoveHostSources Type = 44 + CreateWorkerLed Type = 45 // When adding new actions, be sure to update: // @@ -104,6 +105,7 @@ var Map = map[string]Type{ AddHostSources.String(): AddHostSources, SetHostSources.String(): SetHostSources, RemoveHostSources.String(): RemoveHostSources, + CreateWorkerLed.String(): CreateWorkerLed, } func (a Type) String() string { @@ -153,9 +155,20 @@ func (a Type) String() string { "add-host-sources", "set-host-sources", "remove-host-sources", + "create:worker-led", }[a] } +// IsActionOrParent tests whether the given action is either the same as the +// suspect or is a parent of the suspect. This is used in some parts of ACL +// checking. +func (act Type) IsActionOrParent(suspect Type) bool { + if act == suspect { + return true + } + return strings.HasPrefix(suspect.String(), fmt.Sprintf("%s:", act.String())) +} + // ActionSet stores a slice of action types type ActionSet []Type diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index c9de61572..e644d3976 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -95,6 +95,10 @@ func TestAction(t *testing.T) { action: NoOp, want: "no-op", }, + { + action: CreateWorkerLed, + want: "create:worker-led", + }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { @@ -216,3 +220,40 @@ func TestOnlySelf(t *testing.T) { }) } } + +func TestIsActionOrParent(t *testing.T) { + tests := []struct { + name string + base Type + comp Type + want bool + }{ + { + name: "same", + base: Cancel, + comp: Cancel, + want: true, + }, + { + name: "different", + base: Cancel, + comp: Read, + }, + { + name: "different base and comp", + base: List, + comp: CancelSelf, + }, + { + name: "same base and comp", + base: Cancel, + comp: CancelSelf, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.base.IsActionOrParent(tt.comp)) + }) + } +} diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index 4d30ab4a2..84fca300e 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -29,7 +29,7 @@ const ( // NOTE: When adding a new type, be sure to update: // // * The Grant.validateType function and test - // * The perms.topLevelTypes function + // * The perms.topLevelType function // * The scopes service collection actions for appropriate scopes // * The Test_AnonRestrictions test ) diff --git a/sdk/pbs/controller/api/resources/sessions/session.pb.go b/sdk/pbs/controller/api/resources/sessions/session.pb.go index 9b50dd7fd..09e092cea 100644 --- a/sdk/pbs/controller/api/resources/sessions/session.pb.go +++ b/sdk/pbs/controller/api/resources/sessions/session.pb.go @@ -22,54 +22,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type WorkerInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The address of the worker. - Address string `protobuf:"bytes,10,opt,name=address,proto3" json:"address,omitempty" class:"public"` // @gotags: `class:"public"` -} - -func (x *WorkerInfo) Reset() { - *x = WorkerInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *WorkerInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WorkerInfo) ProtoMessage() {} - -func (x *WorkerInfo) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WorkerInfo.ProtoReflect.Descriptor instead. -func (*WorkerInfo) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{0} -} - -func (x *WorkerInfo) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - type SessionState struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -86,7 +38,7 @@ type SessionState struct { func (x *SessionState) Reset() { *x = SessionState{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[1] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -99,7 +51,7 @@ func (x *SessionState) String() string { func (*SessionState) ProtoMessage() {} func (x *SessionState) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[1] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -112,7 +64,7 @@ func (x *SessionState) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionState.ProtoReflect.Descriptor instead. func (*SessionState) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{1} + return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{0} } func (x *SessionState) GetStatus() string { @@ -161,7 +113,7 @@ type Connection struct { func (x *Connection) Reset() { *x = Connection{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[2] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -174,7 +126,7 @@ func (x *Connection) String() string { func (*Connection) ProtoMessage() {} func (x *Connection) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[2] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -187,7 +139,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message { // Deprecated: Use Connection.ProtoReflect.Descriptor instead. func (*Connection) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{2} + return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{1} } func (x *Connection) GetClientTcpAddress() string { @@ -277,8 +229,6 @@ type Session struct { States []*SessionState `protobuf:"bytes,170,rep,name=states,proto3" json:"states,omitempty"` // Output only. The current status of this Session. Status string `protobuf:"bytes,180,opt,name=status,proto3" json:"status,omitempty" class:"public"` // @gotags: `class:"public"` - // Output only. Worker information given to the client. - WorkerInfo []*WorkerInfo `protobuf:"bytes,190,rep,name=worker_info,proto3" json:"worker_info,omitempty"` // Output only. The certificate generated for the session. Raw DER bytes. Certificate []byte `protobuf:"bytes,200,opt,name=certificate,proto3" json:"certificate,omitempty" class:"public"` // @gotags: `class:"public"` // Output only. If the session is terminated, this provides a short description as to why. @@ -292,7 +242,7 @@ type Session struct { func (x *Session) Reset() { *x = Session{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[3] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -305,7 +255,7 @@ func (x *Session) String() string { func (*Session) ProtoMessage() {} func (x *Session) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[3] + mi := &file_controller_api_resources_sessions_v1_session_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -318,7 +268,7 @@ func (x *Session) ProtoReflect() protoreflect.Message { // Deprecated: Use Session.ProtoReflect.Descriptor instead. func (*Session) Descriptor() ([]byte, []int) { - return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{3} + return file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP(), []int{2} } func (x *Session) GetId() string { @@ -433,13 +383,6 @@ func (x *Session) GetStatus() string { return "" } -func (x *Session) GetWorkerInfo() []*WorkerInfo { - if x != nil { - return x.WorkerInfo - } - return nil -} - func (x *Session) GetCertificate() []byte { if x != nil { return x.Certificate @@ -481,98 +424,91 @@ var file_controller_api_resources_sessions_v1_session_proto_rawDesc = []byte{ 0x72, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x26, 0x0a, 0x0a, 0x57, - 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3a, 0x0a, 0x0a, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x22, 0x9f, 0x02, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x2c, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, - 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x63, 0x70, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x0f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x50, - 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x55, 0x70, 0x12, 0x1d, - 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x23, 0x0a, - 0x0d, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x22, 0xbd, 0x07, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, - 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, - 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x44, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x01, 0x0a, 0x0c, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x6f, 0x73, - 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x8c, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, 0x6f, 0x73, 0x74, - 0x5f, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x96, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, - 0x12, 0x1b, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0xa0, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4b, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0xaa, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, - 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, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0xb4, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x53, 0x0a, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x6e, - 0x66, 0x6f, 0x18, 0xbe, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 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, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x77, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0xc8, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x12, 0x74, - 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x18, 0xd2, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x12, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x53, 0x0a, - 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xb6, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x36, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, + 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x9f, 0x02, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x74, 0x63, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, + 0x14, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x2a, 0x0a, 0x11, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x63, 0x70, 0x5f, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x65, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x63, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x55, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x64, 0x6f, 0x77, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x5f, + 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, + 0x6f, 0x73, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xf5, 0x06, 0x0a, 0x07, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x5a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x44, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, + 0x0d, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x6e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x78, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x12, 0x21, 0x0a, + 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x82, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, + 0x12, 0x19, 0x0a, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x8c, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x96, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0xa0, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, + 0xaa, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 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, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x12, 0x17, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0xb4, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0b, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0xc8, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, + 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x18, 0xd2, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x53, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0xb6, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 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, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x6e, + 0x66, 0x6f, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, @@ -592,30 +528,28 @@ func file_controller_api_resources_sessions_v1_session_proto_rawDescGZIP() []byt return file_controller_api_resources_sessions_v1_session_proto_rawDescData } -var file_controller_api_resources_sessions_v1_session_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_controller_api_resources_sessions_v1_session_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_controller_api_resources_sessions_v1_session_proto_goTypes = []interface{}{ - (*WorkerInfo)(nil), // 0: controller.api.resources.sessions.v1.WorkerInfo - (*SessionState)(nil), // 1: controller.api.resources.sessions.v1.SessionState - (*Connection)(nil), // 2: controller.api.resources.sessions.v1.Connection - (*Session)(nil), // 3: controller.api.resources.sessions.v1.Session - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*scopes.ScopeInfo)(nil), // 5: controller.api.resources.scopes.v1.ScopeInfo + (*SessionState)(nil), // 0: controller.api.resources.sessions.v1.SessionState + (*Connection)(nil), // 1: controller.api.resources.sessions.v1.Connection + (*Session)(nil), // 2: controller.api.resources.sessions.v1.Session + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp + (*scopes.ScopeInfo)(nil), // 4: controller.api.resources.scopes.v1.ScopeInfo } var file_controller_api_resources_sessions_v1_session_proto_depIdxs = []int32{ - 4, // 0: controller.api.resources.sessions.v1.SessionState.start_time:type_name -> google.protobuf.Timestamp - 4, // 1: controller.api.resources.sessions.v1.SessionState.end_time:type_name -> google.protobuf.Timestamp - 5, // 2: controller.api.resources.sessions.v1.Session.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 4, // 3: controller.api.resources.sessions.v1.Session.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.sessions.v1.Session.updated_time:type_name -> google.protobuf.Timestamp - 4, // 5: controller.api.resources.sessions.v1.Session.expiration_time:type_name -> google.protobuf.Timestamp - 1, // 6: controller.api.resources.sessions.v1.Session.states:type_name -> controller.api.resources.sessions.v1.SessionState - 0, // 7: controller.api.resources.sessions.v1.Session.worker_info:type_name -> controller.api.resources.sessions.v1.WorkerInfo - 2, // 8: controller.api.resources.sessions.v1.Session.connections:type_name -> controller.api.resources.sessions.v1.Connection - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] 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 + 3, // 0: controller.api.resources.sessions.v1.SessionState.start_time:type_name -> google.protobuf.Timestamp + 3, // 1: controller.api.resources.sessions.v1.SessionState.end_time:type_name -> google.protobuf.Timestamp + 4, // 2: controller.api.resources.sessions.v1.Session.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 3, // 3: controller.api.resources.sessions.v1.Session.created_time:type_name -> google.protobuf.Timestamp + 3, // 4: controller.api.resources.sessions.v1.Session.updated_time:type_name -> google.protobuf.Timestamp + 3, // 5: controller.api.resources.sessions.v1.Session.expiration_time:type_name -> google.protobuf.Timestamp + 0, // 6: controller.api.resources.sessions.v1.Session.states:type_name -> controller.api.resources.sessions.v1.SessionState + 1, // 7: controller.api.resources.sessions.v1.Session.connections:type_name -> controller.api.resources.sessions.v1.Connection + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_api_resources_sessions_v1_session_proto_init() } @@ -625,18 +559,6 @@ func file_controller_api_resources_sessions_v1_session_proto_init() { } if !protoimpl.UnsafeEnabled { file_controller_api_resources_sessions_v1_session_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkerInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_controller_api_resources_sessions_v1_session_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SessionState); i { case 0: return &v.state @@ -648,7 +570,7 @@ func file_controller_api_resources_sessions_v1_session_proto_init() { return nil } } - file_controller_api_resources_sessions_v1_session_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_controller_api_resources_sessions_v1_session_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Connection); i { case 0: return &v.state @@ -660,7 +582,7 @@ func file_controller_api_resources_sessions_v1_session_proto_init() { return nil } } - file_controller_api_resources_sessions_v1_session_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_controller_api_resources_sessions_v1_session_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Session); i { case 0: return &v.state @@ -679,7 +601,7 @@ func file_controller_api_resources_sessions_v1_session_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_sessions_v1_session_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/pbs/controller/api/resources/workers/worker.pb.go b/sdk/pbs/controller/api/resources/workers/worker.pb.go new file mode 100644 index 000000000..1091456e8 --- /dev/null +++ b/sdk/pbs/controller/api/resources/workers/worker.pb.go @@ -0,0 +1,405 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc (unknown) +// source: controller/api/resources/workers/v1/worker.proto + +package workers + +import ( + scopes "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Worker contains all fields related to a Worker resource +type Worker struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Output only. The ID of the User. + Id string `protobuf:"bytes,10,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` + // The ID of the Scope this resource is in. + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. Scope information for this resource. + Scope *scopes.ScopeInfo `protobuf:"bytes,30,opt,name=scope,proto3" json:"scope,omitempty"` + // Optional name for identification purposes. Can only be set through the API + // for `pki`-type workers; read-only for `kms`-type workers. + Name *wrapperspb.StringValue `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Optional user-set description for identification purposes. Can only be set + // through the API for `pki`-type workers; read-only for `kms`-type workers. + Description *wrapperspb.StringValue `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Output only. The time this resource was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,60,opt,name=created_time,proto3" json:"created_time,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The time this resource was last updated. + UpdatedTime *timestamppb.Timestamp `protobuf:"bytes,70,opt,name=updated_time,proto3" json:"updated_time,omitempty" class:"public"` // @gotags: `class:"public"` + // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + Version uint32 `protobuf:"varint,80,opt,name=version,proto3" json:"version,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The address that this worker is reachable at, as sourced from + // the worker's configuration file. + Address string `protobuf:"bytes,90,opt,name=address,proto3" json:"address,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The deduplicated union of the tags reported by the worker from + // its configuration and any tags added through other means. This is used + // when applying worker filters. + CanonicalTags map[string]*structpb.ListValue `protobuf:"bytes,120,rep,name=canonical_tags,proto3" json:"canonical_tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3" class:"public"` // @gotags: `class:"public"` + // Output only. The tags set in the worker's configuration file. + ConfigTags map[string]*structpb.ListValue `protobuf:"bytes,130,rep,name=config_tags,proto3" json:"config_tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3" class:"public"` // @gotags: `class:"public"` + // Output only. The time this worker daemon last reported its status. + LastStatusTime *timestamppb.Timestamp `protobuf:"bytes,140,opt,name=last_status_time,proto3" json:"last_status_time,omitempty" class:"public"` // @gotags: `class:"public"` + // worker_generated_auth_token is input only. Supports the worker led node + // enrollment flow where this credentials token is produced by a worker. This + // token is a base58 encoded types.FetchNodeCredentialsRequest from + // https://github.com/hashicorp/nodeenrollment + WorkerGeneratedAuthToken *wrapperspb.StringValue `protobuf:"bytes,150,opt,name=worker_generated_auth_token,proto3" json:"worker_generated_auth_token,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The number of connections that this worker is currently handling. + ActiveConnectionCount *wrapperspb.UInt32Value `protobuf:"bytes,160,opt,name=active_connection_count,proto3" json:"active_connection_count,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The type of the worker, denoted by how it authenticates: `pki` + // or `kms`. + Type string `protobuf:"bytes,170,opt,name=type,proto3" json:"type,omitempty"` + // Output only. The available actions on this resource for the requester. + AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *Worker) Reset() { + *x = Worker{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_workers_v1_worker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Worker) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Worker) ProtoMessage() {} + +func (x *Worker) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_workers_v1_worker_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Worker.ProtoReflect.Descriptor instead. +func (*Worker) Descriptor() ([]byte, []int) { + return file_controller_api_resources_workers_v1_worker_proto_rawDescGZIP(), []int{0} +} + +func (x *Worker) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Worker) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Worker) GetScope() *scopes.ScopeInfo { + if x != nil { + return x.Scope + } + return nil +} + +func (x *Worker) GetName() *wrapperspb.StringValue { + if x != nil { + return x.Name + } + return nil +} + +func (x *Worker) GetDescription() *wrapperspb.StringValue { + if x != nil { + return x.Description + } + return nil +} + +func (x *Worker) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *Worker) GetUpdatedTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedTime + } + return nil +} + +func (x *Worker) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Worker) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Worker) GetCanonicalTags() map[string]*structpb.ListValue { + if x != nil { + return x.CanonicalTags + } + return nil +} + +func (x *Worker) GetConfigTags() map[string]*structpb.ListValue { + if x != nil { + return x.ConfigTags + } + return nil +} + +func (x *Worker) GetLastStatusTime() *timestamppb.Timestamp { + if x != nil { + return x.LastStatusTime + } + return nil +} + +func (x *Worker) GetWorkerGeneratedAuthToken() *wrapperspb.StringValue { + if x != nil { + return x.WorkerGeneratedAuthToken + } + return nil +} + +func (x *Worker) GetActiveConnectionCount() *wrapperspb.UInt32Value { + if x != nil { + return x.ActiveConnectionCount + } + return nil +} + +func (x *Worker) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Worker) GetAuthorizedActions() []string { + if x != nil { + return x.AuthorizedActions + } + return nil +} + +var File_controller_api_resources_workers_v1_worker_proto protoreflect.FileDescriptor + +var file_controller_api_resources_workers_v1_worker_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x23, 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, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xa3, 0x09, 0x0a, 0x06, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x46, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, + 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x66, + 0x0a, 0x0e, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, + 0x18, 0x78, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 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, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x61, 0x67, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, + 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x12, 0x5e, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x82, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 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, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x12, 0x47, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x8c, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, + 0x5f, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x96, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x67, 0x65, 0x6e, 0x65, + 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x12, 0x57, 0x0a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0xa0, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x17, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x13, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0xaa, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f, + 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, + 0x5c, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x61, 0x67, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x59, 0x0a, + 0x0f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x50, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, + 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, + 0x72, 0x73, 0x3b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controller_api_resources_workers_v1_worker_proto_rawDescOnce sync.Once + file_controller_api_resources_workers_v1_worker_proto_rawDescData = file_controller_api_resources_workers_v1_worker_proto_rawDesc +) + +func file_controller_api_resources_workers_v1_worker_proto_rawDescGZIP() []byte { + file_controller_api_resources_workers_v1_worker_proto_rawDescOnce.Do(func() { + file_controller_api_resources_workers_v1_worker_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_resources_workers_v1_worker_proto_rawDescData) + }) + return file_controller_api_resources_workers_v1_worker_proto_rawDescData +} + +var file_controller_api_resources_workers_v1_worker_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_api_resources_workers_v1_worker_proto_goTypes = []interface{}{ + (*Worker)(nil), // 0: controller.api.resources.workers.v1.Worker + nil, // 1: controller.api.resources.workers.v1.Worker.CanonicalTagsEntry + nil, // 2: controller.api.resources.workers.v1.Worker.ConfigTagsEntry + (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 4: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*wrapperspb.UInt32Value)(nil), // 6: google.protobuf.UInt32Value + (*structpb.ListValue)(nil), // 7: google.protobuf.ListValue +} +var file_controller_api_resources_workers_v1_worker_proto_depIdxs = []int32{ + 3, // 0: controller.api.resources.workers.v1.Worker.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 4, // 1: controller.api.resources.workers.v1.Worker.name:type_name -> google.protobuf.StringValue + 4, // 2: controller.api.resources.workers.v1.Worker.description:type_name -> google.protobuf.StringValue + 5, // 3: controller.api.resources.workers.v1.Worker.created_time:type_name -> google.protobuf.Timestamp + 5, // 4: controller.api.resources.workers.v1.Worker.updated_time:type_name -> google.protobuf.Timestamp + 1, // 5: controller.api.resources.workers.v1.Worker.canonical_tags:type_name -> controller.api.resources.workers.v1.Worker.CanonicalTagsEntry + 2, // 6: controller.api.resources.workers.v1.Worker.config_tags:type_name -> controller.api.resources.workers.v1.Worker.ConfigTagsEntry + 5, // 7: controller.api.resources.workers.v1.Worker.last_status_time:type_name -> google.protobuf.Timestamp + 4, // 8: controller.api.resources.workers.v1.Worker.worker_generated_auth_token:type_name -> google.protobuf.StringValue + 6, // 9: controller.api.resources.workers.v1.Worker.active_connection_count:type_name -> google.protobuf.UInt32Value + 7, // 10: controller.api.resources.workers.v1.Worker.CanonicalTagsEntry.value:type_name -> google.protobuf.ListValue + 7, // 11: controller.api.resources.workers.v1.Worker.ConfigTagsEntry.value:type_name -> google.protobuf.ListValue + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name +} + +func init() { file_controller_api_resources_workers_v1_worker_proto_init() } +func file_controller_api_resources_workers_v1_worker_proto_init() { + if File_controller_api_resources_workers_v1_worker_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_api_resources_workers_v1_worker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Worker); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_api_resources_workers_v1_worker_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_api_resources_workers_v1_worker_proto_goTypes, + DependencyIndexes: file_controller_api_resources_workers_v1_worker_proto_depIdxs, + MessageInfos: file_controller_api_resources_workers_v1_worker_proto_msgTypes, + }.Build() + File_controller_api_resources_workers_v1_worker_proto = out.File + file_controller_api_resources_workers_v1_worker_proto_rawDesc = nil + file_controller_api_resources_workers_v1_worker_proto_goTypes = nil + file_controller_api_resources_workers_v1_worker_proto_depIdxs = nil +} diff --git a/testing/dbtest/session_list_benchmarks_dump_generation_test.go b/testing/dbtest/session_list_benchmarks_dump_generation_test.go index 9178c559b..2b7beb032 100644 --- a/testing/dbtest/session_list_benchmarks_dump_generation_test.go +++ b/testing/dbtest/session_list_benchmarks_dump_generation_test.go @@ -129,15 +129,7 @@ func TestGenerateSessionBenchmarkTemplateDumps(t *testing.T) { require.NoError(err) connRepo, err := session.NewConnectionRepository(ctx, rw, rw, kms) require.NoError(err) - serversRepo, err := servers.NewRepository(rw, rw, kms) - require.NoError(err) - worker := &servers.Server{ - PrivateId: "test1", - Type: "worker", - Address: "127.0.0.1", - } - _, _, err = serversRepo.UpsertServer(ctx, worker) - require.NoError(err) + _ = servers.TestKmsWorker(t, conn, wrap) usersStart := time.Now() t.Logf("Populating %d users", scenario.users) @@ -196,7 +188,7 @@ func TestGenerateSessionBenchmarkTemplateDumps(t *testing.T) { ScopeId: users[userIndex].scopeId, Endpoint: "tcp://127.0.0.1:22", }) - cycleSessionStates(t, ctx, sess, sessRepo, connRepo, conn, worker, scenario.connsPerSession) + cycleSessionStates(t, ctx, sess, sessRepo, connRepo, conn, scenario.connsPerSession) return nil }) } @@ -220,8 +212,8 @@ func TestGenerateSessionBenchmarkTemplateDumps(t *testing.T) { } } -func cycleSessionStates(t testing.TB, ctx context.Context, sess *session.Session, sessRepo *session.Repository, connRepo *session.ConnectionRepository, conn *db.DB, worker *servers.Server, numConns int) { - sess, _, err := sessRepo.ActivateSession(ctx, sess.PublicId, sess.Version, worker.PrivateId, worker.Type, []byte(`tofu`)) +func cycleSessionStates(t testing.TB, ctx context.Context, sess *session.Session, sessRepo *session.Repository, connRepo *session.ConnectionRepository, conn *db.DB, numConns int) { + sess, _, err := sessRepo.ActivateSession(ctx, sess.PublicId, sess.Version, []byte(`tofu`)) require.NoError(t, err) var closeWiths []session.CloseWith for i := 0; i < numConns; i++ { diff --git a/website/content/docs/concepts/filtering/events.mdx b/website/content/docs/concepts/filtering/events.mdx index f2397d780..a2e1beb23 100644 --- a/website/content/docs/concepts/filtering/events.mdx +++ b/website/content/docs/concepts/filtering/events.mdx @@ -75,8 +75,8 @@ sink "stderr" = { event_types = ["*"] format = "cloudevents-text" allow_filters = [ - '"/Data/request_info/Path" contains ":authenticate"' - '"/Data/Op" contains ".createClientConn"' + '"/data/request_info/path" contains ":authenticate"' + '"/data/op" contains ".createClientConn"' ] # note: deny_filters are also supported. } @@ -85,8 +85,8 @@ sink "stderr" = { When running `boundary dev` the example allow filter can be given via: ```bash boundary dev \ - -event-allow-filter '"/Data/request_info/Path" contains ":authenticate"' \ - -event-allow-filter '"/Data/Op" contains ".createClientConn"' + -event-allow-filter '"/data/request_info/path" contains ":authenticate"' \ + -event-allow-filter '"/data/op" contains ".createClientConn"' ``` ~> Double quotes are part of the filter syntax; when using the CLI, it is likely easier to surround the filter with single quotes than to deal with escaping diff --git a/website/content/docs/configuration/worker.mdx b/website/content/docs/configuration/worker.mdx index 72e1be5a7..7a4a62c5c 100644 --- a/website/content/docs/configuration/worker.mdx +++ b/website/content/docs/configuration/worker.mdx @@ -36,8 +36,8 @@ worker { an env var (env://) from which the address will be read; or a [go-sockaddr template](https://godoc.org/github.com/hashicorp/go-sockaddr/template). -- `controllers` - A list of hosts/IP addresses and optionally ports for reaching - controllers. The port will default to :9201 if not specified. This value can be +- `initial_upstreams` - A list of hosts/IP addresses and optionally ports for reaching + controllers or workers. The port will default to :9201 if not specified. This value can be a direct access string array with the addresses, or it can refer to a file on disk (file://) from which the addresses will be read, or an env var (env://) from which the addresses will be read. When using env or file, their contents