From 5d527c5e99718677f94651edf80c46379111e1d5 Mon Sep 17 00:00:00 2001 From: Loann Le <84412881+taoism4504@users.noreply.github.com> Date: Thu, 14 Oct 2021 17:03:25 -0700 Subject: [PATCH 01/17] Boundary documentation: created a boundary intro page (#1601) * added an boundary intro page * Update what-is-boundary.mdx deleted an extra space * added an image * Apply suggestions from Robin's code review Co-authored-by: Robin Beck Co-authored-by: Pete Pacent Co-authored-by: Robin Beck --- website/content/docs/what-is-boundary.mdx | 54 ++++++++++++++++++++++ website/data/docs-nav-data.json | 4 ++ website/public/img/boundary.png | Bin 0 -> 178107 bytes 3 files changed, 58 insertions(+) create mode 100644 website/content/docs/what-is-boundary.mdx create mode 100644 website/public/img/boundary.png diff --git a/website/content/docs/what-is-boundary.mdx b/website/content/docs/what-is-boundary.mdx new file mode 100644 index 000000000..c587b18bd --- /dev/null +++ b/website/content/docs/what-is-boundary.mdx @@ -0,0 +1,54 @@ +--- +layout: docs +page_title: What is Boundary? +description: And introduction to Boundary +--- + +# What is Boundary? + +Welcome to Boundary! This introduction section covers what Boundary is, the problem Boundary intends to solve, and how Boundary compares to other software. If you want to dive into using Boundary, head over to the [Getting Started](/docs/getting-started) section. + +Boundary is an intelligent proxy that creates granular, identity-based access controls for dynamic infrastructure. Boundary’s workflow layers security controls and integrations at multiple levels to monitor and manage user access through: + +- Tightly scoped identity-based permissions +- Just-in-time network and credential access for sessions via [HashiCorp Vault](https://vaultproject.io/) +- Single sign-on to target services and applications via external identity providers +- Access-as-code to automate the configuration of user permissions +- Automated discovery of target systems +- Session monitoring and management for access created via Boundary. + +## Boundary Goals + +**Zero Trust Security**: The foundation of Boundary is an identity-based, “Zero-Trust”, access model. Zero-Trust means user access is continuously authenticated and only authorized when rules and policies tied to the user’s identity are verified. + +**A Consistent Workflow for Access**: Boundary connects the user securely to their infrastructure regardless of cloud platform, target environment, or identity provider. Boundary creates a consistent workflow for user authentication and authorization to create secure, ephemeral sessions that can be monitored and managed by administrators. + +![Boundary workflow](/img/boundary.png) + +**Extensibility with the Ecosystem**: Modern organizations have a multilayered matrix of identity providers, policy engines, secrets management tools, target types, and cloud providers that must integrate and reside within the access workflow. Boundary's extensible ecosystem supports the user's vendor-of-choice in this access workflow. + +## Why Boundary? + +With the many varying infrastructure services and tooling used in increasingly dynamic environments, organizations must have secure access to all targets within and beyond their perimeter. + +Boundary provides a simple way for verified users to have secure access to cloud and self-managed infrastructures without exposing your network or the use of managing credentials. Boundary is an open source workflow that enables just-in-time, role-based access for dynamic infrastructure. + +## Boundary Use-Cases + +### Zero Trust access + +Boundary’s access-on-demand workflow securely connects trusted identities to infrastructure services based on granular, admin-defined permission grants. Boundary removes the need to create or store credentials when accessing services. In this way, Boundary can be used to extend or replace many traditional access solutions like VPNs. + +Traditional access solutions do not employ the the Zero-Trust philosophy - meaning they fail to authenticate and authorize users’ access and actions continuously, and often lack granular access controls. + +### Multi-Cloud Access + +Having different access workflows for various infrastructure and identity providers doesn’t scale well for administrators or users. Multi-cloud organizations have a growing need to standardize access through a single workflow. Boundary creates a centralized layer of identity-based authentication and authorization to manage access to the infrastructure, regardless of the platform on which it resides. + +### Single Sign-on with Integrated Secrets Management + +Boundary enables a single sign-on access model with authentication from trusted identity providers, such as Azure Active Directory, Auth0, and Okta. Once authenticated, users may create sessions with integrated credential management from HashiCorp Vault without the need to re-authenticate. + +### Session Monitoring + +Boundary provides session monitoring capabilities that give security administrators visibility into user access. Sessions are logged and consumable via the Boundary administrator UI as well as business intelligence (BI) and security information and event management (SIEM) tools. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index f7164abb7..8da8f03a4 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -1,4 +1,8 @@ [ + { + "title": "What is Boundary?", + "path": "what-is-boundary" + }, { "title": "Getting Started", "routes": [ diff --git a/website/public/img/boundary.png b/website/public/img/boundary.png new file mode 100644 index 0000000000000000000000000000000000000000..23597cdb5b3c6db0bebeefb85a61d93cfc5dbb99 GIT binary patch literal 178107 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 z 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�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

qk#nug{ki0^7|AK3mtASVBQI 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 Date: Thu, 23 Sep 2021 10:57:14 -0400 Subject: [PATCH 02/17] chore: Add pre-push hook --- .hooks/pre-push | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 .hooks/pre-push diff --git a/.hooks/pre-push b/.hooks/pre-push new file mode 100755 index 000000000..1cb94652d --- /dev/null +++ b/.hooks/pre-push @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +remote="$1" +remote_url=$(git remote get-url $remote) + +if [[ $remote_url == *"boundary-enterprise"* ]]; then + exit 0 +fi + +if [ -f cmd/boundary/package_registration_ent.go ]; then + echo "Found enterprise file while pushing to oss remote" + exit 1 +fi + +exit 0 From 4b6a16034e7c0af2d07931032e9d01f0a5587527 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 13 Oct 2021 15:09:37 -0400 Subject: [PATCH 03/17] chore: Add ci job to trigger update to enterprise --- .circleci/config.yml | 18 ++++++++++++++++++ .circleci/config/jobs/trigger-merge-to-ent.yml | 7 +++++++ .../config/workflows/trigger-merge-to-ent.yml | 6 ++++++ scripts/trigger-merge-to-ent | 17 +++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 .circleci/config/jobs/trigger-merge-to-ent.yml create mode 100644 .circleci/config/workflows/trigger-merge-to-ent.yml create mode 100755 scripts/trigger-merge-to-ent diff --git a/.circleci/config.yml b/.circleci/config.yml index ca89773db..781db88a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1192,6 +1192,17 @@ jobs: name: Save package cache paths: - .buildcache/packages/store + trigger-merge-to-ent: + machine: + image: ubuntu-2004:202107-02 + resource_class: medium + working_directory: ~/boundary + steps: + - checkout + - run: + command: | + ./scripts/trigger-merge-to-ent + name: Trigger Merge to Enterprise openbsd_amd64_package: docker: - image: docker.mirror.hashicorp.services/circleci/buildpack-deps @@ -1569,4 +1580,11 @@ workflows: branches: only: - stable-website + trigger-merge-to-ent: + jobs: + - trigger-merge-to-ent: + filters: + branches: + only: + - main version: 2 diff --git a/.circleci/config/jobs/trigger-merge-to-ent.yml b/.circleci/config/jobs/trigger-merge-to-ent.yml new file mode 100644 index 000000000..b976eab5c --- /dev/null +++ b/.circleci/config/jobs/trigger-merge-to-ent.yml @@ -0,0 +1,7 @@ +executor: go-machine-medium +steps: +- checkout +- run: + name: "Trigger Merge to Enterprise" + command: | + ./scripts/trigger-merge-to-ent diff --git a/.circleci/config/workflows/trigger-merge-to-ent.yml b/.circleci/config/workflows/trigger-merge-to-ent.yml new file mode 100644 index 000000000..12edc67ce --- /dev/null +++ b/.circleci/config/workflows/trigger-merge-to-ent.yml @@ -0,0 +1,6 @@ +jobs: + - trigger-merge-to-ent: + filters: + branches: + only: + - main diff --git a/scripts/trigger-merge-to-ent b/scripts/trigger-merge-to-ent new file mode 100755 index 000000000..1a6fb7a09 --- /dev/null +++ b/scripts/trigger-merge-to-ent @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [[ -z "${CIRCLE_REPOSITORY_URL}" ]]; then + exit 0 +fi + +if [[ "${CIRCLE_REPOSITORY_URL}" == *"boundary-enterprise"* ]]; then + exit 0 +fi + +if [[ -z ${CIRCLE_CI_ENT_TOK} ]]; then + exit 0 +fi + +curl -u ${CIRCLE_CI_ENT_TOK}: \ + -d 'build_parameters[CIRCLE_JOB]=merge-oss-main' \ + https://circleci.com/api/v1.1/project/github/hashicorp/boundary-enterprise/tree/main From 6fc950537828782afdcf76c2581c3dd84e30f4e1 Mon Sep 17 00:00:00 2001 From: Noel Quiles <3746694+EnMod@users.noreply.github.com> Date: Fri, 15 Oct 2021 16:42:05 -0400 Subject: [PATCH 04/17] website: Upgrade platform analytics package (#1606) --- website/package-lock.json | 99 +++++++++++++++++++++------------------ website/package.json | 3 +- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index a085e664e..561a5be85 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -11,6 +11,7 @@ "@apidevtools/json-schema-ref-parser": "9.0.9", "@hashicorp/mktg-global-styles": "^4.0.0", "@hashicorp/mktg-logos": "^1.2.0", + "@hashicorp/platform-analytics": "^0.2.0", "@hashicorp/platform-code-highlighting": "^0.1.2", "@hashicorp/platform-runtime-error-monitoring": "^0.1.0", "@hashicorp/platform-util": "^0.1.0", @@ -59,6 +60,18 @@ "npm": ">=7.0.0" } }, + "@hashicorp/platform-analytics": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@hashicorp/platform-analytics/-/platform-analytics-0.1.0.tgz", + "integrity": "sha512-kR/E0KWemjazSrSFN9l22v8JqlQvkgjgkZHPJEhGiBt05LMtt2ugxIakv+gnzW5lU9434nnr7oDooUyj4eufPA==", + "extraneous": true + }, + "fathom-client": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.2.0.tgz", + "integrity": "sha512-WF/qA5wXYSuA5K8uiIhGNbErOcTAmfLEWrBxWP2px2dEc9waH9zxjdh9k0F907VCBhdJrv+f7V3HT/Pmro40zA==", + "extraneous": true + }, "node_modules/@algolia/cache-browser-local-storage": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.10.5.tgz", @@ -909,6 +922,17 @@ "node": ">=4" } }, + "node_modules/@hashicorp/platform-analytics": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@hashicorp/platform-analytics/-/platform-analytics-0.2.0.tgz", + "integrity": "sha512-4Pmb4Fy/2eDCZPFu/O4wKK2L5VIqwsXfDPDGX3eA4Dk67ytZ//SXuW9oFtG97ACyW/p1i72EmwoqcrR6usDwtg==", + "dependencies": { + "fathom-client": "^3.2.0" + }, + "peerDependencies": { + "react": ">= 16.x" + } + }, "node_modules/@hashicorp/platform-cli": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@hashicorp/platform-cli/-/platform-cli-1.2.0.tgz", @@ -1614,14 +1638,6 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, - "@hashicorp/platform-analytics": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@hashicorp/platform-analytics/-/platform-analytics-0.1.0.tgz", - "integrity": "sha512-kR/E0KWemjazSrSFN9l22v8JqlQvkgjgkZHPJEhGiBt05LMtt2ugxIakv+gnzW5lU9434nnr7oDooUyj4eufPA==", - "requires": { - "fathom-client": "^3.2.0" - } - }, "node_modules/@hashicorp/react-text-input/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2303,13 +2319,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.2.tgz", "integrity": "sha512-hZuwOlGOwBZADA8EyDYyjx3+4JGIGjSHDHWrmpI7g5rFmQNltjlbaefAbiU5Kk7j3BUSDwt30quJRFv3nyJQ0w==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 10" } @@ -2318,13 +2330,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.2.tgz", "integrity": "sha512-PGOp0E1GisU+EJJlsmJVGE+aPYD0Uh7zqgsrpD3F/Y3766Ptfbe1lEPPWnRDl+OzSSrSrX1lkyM/Jlmh5OwNvA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 10" } @@ -2333,13 +2341,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.2.tgz", "integrity": "sha512-YcDHTJjn/8RqvyJVB6pvEKXihDcdrOwga3GfMv/QtVeLphTouY4BIcEUfrG5+26Nf37MP1ywN3RRl1TxpurAsQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 10" } @@ -2348,13 +2352,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.2.tgz", "integrity": "sha512-e/pIKVdB+tGQYa1cW3sAeHm8gzEri/HYLZHT4WZojrUxgWXqx8pk7S7Xs47uBcFTqBDRvK3EcQpPLf3XdVsDdg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">= 10" } @@ -5664,9 +5664,7 @@ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "devOptional": true, - "engines": [ - "node >= 0.8" - ], + "engines": ["node >= 0.8"], "peer": true, "dependencies": { "buffer-from": "^1.0.0", @@ -7572,11 +7570,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "fathom-client": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.2.0.tgz", - "integrity": "sha512-WF/qA5wXYSuA5K8uiIhGNbErOcTAmfLEWrBxWP2px2dEc9waH9zxjdh9k0F907VCBhdJrv+f7V3HT/Pmro40zA==" - }, "node_modules/eslint/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -8303,6 +8296,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fathom-client": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.2.0.tgz", + "integrity": "sha512-WF/qA5wXYSuA5K8uiIhGNbErOcTAmfLEWrBxWP2px2dEc9waH9zxjdh9k0F907VCBhdJrv+f7V3HT/Pmro40zA==" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -8771,9 +8769,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -19476,9 +19472,7 @@ "dev": true, "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "peer": true, "dependencies": { "bindings": "^1.5.0", @@ -21006,6 +21000,14 @@ } } }, + "@hashicorp/platform-analytics": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@hashicorp/platform-analytics/-/platform-analytics-0.2.0.tgz", + "integrity": "sha512-4Pmb4Fy/2eDCZPFu/O4wKK2L5VIqwsXfDPDGX3eA4Dk67ytZ//SXuW9oFtG97ACyW/p1i72EmwoqcrR6usDwtg==", + "requires": { + "fathom-client": "^3.2.0" + } + }, "@hashicorp/platform-cli": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@hashicorp/platform-cli/-/platform-cli-1.2.0.tgz", @@ -26952,6 +26954,11 @@ "reusify": "^1.0.4" } }, + "fathom-client": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.2.0.tgz", + "integrity": "sha512-WF/qA5wXYSuA5K8uiIhGNbErOcTAmfLEWrBxWP2px2dEc9waH9zxjdh9k0F907VCBhdJrv+f7V3HT/Pmro40zA==" + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", diff --git a/website/package.json b/website/package.json index 58a842a6e..c2b6cd3ff 100644 --- a/website/package.json +++ b/website/package.json @@ -7,7 +7,7 @@ "@apidevtools/json-schema-ref-parser": "9.0.9", "@hashicorp/mktg-global-styles": "^4.0.0", "@hashicorp/mktg-logos": "^1.2.0", - "@hashicorp/platform-analytics": "^0.1.0", + "@hashicorp/platform-analytics": "^0.2.0", "@hashicorp/platform-code-highlighting": "^0.1.2", "@hashicorp/platform-runtime-error-monitoring": "^0.1.0", "@hashicorp/platform-util": "^0.1.0", @@ -33,7 +33,6 @@ "classnames": "2.2.6", "next": "^11.1.2", "next-mdx-remote": "^3.0.4", - "fathom-client": "^3.2.0", "next-remote-watch": "1.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", From 51812ff77937a3e5e6873995b07ae5400965ac7c Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 19 Oct 2021 09:37:32 -0400 Subject: [PATCH 05/17] docs(target): Mark additional fields as output only (#1609) When creating or updating a target, the following fields should have been marked as output only: - host_set_ids - host_source_ids - application_credential_source_ids - application_credential_library_ids This updates the protobuf and generated files including the swagger file used by the docs site to correctly mark these are output only. Fixes #1588 --- internal/gen/controller.swagger.json | 12 ++++++++---- .../controller/api/resources/targets/v1/target.proto | 10 +++++----- .../controller/api/resources/targets/target.pb.go | 8 ++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index e3eb1d608..ee2a1d62c 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -4981,7 +4981,8 @@ "items": { "type": "string" }, - "description": "The IDs of the Host Sets associated with this Target." + "description": "Output only. The IDs of the Host Sets associated with this Target.", + "readOnly": true }, "host_sets": { "type": "array", @@ -4996,7 +4997,8 @@ "items": { "type": "string" }, - "description": "The IDs of the Host Sources associated with this Target." + "description": "Output only. The IDs of the Host Sources associated with this Target.", + "readOnly": true }, "host_sources": { "type": "array", @@ -5025,7 +5027,8 @@ "items": { "type": "string" }, - "description": "The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead." + "description": "Output only. The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead.", + "readOnly": true }, "application_credential_libraries": { "type": "array", @@ -5040,7 +5043,8 @@ "items": { "type": "string" }, - "description": "The IDs of the application credential source ids associated with this Target." + "description": "Output only. The IDs of the application credential source ids associated with this Target.", + "readOnly": true }, "application_credential_sources": { "type": "array", diff --git a/internal/proto/local/controller/api/resources/targets/v1/target.proto b/internal/proto/local/controller/api/resources/targets/v1/target.proto index 82aaf87c5..368995f3f 100644 --- a/internal/proto/local/controller/api/resources/targets/v1/target.proto +++ b/internal/proto/local/controller/api/resources/targets/v1/target.proto @@ -112,13 +112,13 @@ message Target { // The type of the Target. string type = 90; - // The IDs of the Host Sets associated with this Target. + // Output only. The IDs of the Host Sets associated with this Target. repeated string host_set_ids = 100 [json_name="host_set_ids"]; // Output only. The Host Sets associated with this Target. repeated HostSet host_sets = 110 [json_name="host_sets"]; - // The IDs of the Host Sources associated with this Target. + // Output only. The IDs of the Host Sources associated with this Target. repeated string host_source_ids = 420 [json_name="host_source_ids"]; // Output only. The Host Sources associated with this Target. @@ -133,12 +133,12 @@ message Target { // Optional boolean expression to filter the workers that are allowed to satisfy this request. google.protobuf.StringValue worker_filter = 140 [json_name="worker_filter", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this: "worker_filter" that: "WorkerFilter"}]; - // The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead. + // Output only. The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead. repeated string application_credential_library_ids = 150 [json_name="application_credential_library_ids", deprecated = true]; // Output only. The application credential libraries associated with this Target. Deprecated: use application_credential_sources instead. repeated CredentialLibrary application_credential_libraries = 180 [json_name="application_credential_libraries", deprecated = true]; - // The IDs of the application credential source ids associated with this Target. + // Output only. The IDs of the application credential source ids associated with this Target. repeated string application_credential_source_ids = 400 [json_name="application_credential_source_ids"]; // Output only. The application credential sources associated with this Target. repeated CredentialSource application_credential_sources = 410 [json_name="application_credential_sources"]; @@ -238,4 +238,4 @@ message SessionAuthorization { // Output only. The credentials for this session. repeated SessionCredential credentials = 110 [json_name="credentials"]; -} \ No newline at end of file +} diff --git a/sdk/pbs/controller/api/resources/targets/target.pb.go b/sdk/pbs/controller/api/resources/targets/target.pb.go index b099979fb..cf0c3a283 100644 --- a/sdk/pbs/controller/api/resources/targets/target.pb.go +++ b/sdk/pbs/controller/api/resources/targets/target.pb.go @@ -461,11 +461,11 @@ type Target struct { Version uint32 `protobuf:"varint,80,opt,name=version,proto3" json:"version,omitempty"` // The type of the Target. Type string `protobuf:"bytes,90,opt,name=type,proto3" json:"type,omitempty"` - // The IDs of the Host Sets associated with this Target. + // Output only. The IDs of the Host Sets associated with this Target. HostSetIds []string `protobuf:"bytes,100,rep,name=host_set_ids,proto3" json:"host_set_ids,omitempty"` // Output only. The Host Sets associated with this Target. HostSets []*HostSet `protobuf:"bytes,110,rep,name=host_sets,proto3" json:"host_sets,omitempty"` - // The IDs of the Host Sources associated with this Target. + // Output only. The IDs of the Host Sources associated with this Target. HostSourceIds []string `protobuf:"bytes,420,rep,name=host_source_ids,proto3" json:"host_source_ids,omitempty"` // Output only. The Host Sources associated with this Target. HostSources []*HostSource `protobuf:"bytes,430,rep,name=host_sources,proto3" json:"host_sources,omitempty"` @@ -475,7 +475,7 @@ type Target struct { SessionConnectionLimit *wrapperspb.Int32Value `protobuf:"bytes,130,opt,name=session_connection_limit,proto3" json:"session_connection_limit,omitempty"` // Optional boolean expression to filter the workers that are allowed to satisfy this request. WorkerFilter *wrapperspb.StringValue `protobuf:"bytes,140,opt,name=worker_filter,proto3" json:"worker_filter,omitempty"` - // The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead. + // Output only. The IDs of the application credential library ids associated with this Target. Deprecated: use application_credential_source_ids instead. // // Deprecated: Do not use. ApplicationCredentialLibraryIds []string `protobuf:"bytes,150,rep,name=application_credential_library_ids,proto3" json:"application_credential_library_ids,omitempty"` @@ -483,7 +483,7 @@ type Target struct { // // Deprecated: Do not use. ApplicationCredentialLibraries []*CredentialLibrary `protobuf:"bytes,180,rep,name=application_credential_libraries,proto3" json:"application_credential_libraries,omitempty"` - // The IDs of the application credential source ids associated with this Target. + // Output only. The IDs of the application credential source ids associated with this Target. ApplicationCredentialSourceIds []string `protobuf:"bytes,400,rep,name=application_credential_source_ids,proto3" json:"application_credential_source_ids,omitempty"` // Output only. The application credential sources associated with this Target. ApplicationCredentialSources []*CredentialSource `protobuf:"bytes,410,rep,name=application_credential_sources,proto3" json:"application_credential_sources,omitempty"` From b48f11dddbaae5eaf9f24df4dc38759ea74804bb Mon Sep 17 00:00:00 2001 From: Brandon Romano Date: Tue, 19 Oct 2021 06:55:38 -0700 Subject: [PATCH 06/17] Prep Banner for HashiConf (#1610) --- website/data/alert-banner.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/website/data/alert-banner.js b/website/data/alert-banner.js index 2fe3f3bad..fe3e5abb5 100644 --- a/website/data/alert-banner.js +++ b/website/data/alert-banner.js @@ -2,12 +2,11 @@ export const ALERT_BANNER_ACTIVE = true // https://github.com/hashicorp/web-components/tree/master/packages/alert-banner export default { - tag: 'Oct 19-21', - url: - 'https://hashiconf.com/global/?utm_campaign=22Q3_WW_HASHICONFGLOBAL_EVENT-USER&utm_source=CorpBanner&utm_medium=EVT&utm_offer=EVENT-USER', - text: 'The countdown to HashiConf Global is on. View the full schedule now.', - linkText: 'View Schedule', - // Set the expirationDate prop with a datetime string (e.g. '2020-01-31T12:00:00-07:00') - // if you'd like the component to stop showing at or after a certain date - expirationDate: '2021-10-19T12:00:00-07:00', + tag: 'Oct 19-21', + url: 'https://hashiconf.com/global/?utm_source=DocsBanner', + text: 'HashiConf Global is live.', + linkText: 'Join Now', + // Set the expirationDate prop with a datetime string (e.g. '2020-01-31T12:00:00-07:00') + // if you'd like the component to stop showing at or after a certain date + expirationDate: '2021-10-21T12:00:00-07:00', } From eac433f7ff929cb35babf716c0036c4dd713af1c Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 29 Sep 2021 13:13:49 -0400 Subject: [PATCH 07/17] refact(target): Move tcp into subpackage This moves all of the tcp target functionality into a tcp package. It also establishes a Register function in the target package to allow specific target types to be registered at initilazation time. When registering, a target type needs to provide its Subtype, as well as some RepositoryHooks that are used by the target Repository. --- Makefile | 1 + cmd/boundary/.gitignore | 1 + cmd/boundary/package_registry_oss.go | 6 + internal/cmd/base/initial_resources.go | 7 +- internal/cmd/commands/dev/dev.go | 4 +- internal/credential/vault/jobs_test.go | 19 +- .../vault/repository_credentials_test.go | 7 +- .../credential/vault/testing_external_test.go | 3 +- .../migrations/oss/postgres_14_01_test.go | 3 +- .../migrations/oss/postgres_16_05_test.go | 7 +- .../storage/target/store/v1/target.proto | 64 -- .../storage/target/tcp/store/v1/target.proto | 72 ++ .../handlers/sessions/session_service_test.go | 19 +- .../handlers/targets/target_service.go | 60 +- .../handlers/targets/target_service_test.go | 201 ++--- internal/session/repository_session_test.go | 7 +- internal/session/testing.go | 3 +- internal/target/credential_library_test.go | 9 +- internal/target/exports_test.go | 7 + internal/target/host_set.go | 7 - internal/target/ids.go | 19 - internal/target/ids_test.go | 18 - internal/target/options.go | 94 +-- internal/target/options_test.go | 60 +- internal/target/registry.go | 112 +++ internal/target/repository.go | 265 ++++++- .../target/repository_credential_source.go | 54 +- internal/target/repository_host_source.go | 58 +- internal/target/repository_tcp_target.go | 198 ----- internal/target/repository_test.go | 694 +++++++++--------- internal/target/store/target.pb.go | 281 +------ internal/target/subtype.go | 41 -- internal/target/target.go | 66 +- internal/target/target_test.go | 9 +- internal/target/tcp/exports_test.go | 19 + .../target/{ => tcp}/immutable_fields_test.go | 68 +- internal/target/tcp/register.go | 37 + .../repository_credential_source_test.go | 64 +- .../{ => tcp}/repository_host_source_test.go | 88 +-- .../{ => tcp}/repository_tcp_target_test.go | 167 +++-- internal/target/tcp/repository_test.go | 347 +++++++++ internal/target/tcp/store/target.pb.go | 306 ++++++++ internal/target/tcp/target.go | 173 +++++ .../target_test.go} | 139 ++-- internal/target/tcp/testing.go | 62 ++ internal/target/{ => tcp}/testing_test.go | 40 +- internal/target/tcp_target.go | 110 --- internal/target/testing.go | 51 -- internal/tests/api/targets/target_test.go | 4 +- 49 files changed, 2416 insertions(+), 1735 deletions(-) create mode 100644 cmd/boundary/.gitignore create mode 100644 cmd/boundary/package_registry_oss.go create mode 100644 internal/proto/local/controller/storage/target/tcp/store/v1/target.proto create mode 100644 internal/target/exports_test.go delete mode 100644 internal/target/ids.go delete mode 100644 internal/target/ids_test.go create mode 100644 internal/target/registry.go delete mode 100644 internal/target/repository_tcp_target.go delete mode 100644 internal/target/subtype.go create mode 100644 internal/target/tcp/exports_test.go rename internal/target/{ => tcp}/immutable_fields_test.go (75%) create mode 100644 internal/target/tcp/register.go rename internal/target/{ => tcp}/repository_credential_source_test.go (89%) rename internal/target/{ => tcp}/repository_host_source_test.go (85%) rename internal/target/{ => tcp}/repository_tcp_target_test.go (68%) create mode 100644 internal/target/tcp/repository_test.go create mode 100644 internal/target/tcp/store/target.pb.go create mode 100644 internal/target/tcp/target.go rename internal/target/{tcp_target_test.go => tcp/target_test.go} (71%) create mode 100644 internal/target/tcp/testing.go rename internal/target/{ => tcp}/testing_test.go (58%) delete mode 100644 internal/target/tcp_target.go diff --git a/Makefile b/Makefile index 26d5dd77c..51296c11e 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,7 @@ protobuild: @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/target/store/target.pb.go + @protoc-go-inject-tag -input=./internal/target/tcp/store/target.pb.go @protoc-go-inject-tag -input=./internal/auth/oidc/store/oidc.pb.go @protoc-go-inject-tag -input=./internal/scheduler/job/store/job.pb.go @protoc-go-inject-tag -input=./internal/credential/store/credential.pb.go diff --git a/cmd/boundary/.gitignore b/cmd/boundary/.gitignore new file mode 100644 index 000000000..db02d202c --- /dev/null +++ b/cmd/boundary/.gitignore @@ -0,0 +1 @@ +/boundary diff --git a/cmd/boundary/package_registry_oss.go b/cmd/boundary/package_registry_oss.go new file mode 100644 index 000000000..65616ff18 --- /dev/null +++ b/cmd/boundary/package_registry_oss.go @@ -0,0 +1,6 @@ +package main + +import ( + // Enable tcp target support. + _ "github.com/hashicorp/boundary/internal/target/tcp" +) diff --git a/internal/cmd/base/initial_resources.go b/internal/cmd/base/initial_resources.go index ecf2e34f6..a0115f361 100644 --- a/internal/cmd/base/initial_resources.go +++ b/internal/cmd/base/initial_resources.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/go-secure-stdlib/base62" ) @@ -495,7 +496,7 @@ func (b *Server) CreateInitialTarget(ctx context.Context) (target.Target, error) // Host Catalog if b.DevTargetId == "" { - b.DevTargetId, err = db.NewPublicId(target.TcpTargetPrefix) + b.DevTargetId, err = db.NewPublicId(tcp.TargetPrefix) if err != nil { return nil, fmt.Errorf("error generating initial target id: %w", err) } @@ -512,11 +513,11 @@ func (b *Server) CreateInitialTarget(ctx context.Context) (target.Target, error) target.WithSessionConnectionLimit(int32(b.DevTargetSessionConnectionLimit)), target.WithPublicId(b.DevTargetId), } - t, err := target.NewTcpTarget(b.DevProjectId, opts...) + t, err := tcp.New(b.DevProjectId, opts...) if err != nil { return nil, fmt.Errorf("error creating in memory target: %w", err) } - tt, _, _, err := targetRepo.CreateTcpTarget(cancelCtx, t, opts...) + tt, _, _, err := targetRepo.CreateTarget(cancelCtx, t, opts...) if err != nil { return nil, fmt.Errorf("error saving target to the db: %w", err) } diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index 3e3de4cd7..0fae56c09 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -22,7 +22,7 @@ import ( "github.com/hashicorp/boundary/internal/servers/controller" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/worker" - "github.com/hashicorp/boundary/internal/target" + "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" @@ -373,7 +373,7 @@ func (c *Command) Run(args []string) int { c.DevHostCatalogId = fmt.Sprintf("%s_%s", static.HostCatalogPrefix, c.flagIdSuffix) c.DevHostSetId = fmt.Sprintf("%s_%s", static.HostSetPrefix, c.flagIdSuffix) c.DevHostId = fmt.Sprintf("%s_%s", static.HostPrefix, c.flagIdSuffix) - c.DevTargetId = fmt.Sprintf("%s_%s", target.TcpTargetPrefix, c.flagIdSuffix) + c.DevTargetId = fmt.Sprintf("%s_%s", tcp.TargetPrefix, c.flagIdSuffix) } if c.flagLoginName != "" { c.DevLoginName = c.flagLoginName diff --git a/internal/credential/vault/jobs_test.go b/internal/credential/vault/jobs_test.go index 3ceeaadf2..b6f16e73e 100644 --- a/internal/credential/vault/jobs_test.go +++ b/internal/credential/vault/jobs_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" wrapping "github.com/hashicorp/go-kms-wrapping" vault "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" @@ -815,7 +816,7 @@ func TestTokenRevocationJob_Run(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -1008,7 +1009,7 @@ func TestCredentialRenewalJob_RunLimits(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -1117,7 +1118,7 @@ func TestCredentialRenewalJob_Run(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -1223,7 +1224,7 @@ func TestCredentialRenewalJob_RunExpired(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -1296,7 +1297,7 @@ func TestCredentialRenewalJob_NextRunIn(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -1535,7 +1536,7 @@ func TestCredentialRevocationJob_RunLimits(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, @@ -1646,7 +1647,7 @@ func TestCredentialRevocationJob_Run(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -1736,7 +1737,7 @@ func TestCredentialRevocationJob_RunDeleted(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), @@ -2114,7 +2115,7 @@ func TestCredentialCleanupJob_Run(t *testing.T) { 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 := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess1 := session.TestSession(t, conn, wrapper, session.ComposedOf{ UserId: uId, HostId: h.GetPublicId(), diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 8c0604d02..20f9d1cc5 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/boundary/internal/scheduler" "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" ) @@ -103,7 +104,7 @@ func TestRepository_IssueCredentials(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) rc2dc := func(rcs []credential.Request) []*session.DynamicCredential { var dcs []*session.DynamicCredential @@ -221,7 +222,7 @@ func TestRepository_Revoke(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) const ( @@ -339,7 +340,7 @@ func Test_TerminateSession(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) const ( diff --git a/internal/credential/vault/testing_external_test.go b/internal/credential/vault/testing_external_test.go index ef7c17f45..4b24273eb 100644 --- a/internal/credential/vault/testing_external_test.go +++ b/internal/credential/vault/testing_external_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "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" ) @@ -34,7 +35,7 @@ func Test_TestCredentials(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) target.TestCredentialLibrary(t, conn, tar.GetPublicId(), cl.GetPublicId()) at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) diff --git a/internal/db/schema/migrations/oss/postgres_14_01_test.go b/internal/db/schema/migrations/oss/postgres_14_01_test.go index 3483a078a..fcc5421b2 100644 --- a/internal/db/schema/migrations/oss/postgres_14_01_test.go +++ b/internal/db/schema/migrations/oss/postgres_14_01_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/testing/dbtest" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/stretchr/testify/assert" @@ -79,7 +80,7 @@ func TestMigrations_UserDimension(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) var sessions []*session.Session kmsCache := kms.TestKms(t, conn, wrapper) diff --git a/internal/db/schema/migrations/oss/postgres_16_05_test.go b/internal/db/schema/migrations/oss/postgres_16_05_test.go index f2d960738..58cbda927 100644 --- a/internal/db/schema/migrations/oss/postgres_16_05_test.go +++ b/internal/db/schema/migrations/oss/postgres_16_05_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/hashicorp/boundary/testing/dbtest" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/stretchr/testify/assert" @@ -81,7 +82,7 @@ func TestMigrations_CredentialDimension(t *testing.T) { h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := target.TestTcpTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, prj.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) var sessions []*session.Session kmsCache := kms.TestKms(t, conn, wrapper) @@ -157,12 +158,12 @@ func TestMigrations_CredentialDimension(t *testing.T) { require.Equal(want, state) } -func testSessionCredentialParams(t *testing.T, conn *db.DB, kms *kms.Kms, wrapper wrapping.Wrapper, tar *target.TcpTarget) []*session.DynamicCredential { +func testSessionCredentialParams(t *testing.T, conn *db.DB, kms *kms.Kms, wrapper wrapping.Wrapper, tar target.Target) []*session.DynamicCredential { t.Helper() rw := db.New(conn) ctx := context.Background() - stores := vault.TestCredentialStores(t, conn, wrapper, tar.ScopeId, 1) + stores := vault.TestCredentialStores(t, conn, wrapper, tar.GetScopeId(), 1) libs := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) targetRepo, err := target.NewRepository(rw, rw, kms) diff --git a/internal/proto/local/controller/storage/target/store/v1/target.proto b/internal/proto/local/controller/storage/target/store/v1/target.proto index 837b66ae1..748827bb4 100644 --- a/internal/proto/local/controller/storage/target/store/v1/target.proto +++ b/internal/proto/local/controller/storage/target/store/v1/target.proto @@ -73,70 +73,6 @@ message TargetHostSet { timestamp.v1.Timestamp create_time = 30; } -message TcpTarget { - // public_id is used to access the TargetTcp via an API - // @inject_tag: gorm:"primary_key" - string public_id = 10; - - // scope id for the TargetTcp - // @inject_tag: `gorm:"default:null"` - string scope_id = 20; - - // name is the optional friendly name used to - // access the TargetTcp via an API - // @inject_tag: `gorm:"default:null"` - string name = 30 - [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; - - // description of the TargetTcp - // @inject_tag: `gorm:"default:null"` - string description = 40 [(custom_options.v1.mask_mapping) = { - this: "description" - that: "description" - }]; - - // create_time from the RDBMS - // @inject_tag: `gorm:"default:current_timestamp"` - timestamp.v1.Timestamp create_time = 50; - - // update_time from the RDBMS - // @inject_tag: `gorm:"default:current_timestamp"` - timestamp.v1.Timestamp update_time = 60; - - // version allows optimistic locking of the TargetTcp when modifying the - // TargetTcp - // @inject_tag: `gorm:"default:null"` - uint32 version = 70; - - // default port of the TargetTcp - // @inject_tag: `gorm:"default:null"` - uint32 default_port = 80 [(custom_options.v1.mask_mapping) = { - this: "DefaultPort" - that: "attributes.default_port" - }]; - - // Maximum total lifetime of a created session, in seconds - // @inject_tag: `gorm:"default:null"` - uint32 session_max_seconds = 100 [(custom_options.v1.mask_mapping) = { - this: "SessionMaxSeconds" - that: "session_max_seconds" - }]; - - // Maximum number of connections in a session - // @inject_tag: `gorm:"default:null"` - int32 session_connection_limit = 110 [(custom_options.v1.mask_mapping) = { - this: "SessionConnectionLimit" - that: "session_connection_limit" - }]; - - // A boolean expression that allows filtering the workers that can handle a session - // @inject_tag: `gorm:"default:null"` - string worker_filter = 120 [(custom_options.v1.mask_mapping) = { - this: "WorkerFilter" - that: "worker_filter" - }]; -} - message CredentialLibrary { // target_id of the Target // @inject_tag: gorm:"primary_key" diff --git a/internal/proto/local/controller/storage/target/tcp/store/v1/target.proto b/internal/proto/local/controller/storage/target/tcp/store/v1/target.proto new file mode 100644 index 000000000..beffda5f0 --- /dev/null +++ b/internal/proto/local/controller/storage/target/tcp/store/v1/target.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; + +package controller.storage.target.tcp.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/target/tcp/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +message Target { + // public_id is used to access the tcp.Target via an API + // @inject_tag: gorm:"primary_key" + string public_id = 10; + + // scope id for the tcp.Target + // @inject_tag: `gorm:"default:null"` + string scope_id = 20; + + // name is the optional friendly name used to + // access the tcp.Target via an API + // @inject_tag: `gorm:"default:null"` + string name = 30 + [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; + + // description of the tcp.Target + // @inject_tag: `gorm:"default:null"` + string description = 40 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 50; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 60; + + // version allows optimistic locking of the tcp.Target when modifying the + // tcp.Target + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // default port of the tcp.Target + // @inject_tag: `gorm:"default:null"` + uint32 default_port = 80 [(custom_options.v1.mask_mapping) = { + this: "DefaultPort" + that: "attributes.default_port" + }]; + + // Maximum total lifetime of a created session, in seconds + // @inject_tag: `gorm:"default:null"` + uint32 session_max_seconds = 100 [(custom_options.v1.mask_mapping) = { + this: "SessionMaxSeconds" + that: "session_max_seconds" + }]; + + // Maximum number of connections in a session + // @inject_tag: `gorm:"default:null"` + int32 session_connection_limit = 110 [(custom_options.v1.mask_mapping) = { + this: "SessionConnectionLimit" + that: "session_connection_limit" + }]; + + // A boolean expression that allows filtering the workers that can handle a session + // @inject_tag: `gorm:"default:null"` + string worker_filter = 120 [(custom_options.v1.mask_mapping) = { + this: "WorkerFilter" + that: "worker_filter" + }]; +} + diff --git a/internal/servers/controller/handlers/sessions/session_service_test.go b/internal/servers/controller/handlers/sessions/session_service_test.go index 5f44e671f..a997d8d65 100644 --- a/internal/servers/controller/handlers/sessions/session_service_test.go +++ b/internal/servers/controller/handlers/sessions/session_service_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/boundary/internal/servers/controller/handlers/sessions" "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/scope" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/sessions" @@ -57,7 +58,7 @@ func TestGetSession(t *testing.T) { 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 := target.TestTcpTarget(t, conn, p.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, p.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrap, session.ComposedOf{ UserId: uId, @@ -86,7 +87,7 @@ func TestGetSession(t *testing.T) { Scope: &scopes.ScopeInfo{Id: p.GetPublicId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, States: []*pb.SessionState{{Status: session.StatusPending.String(), StartTime: sess.CreateTime.GetTimestamp()}}, Certificate: sess.Certificate, - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, } @@ -174,7 +175,7 @@ func TestList_Self(t *testing.T) { 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 := target.TestTcpTarget(t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) // By default a user can read/cancel their own sessions. session.TestSession(t, conn, wrap, session.ComposedOf{ @@ -258,13 +259,13 @@ func TestList(t *testing.T) { 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 := target.TestTcpTarget(t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) hcOther := static.TestCatalogs(t, conn, pWithOtherSessions.GetPublicId(), 1)[0] hsOther := static.TestSets(t, conn, hcOther.GetPublicId(), 1)[0] hOther := static.TestHosts(t, conn, hcOther.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hsOther.GetPublicId(), []*static.Host{hOther}) - tarOther := target.TestTcpTarget(t, conn, pWithOtherSessions.GetPublicId(), "test", target.WithHostSources([]string{hsOther.GetPublicId()})) + tarOther := tcp.TestTarget(t, conn, pWithOtherSessions.GetPublicId(), "test", target.WithHostSources([]string{hsOther.GetPublicId()})) var wantSession []*pb.Session var totalSession []*pb.Session @@ -298,7 +299,7 @@ func TestList(t *testing.T) { Status: status, States: states, Certificate: sess.Certificate, - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, }) @@ -333,7 +334,7 @@ func TestList(t *testing.T) { Status: status, States: states, Certificate: sess.Certificate, - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, }) } @@ -469,7 +470,7 @@ func TestCancel(t *testing.T) { 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 := target.TestTcpTarget(t, conn, p.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(t, conn, p.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) sess := session.TestSession(t, conn, wrap, session.ComposedOf{ UserId: uId, @@ -496,7 +497,7 @@ func TestCancel(t *testing.T) { Scope: &scopes.ScopeInfo{Id: p.GetPublicId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, Status: session.StatusCanceling.String(), Certificate: sess.Certificate, - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, } diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index da31f23f3..b16da14ee 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -29,7 +29,8 @@ import ( "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" - "github.com/hashicorp/boundary/internal/target/store" + "github.com/hashicorp/boundary/internal/target/tcp" + tcpStore "github.com/hashicorp/boundary/internal/target/tcp/store" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" @@ -77,7 +78,10 @@ var ( func init() { var err error - if maskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.TcpTarget{}}, handlers.MaskSource{&pb.Target{}, &pb.TcpTargetAttributes{}}); err != nil { + if maskManager, err = handlers.NewMaskManager( + handlers.MaskDestination{&tcpStore.Target{}}, + handlers.MaskSource{&pb.Target{}, &pb.TcpTargetAttributes{}}, + ); err != nil { panic(err) } } @@ -992,7 +996,7 @@ HostSourceIterationLoop: // Generate the endpoint URL endpointUrl := &url.URL{ - Scheme: t.GetType(), + Scheme: t.GetType().String(), } defaultPort := t.GetDefaultPort() var endpointHost string @@ -1118,7 +1122,7 @@ HostSourceIterationLoop: TargetId: t.GetPublicId(), Scope: authResults.Scope, CreatedTime: sess.CreateTime.GetTimestamp(), - Type: t.GetType(), + Type: t.GetType().String(), Certificate: sess.Certificate, PrivateKey: privKey, HostId: chosenId.hostId, @@ -1137,7 +1141,7 @@ HostSourceIterationLoop: TargetId: t.GetPublicId(), Scope: authResults.Scope, CreatedTime: sess.CreateTime.GetTimestamp(), - Type: t.GetType(), + Type: t.GetType().String(), AuthorizationToken: string(encodedMarshaledSad), UserId: authResults.UserId, HostId: chosenId.hostId, @@ -1188,7 +1192,7 @@ func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Targ if tcpAttrs.GetDefaultPort().GetValue() != 0 { opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue())) } - u, err := target.NewTcpTarget(item.GetScopeId(), opts...) + u, err := tcp.New(item.GetScopeId(), opts...) if err != nil { return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for creation: %v.", err) } @@ -1196,7 +1200,7 @@ func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Targ if err != nil { return nil, nil, nil, err } - out, hs, cl, err := repo.CreateTcpTarget(ctx, u) + out, hs, cl, err := repo.CreateTarget(ctx, u) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target")) } @@ -1232,7 +1236,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue())) } version := item.GetVersion() - u, err := target.NewTcpTarget(scopeId, opts...) + u, err := tcp.New(scopeId, opts...) if err != nil { return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for update: %v.", err) } @@ -1245,7 +1249,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st if err != nil { return nil, nil, nil, err } - out, hs, cl, rowsUpdated, err := repo.UpdateTcpTarget(ctx, u, version, dbMask) + out, hs, cl, rowsUpdated, err := repo.UpdateTarget(ctx, u, version, dbMask) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update target")) } @@ -1588,7 +1592,7 @@ func toProto(ctx context.Context, in target.Target, hostSources []target.HostSou // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetTargetRequest) error { - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, target.TcpTargetPrefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, tcp.TargetPrefix) } func validateCreateRequest(req *pbs.CreateTargetRequest) error { @@ -1613,7 +1617,7 @@ func validateCreateRequest(req *pbs.CreateTargetRequest) error { badFields[globals.SessionMaxSecondsField] = "This must be greater than zero." } switch target.SubtypeFromType(req.GetItem().GetType()) { - case target.TcpSubtype: + case tcp.Subtype: tcpAttrs := &pb.TcpTargetAttributes{} if err := handlers.StructToProto(req.GetItem().GetAttributes(), tcpAttrs); err != nil { badFields[globals.AttributesField] = "Attribute fields do not match the expected format." @@ -1657,8 +1661,8 @@ func validateUpdateRequest(req *pbs.UpdateTargetRequest) error { badFields[globals.SessionMaxSecondsField] = "This must be greater than zero." } switch target.SubtypeFromId(req.GetItem().GetType()) { - case target.TcpSubtype: - if req.GetItem().GetType() != "" && target.SubtypeFromType(req.GetItem().GetType()) != target.TcpSubtype { + case tcp.Subtype: + if req.GetItem().GetType() != "" && target.SubtypeFromType(req.GetItem().GetType()) != tcp.Subtype { badFields[globals.TypeField] = "Cannot modify the resource type." } tcpAttrs := &pb.TcpTargetAttributes{} @@ -1675,11 +1679,11 @@ func validateUpdateRequest(req *pbs.UpdateTargetRequest) error { } } return badFields - }, target.TcpTargetPrefix) + }, tcp.TargetPrefix) } func validateDeleteRequest(req *pbs.DeleteTargetRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, target.TcpTargetPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, tcp.TargetPrefix) } func validateListRequest(req *pbs.ListTargetsRequest) error { @@ -1699,7 +1703,7 @@ func validateListRequest(req *pbs.ListTargetsRequest) error { func validateAddSetsRequest(req *pbs.AddTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1722,7 +1726,7 @@ func validateAddSetsRequest(req *pbs.AddTargetHostSetsRequest) error { func validateSetSetsRequest(req *pbs.SetTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1742,7 +1746,7 @@ func validateSetSetsRequest(req *pbs.SetTargetHostSetsRequest) error { func validateRemoveSetsRequest(req *pbs.RemoveTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1765,7 +1769,7 @@ func validateRemoveSetsRequest(req *pbs.RemoveTargetHostSetsRequest) error { func validateAddHostSourcesRequest(req *pbs.AddTargetHostSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1788,7 +1792,7 @@ func validateAddHostSourcesRequest(req *pbs.AddTargetHostSourcesRequest) error { func validateSetHostSourcesRequest(req *pbs.SetTargetHostSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1808,7 +1812,7 @@ func validateSetHostSourcesRequest(req *pbs.SetTargetHostSourcesRequest) error { func validateRemoveHostSourcesRequest(req *pbs.RemoveTargetHostSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1831,7 +1835,7 @@ func validateRemoveHostSourcesRequest(req *pbs.RemoveTargetHostSourcesRequest) e func validateAddLibrariesRequest(req *pbs.AddTargetCredentialLibrariesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1854,7 +1858,7 @@ func validateAddLibrariesRequest(req *pbs.AddTargetCredentialLibrariesRequest) e func validateSetLibrariesRequest(req *pbs.SetTargetCredentialLibrariesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1874,7 +1878,7 @@ func validateSetLibrariesRequest(req *pbs.SetTargetCredentialLibrariesRequest) e func validateRemoveLibrariesRequest(req *pbs.RemoveTargetCredentialLibrariesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1897,7 +1901,7 @@ func validateRemoveLibrariesRequest(req *pbs.RemoveTargetCredentialLibrariesRequ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1920,7 +1924,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1940,7 +1944,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourcesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1967,7 +1971,7 @@ func validateAuthorizeSessionRequest(req *pbs.AuthorizeSessionRequest) error { scopeIdEmpty := req.GetScopeId() == "" scopeNameEmpty := req.GetScopeName() == "" if nameEmpty { - if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { + if !handlers.ValidId(handlers.Id(req.GetId()), tcp.TargetPrefix) { badFields[globals.IdField] = "Incorrectly formatted identifier." } if !scopeIdEmpty { diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index 29b185fa3..f5b087d50 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -32,6 +32,7 @@ import ( "github.com/hashicorp/boundary/internal/servers/controller/handlers/workers" "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/scope" credpb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" @@ -106,7 +107,7 @@ func TestGet(t *testing.T) { hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] hs := static.TestSets(t, conn, hc.GetPublicId(), 2) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})) + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})) pTar := &pb.Target{ Id: tar.GetPublicId(), @@ -141,7 +142,7 @@ func TestGet(t *testing.T) { }, { name: "Get a non existing Target", - req: &pbs.GetTargetRequest{Id: target.TcpTargetPrefix + "_DoesntExis"}, + req: &pbs.GetTargetRequest{Id: tcp.TargetPrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -153,7 +154,7 @@ func TestGet(t *testing.T) { }, { name: "space in id", - req: &pbs.GetTargetRequest{Id: target.TcpTargetPrefix + "_1 23456789"}, + req: &pbs.GetTargetRequest{Id: tcp.TargetPrefix + "_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, @@ -201,7 +202,7 @@ func TestList(t *testing.T) { var totalTars []*pb.Target for i := 0; i < 5; i++ { name := fmt.Sprintf("tar%d", i) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), name, target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), name, target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) wantTars = append(wantTars, &pb.Target{ Id: tar.GetPublicId(), ScopeId: proj.GetPublicId(), @@ -217,7 +218,7 @@ func TestList(t *testing.T) { AuthorizedActions: testAuthorizedActions, }) totalTars = append(totalTars, wantTars[i]) - tar = target.TestTcpTarget(t, conn, otherProj.GetPublicId(), name, target.WithHostSources([]string{otherHss[0].GetPublicId(), otherHss[1].GetPublicId()})) + tar = tcp.TestTarget(t, conn, otherProj.GetPublicId(), name, target.WithHostSources([]string{otherHss[0].GetPublicId(), otherHss[1].GetPublicId()})) totalTars = append(totalTars, &pb.Target{ Id: tar.GetPublicId(), ScopeId: otherProj.GetPublicId(), @@ -316,7 +317,7 @@ func TestDelete(t *testing.T) { } _, proj := iam.TestScopes(t, iamRepo) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") s, err := testService(t, conn, kms, wrapper) require.NoError(t, err, "Couldn't create a new target service.") @@ -339,7 +340,7 @@ func TestDelete(t *testing.T) { name: "Delete Not Existing Target", scopeId: proj.GetPublicId(), req: &pbs.DeleteTargetRequest{ - Id: target.TcpTargetPrefix + "_doesntexis", + Id: tcp.TargetPrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -347,7 +348,7 @@ func TestDelete(t *testing.T) { name: "Bad target id formatting", scopeId: proj.GetPublicId(), req: &pbs.DeleteTargetRequest{ - Id: target.TcpTargetPrefix + "_bad_format", + Id: tcp.TargetPrefix + "_bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, @@ -379,7 +380,7 @@ func TestDelete_twice(t *testing.T) { _, proj := iam.TestScopes(t, iamRepo) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") s, err := testService(t, conn, kms, wrapper) require.NoError(err, "Couldn't create a new target service.") @@ -426,7 +427,7 @@ func TestCreate(t *testing.T) { WorkerFilter: wrapperspb.String(`type == "bar"`), }}, res: &pbs.CreateTargetResponse{ - Uri: fmt.Sprintf("targets/%s_", target.TcpTargetPrefix), + Uri: fmt.Sprintf("targets/%s_", tcp.TargetPrefix), Item: &pb.Target{ ScopeId: proj.GetPublicId(), Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, @@ -522,7 +523,7 @@ func TestCreate(t *testing.T) { if got != nil { assert.Contains(got.GetUri(), tc.res.GetUri()) - assert.True(strings.HasPrefix(got.GetItem().GetId(), target.TcpTargetPrefix), got.GetItem().GetId()) + assert.True(strings.HasPrefix(got.GetItem().GetId(), tcp.TargetPrefix), got.GetItem().GetId()) // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" @@ -570,18 +571,18 @@ func TestUpdate(t *testing.T) { {Id: hs[1].GetPublicId(), HostCatalogId: hs[1].GetCatalogId()}, } - tar, err := target.NewTcpTarget(proj.GetPublicId(), target.WithName("default"), target.WithDescription("default")) + tar, err := tcp.New(proj.GetPublicId(), target.WithName("default"), target.WithDescription("default")) tar.DefaultPort = 2 require.NoError(t, err) - gtar, _, _, err := repo.CreateTcpTarget(context.Background(), tar, target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})) + gtar, _, _, err := repo.CreateTarget(context.Background(), tar, target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})) require.NoError(t, err) - tar = gtar.(*target.TcpTarget) + tar = gtar.(*tcp.Target) var version uint32 = 1 resetTarget := func() { version++ - _, _, _, _, err = repo.UpdateTcpTarget(context.Background(), tar, version, []string{"Name", "Description"}) + _, _, _, _, err = repo.UpdateTarget(context.Background(), tar, version, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset target.") version++ } @@ -611,7 +612,7 @@ func TestUpdate(t *testing.T) { Description: wrapperspb.String("desc"), SessionMaxSeconds: wrapperspb.UInt32(3600), SessionConnectionLimit: wrapperspb.Int32(5), - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), }, }, res: &pbs.UpdateTargetResponse{ @@ -645,7 +646,7 @@ func TestUpdate(t *testing.T) { Item: &pb.Target{ Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), - Type: target.TcpSubtype.String(), + Type: tcp.Subtype.String(), }, }, res: &pbs.UpdateTargetResponse{ @@ -826,7 +827,7 @@ func TestUpdate(t *testing.T) { { name: "Update a Non Existing Target", req: &pbs.UpdateTargetRequest{ - Id: target.TcpTargetPrefix + "_DoesntExis", + Id: tcp.TargetPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, @@ -938,10 +939,10 @@ func TestUpdate_BadVersion(t *testing.T) { repo, err := repoFn() require.NoError(t, err, "Couldn't create new target repo.") - tar, err := target.NewTcpTarget(proj.GetPublicId(), target.WithName("default"), target.WithDescription("default")) + tar, err := tcp.New(proj.GetPublicId(), target.WithName("default"), target.WithDescription("default")) tar.DefaultPort = 2 require.NoError(t, err) - gtar, _, _, err := repo.CreateTcpTarget(context.Background(), tar) + gtar, _, _, err := repo.CreateTarget(context.Background(), tar) require.NoError(t, err) tested, err := testService(t, conn, kms, wrapper) @@ -980,25 +981,25 @@ func TestAddTargetHostSets(t *testing.T) { addCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target addHostSets []string resultHostSets []string }{ { name: "Add set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), addHostSets: []string{hs[1].GetPublicId()}, resultHostSets: []string{hs[1].GetPublicId()}, }, { name: "Add set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), addHostSets: []string{hs[1].GetPublicId()}, resultHostSets: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, }, { name: "Add duplicated sets on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated", target.WithHostSources([]string{hs[0].GetPublicId()})), addHostSets: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHostSets: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, }, @@ -1021,7 +1022,7 @@ func TestAddTargetHostSets(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") failCases := []struct { name string @@ -1096,31 +1097,31 @@ func TestSetTargetHostSets(t *testing.T) { setCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target setHostSets []string resultHostSets []string }{ { name: "Set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), setHostSets: []string{hs[1].GetPublicId()}, resultHostSets: []string{hs[1].GetPublicId()}, }, { name: "Set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSets: []string{hs[1].GetPublicId()}, resultHostSets: []string{hs[1].GetPublicId()}, }, { name: "Set duplicate host set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSets: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHostSets: []string{hs[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "another populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSets: []string{}, resultHostSets: nil, }, @@ -1139,7 +1140,7 @@ func TestSetTargetHostSets(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test name") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test name") failCases := []struct { name string @@ -1206,32 +1207,32 @@ func TestRemoveTargetHostSets(t *testing.T) { removeCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target removeHosts []string resultHosts []string wantErr bool }{ { name: "Remove from empty", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), removeHosts: []string{hs[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 sets", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHosts: []string{hs[1].GetPublicId()}, resultHosts: []string{hs[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 sets", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHosts: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHosts: []string{hs[0].GetPublicId()}, }, { name: "Remove all hosts from set", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove all", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHosts: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, resultHosts: []string{}, }, @@ -1258,7 +1259,7 @@ func TestRemoveTargetHostSets(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "testing") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "testing") failCases := []struct { name string @@ -1334,25 +1335,25 @@ func TestAddTargetHostSources(t *testing.T) { addCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target addHostSources []string resultHostSources []string }{ { name: "Add set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), addHostSources: []string{hs[1].GetPublicId()}, resultHostSources: []string{hs[1].GetPublicId()}, }, { name: "Add set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), addHostSources: []string{hs[1].GetPublicId()}, resultHostSources: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, }, { name: "Add duplicated sets on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated", target.WithHostSources([]string{hs[0].GetPublicId()})), addHostSources: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHostSources: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, }, @@ -1375,7 +1376,7 @@ func TestAddTargetHostSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") failCases := []struct { name string @@ -1450,31 +1451,31 @@ func TestSetTargetHostSources(t *testing.T) { setCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target setHostSources []string resultHostSources []string }{ { name: "Set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), setHostSources: []string{hs[1].GetPublicId()}, resultHostSources: []string{hs[1].GetPublicId()}, }, { name: "Set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSources: []string{hs[1].GetPublicId()}, resultHostSources: []string{hs[1].GetPublicId()}, }, { name: "Set duplicate host set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSources: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHostSources: []string{hs[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "another populated", target.WithHostSources([]string{hs[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithHostSources([]string{hs[0].GetPublicId()})), setHostSources: []string{}, resultHostSources: nil, }, @@ -1493,7 +1494,7 @@ func TestSetTargetHostSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test name") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test name") failCases := []struct { name string @@ -1560,32 +1561,32 @@ func TestRemoveTargetHostSources(t *testing.T) { removeCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target removeHostSources []string resultHostSources []string wantErr bool }{ { name: "Remove from empty", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), removeHostSources: []string{hs[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 sets", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHostSources: []string{hs[1].GetPublicId()}, resultHostSources: []string{hs[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 sets", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHostSources: []string{hs[1].GetPublicId(), hs[1].GetPublicId()}, resultHostSources: []string{hs[0].GetPublicId()}, }, { name: "Remove all hosts from set", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove all", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithHostSources([]string{hs[0].GetPublicId(), hs[1].GetPublicId()})), removeHostSources: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, resultHostSources: []string{}, }, @@ -1612,7 +1613,7 @@ func TestRemoveTargetHostSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "testing") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "testing") failCases := []struct { name string @@ -1688,25 +1689,25 @@ func TestAddTargetLibraries(t *testing.T) { addCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target addLibraries []string resultLibraryIds []string }{ { name: "Add library on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty for libraries"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty for libraries"), addLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Add library on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), addLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, { name: "Add duplicated libraries on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), addLibraries: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultLibraryIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, @@ -1737,7 +1738,7 @@ func TestAddTargetLibraries(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") failCases := []struct { name string @@ -1823,32 +1824,32 @@ func TestSetTargetLibraries(t *testing.T) { setCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target setLibraries []string resultLibraryIds []string resultLibraries []*pb.CredentialLibrary }{ { name: "Set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), setLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Set duplicate libraries on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setLibraries: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setLibraries: []string{}, resultLibraryIds: nil, }, @@ -1882,7 +1883,7 @@ func TestSetTargetLibraries(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test name") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test name") failCases := []struct { name string @@ -1949,26 +1950,26 @@ func TestRemoveTargetLibraries(t *testing.T) { removeCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target removeLibs []string resultLibIds []string wantErr bool }{ { name: "Remove from empty", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), removeLibs: []string{cls[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 libraries", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeLibs: []string{cls[1].GetPublicId()}, resultLibIds: []string{cls[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 libraries", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeLibs: []string{ cls[1].GetPublicId(), cls[1].GetPublicId(), }, @@ -1976,7 +1977,7 @@ func TestRemoveTargetLibraries(t *testing.T) { }, { name: "Remove all libraries from target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeLibs: []string{ cls[0].GetPublicId(), cls[1].GetPublicId(), }, @@ -2003,7 +2004,7 @@ func TestRemoveTargetLibraries(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "testing") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "testing") failCases := []struct { name string @@ -2085,25 +2086,25 @@ func TestAddTargetSources(t *testing.T) { addCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target addSources []string resultSourceIds []string }{ { name: "Add source on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty for sources"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty for sources"), addSources: []string{cls[1].GetPublicId()}, resultSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Add source on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), addSources: []string{cls[1].GetPublicId()}, resultSourceIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, { name: "Add duplicated sources on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), addSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultSourceIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, @@ -2134,7 +2135,7 @@ func TestAddTargetSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") failCases := []struct { name string @@ -2220,32 +2221,32 @@ func TestSetTargetCredentialSources(t *testing.T) { setCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target setCredentialSources []string resultCredentialSourceIds []string resultCredentialSources []*pb.CredentialLibrary }{ { name: "Set on empty target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), setCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Set on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Set duplicate sources on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setCredentialSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), setCredentialSources: []string{}, resultCredentialSourceIds: nil, }, @@ -2279,7 +2280,7 @@ func TestSetTargetCredentialSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test name") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test name") failCases := []struct { name string @@ -2346,26 +2347,26 @@ func TestRemoveTargetCredentialSources(t *testing.T) { removeCases := []struct { name string - tar *target.TcpTarget + tar *tcp.Target removeCredentialSources []string resultCredentialSourceIds []string wantErr bool }{ { name: "Remove from empty", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "empty"), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "empty"), removeCredentialSources: []string{cls[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 sources", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 sources", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeCredentialSources: []string{ cls[1].GetPublicId(), cls[1].GetPublicId(), }, @@ -2373,7 +2374,7 @@ func TestRemoveTargetCredentialSources(t *testing.T) { }, { name: "Remove all sources from target", - tar: target.TestTcpTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), removeCredentialSources: []string{ cls[0].GetPublicId(), cls[1].GetPublicId(), }, @@ -2400,7 +2401,7 @@ func TestRemoveTargetCredentialSources(t *testing.T) { }) } - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "testing") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "testing") failCases := []struct { name string @@ -2512,7 +2513,7 @@ func TestAuthorizeSession(t *testing.T) { s, err := targets.NewService(kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, staticHostRepoFn, credentialRepoFn) require.NoError(t, err) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), "test") hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] @@ -2687,7 +2688,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { sec, tok := v.CreateToken(t, vault.WithPolicies([]string{"default", "database"})) store := vault.TestCredentialStore(t, conn, wrapper, proj.GetPublicId(), v.Addr, tok, sec.Auth.Accessor) - workerExists := func(tar *target.TcpTarget) (version uint32) { + workerExists := func(tar *tcp.Target) (version uint32) { workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, &sync.Map{}, kms) _, err := workerService.Status(context.Background(), &spbs.StatusRequest{ Worker: &spb.Server{ @@ -2699,7 +2700,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { return tar.GetVersion() } - hostSetNoHostExists := func(tar *target.TcpTarget) (version uint32) { + hostSetNoHostExists := func(tar *tcp.Target) (version uint32) { hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] @@ -2712,7 +2713,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { return tr.GetItem().GetVersion() } - hostExists := func(tar *target.TcpTarget) (version uint32) { + hostExists := func(tar *tcp.Target) (version uint32) { hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] @@ -2726,7 +2727,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { return apiTar.GetItem().GetVersion() } - libraryExists := func(tar *target.TcpTarget) (version uint32) { + libraryExists := func(tar *tcp.Target) (version uint32) { credService, err := credentiallibraries.NewService(credentialRepoFn, iamRepoFn) require.NoError(t, err) clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credpb.CredentialLibrary{ @@ -2748,7 +2749,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { return tr.GetItem().GetVersion() } - misConfiguredlibraryExists := func(tar *target.TcpTarget) (version uint32) { + misConfiguredlibraryExists := func(tar *tcp.Target) (version uint32) { credService, err := credentiallibraries.NewService(credentialRepoFn, iamRepoFn) require.NoError(t, err) clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credpb.CredentialLibrary{ @@ -2772,33 +2773,33 @@ func TestAuthorizeSession_Errors(t *testing.T) { cases := []struct { name string - setup []func(*target.TcpTarget) uint32 + setup []func(*tcp.Target) uint32 err bool }{ { // This one must be run first since it relies on the DB not having any worker details name: "no worker", - setup: []func(tcpTarget *target.TcpTarget) uint32{hostExists, libraryExists}, + setup: []func(tcpTarget *tcp.Target) uint32{hostExists, libraryExists}, err: true, }, { name: "success", - setup: []func(tcpTarget *target.TcpTarget) uint32{workerExists, hostExists, libraryExists}, + setup: []func(tcpTarget *tcp.Target) uint32{workerExists, hostExists, libraryExists}, }, { name: "no hosts", - setup: []func(tcpTarget *target.TcpTarget) uint32{workerExists, hostSetNoHostExists, libraryExists}, + setup: []func(tcpTarget *tcp.Target) uint32{workerExists, hostSetNoHostExists, libraryExists}, err: true, }, { name: "bad library configuration", - setup: []func(tcpTarget *target.TcpTarget) uint32{workerExists, hostExists, misConfiguredlibraryExists}, + setup: []func(tcpTarget *tcp.Target) uint32{workerExists, hostExists, misConfiguredlibraryExists}, err: true, }, } for i, tc := range cases { t.Run(tc.name, func(t *testing.T) { - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), fmt.Sprintf("test-%d", i)) + tar := tcp.TestTarget(t, conn, proj.GetPublicId(), fmt.Sprintf("test-%d", i)) for _, fn := range tc.setup { ver := fn(tar) diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 890e5b260..68e9076f1 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -15,7 +15,8 @@ import ( "github.com/hashicorp/boundary/internal/host/static" staticStore "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/target" - targetStore "github.com/hashicorp/boundary/internal/target/store" + "github.com/hashicorp/boundary/internal/target/tcp" + tcpStore "github.com/hashicorp/boundary/internal/target/tcp/store" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/jackc/pgconn" "google.golang.org/protobuf/types/known/timestamppb" @@ -1314,8 +1315,8 @@ func TestRepository_CancelSessionViaFKNull(t *testing.T) { cancelFk: func() cancelFk { s := setupFn() - t := &target.TcpTarget{ - TcpTarget: &targetStore.TcpTarget{ + t := &tcp.Target{ + Target: &tcpStore.Target{ PublicId: s.TargetId, }, } diff --git a/internal/session/testing.go b/internal/session/testing.go index 652f5ac98..7a96e2778 100644 --- a/internal/session/testing.go +++ b/internal/session/testing.go @@ -15,6 +15,7 @@ import ( "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" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-uuid" @@ -132,7 +133,7 @@ func TestSessionParams(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper, iamR sets := static.TestSets(t, conn, cats[0].PublicId, 1) _ = static.TestSetMembers(t, conn, sets[0].PublicId, hosts) - tcpTarget := target.TestTcpTarget(t, conn, proj.PublicId, "test target") + tcpTarget := tcp.TestTarget(t, conn, proj.PublicId, "test target") kms := kms.TestKms(t, conn, wrapper) targetRepo, err := target.NewRepository(rw, rw, kms) diff --git a/internal/target/credential_library_test.go b/internal/target/credential_library_test.go index 38a86a4b9..a079f471b 100644 --- a/internal/target/credential_library_test.go +++ b/internal/target/credential_library_test.go @@ -1,10 +1,11 @@ -package target +package target_test import ( "testing" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/target/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,7 +19,7 @@ func TestCredentialLibrary_New(t *testing.T) { tests := []struct { name string args args - want *CredentialLibrary + want *target.CredentialLibrary wantErr errors.Code }{ { @@ -41,7 +42,7 @@ func TestCredentialLibrary_New(t *testing.T) { targetId: "targ_0000000", libraryId: "lib_0000000", }, - want: &CredentialLibrary{ + want: &target.CredentialLibrary{ CredentialLibrary: &store.CredentialLibrary{ TargetId: "targ_0000000", CredentialLibraryId: "lib_0000000", @@ -54,7 +55,7 @@ func TestCredentialLibrary_New(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := NewCredentialLibrary(tt.args.targetId, tt.args.libraryId) + got, err := target.NewCredentialLibrary(tt.args.targetId, tt.args.libraryId) if tt.wantErr != 0 { assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) assert.Nil(got) diff --git a/internal/target/exports_test.go b/internal/target/exports_test.go new file mode 100644 index 000000000..cada8aa31 --- /dev/null +++ b/internal/target/exports_test.go @@ -0,0 +1,7 @@ +package target + +// Expose functions for use in tests. +var ( + AllocTargetView = allocTargetView + TargetsViewDefaultTable = targetsViewDefaultTable +) diff --git a/internal/target/host_set.go b/internal/target/host_set.go index 7ccee9ed5..8326dadda 100644 --- a/internal/target/host_set.go +++ b/internal/target/host_set.go @@ -44,13 +44,6 @@ func NewTargetHostSet(targetId, hostSetId string, _ ...Option) (*TargetHostSet, return t, nil } -// allocTargetHostSet will allocate a target host set -func allocTargetHostSet() TargetHostSet { - return TargetHostSet{ - TargetHostSet: &store.TargetHostSet{}, - } -} - // Clone creates a clone of the target host set func (t *TargetHostSet) Clone() interface{} { cp := proto.Clone(t.TargetHostSet) diff --git a/internal/target/ids.go b/internal/target/ids.go deleted file mode 100644 index 618572621..000000000 --- a/internal/target/ids.go +++ /dev/null @@ -1,19 +0,0 @@ -package target - -import ( - "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/errors" -) - -const ( - TcpTargetPrefix = "ttcp" -) - -func newTcpTargetId() (string, error) { - const op = "target.newTcpTargetId" - id, err := db.NewPublicId(TcpTargetPrefix) - if err != nil { - return "", errors.WrapDeprecated(err, op) - } - return id, nil -} diff --git a/internal/target/ids_test.go b/internal/target/ids_test.go deleted file mode 100644 index eac126b1a..000000000 --- a/internal/target/ids_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package target - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Ids(t *testing.T) { - t.Parallel() - t.Run("tcp", func(t *testing.T) { - id, err := newTcpTargetId() - require.NoError(t, err) - assert.True(t, strings.HasPrefix(id, TcpTargetPrefix+"_")) - }) -} diff --git a/internal/target/options.go b/internal/target/options.go index 1f39fb47b..f87c4763f 100644 --- a/internal/target/options.go +++ b/internal/target/options.go @@ -2,8 +2,8 @@ package target import "time" -// getOpts - iterate the inbound Options and return a struct -func getOpts(opt ...Option) options { +// GetOpts - iterate the inbound Options and return a struct +func GetOpts(opt ...Option) options { opts := getDefaultOptions() for _, o := range opt { o(&opts) @@ -16,54 +16,54 @@ type Option func(*options) // options = how options are represented type options struct { - withName string - withDescription string - withDefaultPort uint32 - withLimit int - withScopeId string - withScopeIds []string - withScopeName string - withUserId string - withTargetType *TargetType - withHostSources []string - withCredentialSources []string - withSessionMaxSeconds uint32 - withSessionConnectionLimit int32 - withPublicId string - withWorkerFilter string + WithName string + WithDescription string + WithDefaultPort uint32 + WithLimit int + WithScopeId string + WithScopeIds []string + WithScopeName string + WithUserId string + WithTargetType *TargetType + WithHostSources []string + WithCredentialSources []string + WithSessionMaxSeconds uint32 + WithSessionConnectionLimit int32 + WithPublicId string + WithWorkerFilter string } func getDefaultOptions() options { return options{ - withName: "", - withDescription: "", - withLimit: 0, - withDefaultPort: 0, - withScopeId: "", - withScopeIds: nil, - withScopeName: "", - withUserId: "", - withTargetType: nil, - withHostSources: nil, - withCredentialSources: nil, - withSessionMaxSeconds: uint32((8 * time.Hour).Seconds()), - withSessionConnectionLimit: 1, - withPublicId: "", - withWorkerFilter: "", + WithName: "", + WithDescription: "", + WithLimit: 0, + WithDefaultPort: 0, + WithScopeId: "", + WithScopeIds: nil, + WithScopeName: "", + WithUserId: "", + WithTargetType: nil, + WithHostSources: nil, + WithCredentialSources: nil, + WithSessionMaxSeconds: uint32((8 * time.Hour).Seconds()), + WithSessionConnectionLimit: 1, + WithPublicId: "", + WithWorkerFilter: "", } } // WithDescription provides an optional description func WithDescription(desc string) Option { return func(o *options) { - o.withDescription = desc + o.WithDescription = desc } } // WithName provides an option to search by a friendly name func WithName(name string) Option { return func(o *options) { - o.withName = name + o.WithName = name } } @@ -72,88 +72,88 @@ func WithName(name string) Option { // If WithLimit == 0, then default limits are used for results. func WithLimit(limit int) Option { return func(o *options) { - o.withLimit = limit + o.WithLimit = limit } } // WithDefaultPort provides an option to specify the default target port. func WithDefaultPort(p uint32) Option { return func(o *options) { - o.withDefaultPort = p + o.WithDefaultPort = p } } // WithScopeId provides an option to search by a scope id func WithScopeId(scopeId string) Option { return func(o *options) { - o.withScopeId = scopeId + o.WithScopeId = scopeId } } // WithScopeId provides an option to search by multiple scope id func WithScopeIds(scopeIds []string) Option { return func(o *options) { - o.withScopeIds = scopeIds + o.WithScopeIds = scopeIds } } // WithScopeId provides an option to search by a scope name func WithScopeName(scopeName string) Option { return func(o *options) { - o.withScopeName = scopeName + o.WithScopeName = scopeName } } // WithUserId provides an option to search by a user public id func WithUserId(userId string) Option { return func(o *options) { - o.withUserId = userId + o.WithUserId = userId } } // WithTargetType provides an option to search by a target type func WithTargetType(t TargetType) Option { return func(o *options) { - o.withTargetType = &t + o.WithTargetType = &t } } // WithHostSources provides an option for providing a list of host source ids func WithHostSources(hs []string) Option { return func(o *options) { - o.withHostSources = hs + o.WithHostSources = hs } } // WithCredentialSources provides an option for providing a list of credential source ids func WithCredentialSources(cl []string) Option { return func(o *options) { - o.withCredentialSources = cl + o.WithCredentialSources = cl } } func WithSessionMaxSeconds(dur uint32) Option { return func(o *options) { - o.withSessionMaxSeconds = dur + o.WithSessionMaxSeconds = dur } } func WithSessionConnectionLimit(limit int32) Option { return func(o *options) { - o.withSessionConnectionLimit = limit + o.WithSessionConnectionLimit = limit } } // WithPublicId provides an optional public id func WithPublicId(id string) Option { return func(o *options) { - o.withPublicId = id + o.WithPublicId = id } } // WithWorkerFilter provides an optional worker filter func WithWorkerFilter(filter string) Option { return func(o *options) { - o.withWorkerFilter = filter + o.WithWorkerFilter = filter } } diff --git a/internal/target/options_test.go b/internal/target/options_test.go index 298cb373c..4c3f73716 100644 --- a/internal/target/options_test.go +++ b/internal/target/options_test.go @@ -11,104 +11,104 @@ func Test_GetOpts(t *testing.T) { t.Parallel() t.Run("WithName", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithName("test")) + opts := GetOpts(WithName("test")) testOpts := getDefaultOptions() - testOpts.withName = "test" + testOpts.WithName = "test" assert.Equal(opts, testOpts) }) t.Run("WithDescription", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithDescription("test desc")) + opts := GetOpts(WithDescription("test desc")) testOpts := getDefaultOptions() - testOpts.withDescription = "test desc" + testOpts.WithDescription = "test desc" assert.Equal(opts, testOpts) }) t.Run("WithLimit", func(t *testing.T) { assert := assert.New(t) // test default of 0 - opts := getOpts() + opts := GetOpts() testOpts := getDefaultOptions() - testOpts.withLimit = 0 + testOpts.WithLimit = 0 assert.Equal(opts, testOpts) - opts = getOpts(WithLimit(-1)) + opts = GetOpts(WithLimit(-1)) testOpts = getDefaultOptions() - testOpts.withLimit = -1 + testOpts.WithLimit = -1 assert.Equal(opts, testOpts) - opts = getOpts(WithLimit(1)) + opts = GetOpts(WithLimit(1)) testOpts = getDefaultOptions() - testOpts.withLimit = 1 + testOpts.WithLimit = 1 assert.Equal(opts, testOpts) }) t.Run("WithDefaultPort", func(t *testing.T) { assert := assert.New(t) // test default of 0 - opts := getOpts() + opts := GetOpts() testOpts := getDefaultOptions() - testOpts.withDefaultPort = 0 + testOpts.WithDefaultPort = 0 assert.Equal(opts, testOpts) - opts = getOpts(WithDefaultPort(22)) + opts = GetOpts(WithDefaultPort(22)) testOpts = getDefaultOptions() - testOpts.withDefaultPort = uint32(22) + testOpts.WithDefaultPort = uint32(22) assert.Equal(opts, testOpts) }) t.Run("WithUserId", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithUserId("testId")) + opts := GetOpts(WithUserId("testId")) testOpts := getDefaultOptions() - testOpts.withUserId = "testId" + testOpts.WithUserId = "testId" assert.Equal(opts, testOpts) }) t.Run("WithScopeId", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithScopeId("testId")) + opts := GetOpts(WithScopeId("testId")) testOpts := getDefaultOptions() - testOpts.withScopeId = "testId" + testOpts.WithScopeId = "testId" assert.Equal(opts, testOpts) }) t.Run("WithScopeName", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithScopeName("testName")) + opts := GetOpts(WithScopeName("testName")) testOpts := getDefaultOptions() - testOpts.withScopeName = "testName" + testOpts.WithScopeName = "testName" assert.Equal(opts, testOpts) }) t.Run("WithPublicId", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithPublicId("testId")) + opts := GetOpts(WithPublicId("testId")) testOpts := getDefaultOptions() - testOpts.withPublicId = "testId" + testOpts.WithPublicId = "testId" assert.Equal(opts, testOpts) }) t.Run("WithTargetType", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithTargetType(TcpTargetType)) + opts := GetOpts(WithTargetType(TcpTargetType)) testOpts := getDefaultOptions() target := TcpTargetType - testOpts.withTargetType = &target + testOpts.WithTargetType = &target assert.Equal(opts, testOpts) }) t.Run("WithHostSources", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithHostSources([]string{"alice", "bob"})) + opts := GetOpts(WithHostSources([]string{"alice", "bob"})) testOpts := getDefaultOptions() - testOpts.withHostSources = []string{"alice", "bob"} + testOpts.WithHostSources = []string{"alice", "bob"} assert.Equal(opts, testOpts) }) t.Run("WithWorkerFilter", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithWorkerFilter(`"/foo" == "bar"`)) + opts := GetOpts(WithWorkerFilter(`"/foo" == "bar"`)) testOpts := getDefaultOptions() - testOpts.withWorkerFilter = `"/foo" == "bar"` + testOpts.WithWorkerFilter = `"/foo" == "bar"` assert.Equal(opts, testOpts) }) t.Run("WithCredentialSources", func(t *testing.T) { assert := assert.New(t) - opts := getOpts(WithCredentialSources([]string{"alice", "bob"})) + opts := GetOpts(WithCredentialSources([]string{"alice", "bob"})) testOpts := getDefaultOptions() - testOpts.withCredentialSources = []string{"alice", "bob"} + testOpts.WithCredentialSources = []string{"alice", "bob"} assert.Equal(opts, testOpts) }) } diff --git a/internal/target/registry.go b/internal/target/registry.go new file mode 100644 index 000000000..d33f3035c --- /dev/null +++ b/internal/target/registry.go @@ -0,0 +1,112 @@ +package target + +import ( + "context" + "fmt" + "sync" + + "github.com/hashicorp/boundary/internal/types/subtypes" +) + +// AllocFunc is a function that creates an in-memory Target. +type AllocFunc func() Target + +// VetFunc is a function that checks the given Target to ensure it can +// be used by the Repository. +type VetFunc func(context.Context, Target) error + +type registryEntry struct { + alloc AllocFunc + vet VetFunc + prefix string +} + +type registry struct { + m map[subtypes.Subtype]*registryEntry + subtypes *subtypes.Registry + + sync.RWMutex +} + +func (r *registry) set(s subtypes.Subtype, entry *registryEntry) { + r.Lock() + defer r.Unlock() + + _, previouslySet := r.m[s] + if previouslySet { + panic(fmt.Sprintf("target subtype %s already registered", s)) + } + + if err := r.subtypes.Register(s, entry.prefix); err != nil { + panic(err) + } + + r.m[s] = entry +} + +func (r *registry) get(s subtypes.Subtype) (*registryEntry, bool) { + r.RLock() + defer r.RUnlock() + + entry, ok := r.m[s] + if ok { + return entry, ok + } + return nil, ok +} + +func (r *registry) allocFunc(s subtypes.Subtype) (AllocFunc, bool) { + entry, ok := r.get(s) + if !ok { + return nil, ok + } + return entry.alloc, ok +} + +func (r *registry) vetFunc(s subtypes.Subtype) (VetFunc, bool) { + entry, ok := r.get(s) + if !ok { + return nil, ok + } + return entry.vet, ok +} + +func (r *registry) idPrefix(s subtypes.Subtype) (string, bool) { + entry, ok := r.get(s) + if !ok { + return "", ok + } + return entry.prefix, ok +} + +var subtypeRegistry = registry{ + m: make(map[subtypes.Subtype]*registryEntry), + subtypes: subtypes.NewRegistry(), +} + +// SubtypeFromType returns the Subtype from the provided string or if +// no Subtype was registered with that string Unknown is returned. +func SubtypeFromType(t string) subtypes.Subtype { + subtypeRegistry.RLock() + defer subtypeRegistry.RUnlock() + return subtypeRegistry.subtypes.SubtypeFromType(t) +} + +// SubtypeFromId returns the Subtype from the provided id if the id's prefix +// was registered with a Subtype. Otherwise Unknown is returned. +func SubtypeFromId(id string) subtypes.Subtype { + subtypeRegistry.RLock() + defer subtypeRegistry.RUnlock() + return subtypeRegistry.subtypes.SubtypeFromId(id) +} + +// Register registers repository hooks and the prefixes for a provided Subtype. Register +// panics if the subtype has already been registered or if any of the +// prefixes are associated with another subtype. +func Register(s subtypes.Subtype, af AllocFunc, vf VetFunc, prefix string) { + subtypeRegistry.set(s, ®istryEntry{ + alloc: af, + vet: vf, + prefix: prefix, + }) +} diff --git a/internal/target/repository.go b/internal/target/repository.go index dff2cbebd..96f53a0d4 100644 --- a/internal/target/repository.go +++ b/internal/target/repository.go @@ -6,6 +6,7 @@ import ( "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/oplog" @@ -13,7 +14,7 @@ import ( // Cloneable provides a cloning interface type Cloneable interface { - Clone() interface{} + Clone() Target } // Repository is the target database repository @@ -39,16 +40,16 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo if kms == nil { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "nil kms") } - opts := getOpts(opt...) - if opts.withLimit == 0 { + opts := GetOpts(opt...) + if opts.WithLimit == 0 { // zero signals the boundary defaults should be used. - opts.withLimit = db.DefaultLimit + opts.WithLimit = db.DefaultLimit } return &Repository{ reader: r, writer: w, kms: kms, - defaultLimit: opts.withLimit, + defaultLimit: opts.WithLimit, }, nil } @@ -57,7 +58,7 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo // found, it will return nil, nil, nil, nil. No options are currently supported. func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, opt ...Option) (Target, []HostSource, []CredentialSource, error) { const op = "target.(Repository).LookupTarget" - opts := getOpts(opt...) + opts := GetOpts(opt...) if publicIdOrName == "" { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") @@ -65,23 +66,23 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op var where []string var whereArgs []interface{} - nameEmpty := opts.withName == "" - scopeIdEmpty := opts.withScopeId == "" - scopeNameEmpty := opts.withScopeName == "" + nameEmpty := opts.WithName == "" + scopeIdEmpty := opts.WithScopeId == "" + scopeNameEmpty := opts.WithScopeName == "" if !nameEmpty { - if opts.withName != publicIdOrName { + if opts.WithName != publicIdOrName { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "name passed in but does not match publicId") } - where, whereArgs = append(where, "lower(name) = lower(?)"), append(whereArgs, opts.withName) + where, whereArgs = append(where, "lower(name) = lower(?)"), append(whereArgs, opts.WithName) switch { case scopeIdEmpty && scopeNameEmpty: return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "using name but both scope ID and scope name are empty") case !scopeIdEmpty && !scopeNameEmpty: return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "using name but both scope ID and scope name are set") case !scopeIdEmpty: - where, whereArgs = append(where, "scope_id = ?"), append(whereArgs, opts.withScopeId) + where, whereArgs = append(where, "scope_id = ?"), append(whereArgs, opts.WithScopeId) case !scopeNameEmpty: - where, whereArgs = append(where, "scope_id = (select public_id from iam_scope where lower(name) = lower(?))"), append(whereArgs, opts.withScopeName) + where, whereArgs = append(where, "scope_id = (select public_id from iam_scope where lower(name) = lower(?))"), append(whereArgs, opts.WithScopeName) default: return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "unknown combination of parameters") } @@ -130,7 +131,7 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op } return nil, nil, nil, errors.Wrap(ctx, err, op) } - subtype, err := target.targetSubtype() + subtype, err := target.targetSubtype(ctx) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op) } @@ -140,18 +141,18 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op // ListTargets in targets in a scope. Supports the WithScopeId, WithLimit, WithTargetType options. func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, error) { const op = "target.(Repository).ListTargets" - opts := getOpts(opt...) - if len(opts.withScopeIds) == 0 && opts.withUserId == "" { + opts := GetOpts(opt...) + if len(opts.WithScopeIds) == 0 && opts.WithUserId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "must specify either scope id or user id") } // TODO (jimlambrt 8/2020) - implement WithUserId() optional filtering. var where []string var args []interface{} - if len(opts.withScopeIds) != 0 { - where, args = append(where, "scope_id in (?)"), append(args, opts.withScopeIds) + if len(opts.WithScopeIds) != 0 { + where, args = append(where, "scope_id in (?)"), append(args, opts.WithScopeIds) } - if opts.withTargetType != nil { - where, args = append(where, "type = ?"), append(args, opts.withTargetType.String()) + if opts.WithTargetType != nil { + where, args = append(where, "type = ?"), append(args, opts.WithTargetType.String()) } var foundTargets []*targetView @@ -163,7 +164,7 @@ func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, targets := make([]Target, 0, len(foundTargets)) for _, t := range foundTargets { - subtype, err := t.targetSubtype() + subtype, err := t.targetSubtype(ctx) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -176,12 +177,12 @@ func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, // repo defaultLimit func (r *Repository) list(ctx context.Context, resources interface{}, where string, args []interface{}, opt ...Option) error { const op = "target.(Repository).list" - opts := getOpts(opt...) + opts := GetOpts(opt...) limit := r.defaultLimit var dbOpts []db.Option - if opts.withLimit != 0 { + if opts.WithLimit != 0 { // non-zero signals an override of the default limit for the repo. - limit = opts.withLimit + limit = opts.WithLimit } dbOpts = append(dbOpts, db.WithLimit(limit)) if err := r.reader.SearchWhere(ctx, resources, where, args, dbOpts...); err != nil { @@ -202,17 +203,16 @@ func (r *Repository) DeleteTarget(ctx context.Context, publicId string, _ ...Opt return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId))) } var metadata oplog.Metadata - var deleteTarget interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = publicId - deleteTarget = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_DELETE) - default: + var deleteTarget Target + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", publicId, t.Type)) } + deleteTarget = alloc() + deleteTarget.SetPublicId(ctx, publicId) + metadata = deleteTarget.Oplog(oplog.OpType_OP_TYPE_DELETE) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.ScopeId, kms.KeyPurposeOplog) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) @@ -277,7 +277,7 @@ func (r *Repository) update(ctx context.Context, target Target, version uint32, if err != nil { return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } - metadata := target.oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata := target.Oplog(oplog.OpType_OP_TYPE_UPDATE) dbOpts = append(dbOpts, db.WithOplog(oplogWrapper, metadata)) var rowsUpdated int @@ -320,3 +320,202 @@ func (r *Repository) update(ctx context.Context, target Target, version uint32, } return returnedTarget.(Target), hostSources, credSources, rowsUpdated, nil } + +// CreateTarget inserts into the repository and returns the new Target with +// its list of host sets and credential libraries. +// WithHostSources and WithCredentialSources are the only supported option. +func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Option) (Target, []HostSource, []CredentialSource, error) { + const op = "target.(Repository).CreateTarget" + opts := GetOpts(opt...) + if target == nil { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing target") + } + + vet, ok := subtypeRegistry.vetFunc(target.GetType()) + if !ok { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType())) + } + if err := vet(ctx, target); err != nil { + return nil, nil, nil, err + } + if target.GetScopeId() == "" { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if target.GetName() == "" { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing name") + } + if target.GetPublicId() != "" { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + + t := target.Clone() + + if opts.WithPublicId != "" { + if err := t.SetPublicId(ctx, opts.WithPublicId); err != nil { + return nil, nil, nil, err + } + } else { + prefix, ok := subtypeRegistry.idPrefix(target.GetType()) + if !ok { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType())) + } + id, err := db.NewPublicId(prefix) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op) + } + t.SetPublicId(ctx, id) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetScopeId(), kms.KeyPurposeOplog) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + newHostSets := make([]interface{}, 0, len(opts.WithHostSources)) + for _, hsId := range opts.WithHostSources { + hostSet, err := NewTargetHostSet(t.GetPublicId(), hsId) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory target host set")) + } + newHostSets = append(newHostSets, hostSet) + } + + newCredLibs := make([]interface{}, 0, len(opts.WithCredentialSources)) + for _, clId := range opts.WithCredentialSources { + credLib, err := NewCredentialLibrary(t.GetPublicId(), clId) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory target credential library")) + } + newCredLibs = append(newCredLibs, credLib) + } + + metadata := t.Oplog(oplog.OpType_OP_TYPE_CREATE) + var returnedTarget interface{} + var returnedHostSources []HostSource + var returnedCredSources []CredentialSource + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + targetTicket, err := w.GetTicket(t) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + msgs := make([]*oplog.Message, 0, 2) + var targetOplogMsg oplog.Message + returnedTarget = t.Clone() + if err := w.Create(ctx, returnedTarget, db.NewOplogMsg(&targetOplogMsg)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target")) + } + msgs = append(msgs, &targetOplogMsg) + if len(newHostSets) > 0 { + hostSetOplogMsgs := make([]*oplog.Message, 0, len(newHostSets)) + if err := w.CreateItems(ctx, newHostSets, db.NewOplogMsgs(&hostSetOplogMsgs)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add host sets")) + } + if returnedHostSources, err = fetchHostSources(ctx, read, t.GetPublicId()); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to read host sources")) + } + msgs = append(msgs, hostSetOplogMsgs...) + } + if len(newCredLibs) > 0 { + credLibOplogMsgs := make([]*oplog.Message, 0, len(newCredLibs)) + if err := w.CreateItems(ctx, newCredLibs, db.NewOplogMsgs(&credLibOplogMsgs)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add credential sources")) + } + if returnedCredSources, err = fetchCredentialSources(ctx, read, t.GetPublicId()); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to read credential sources")) + } + msgs = append(msgs, credLibOplogMsgs...) + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + + return nil + }, + ) + if err != nil { + return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s target id", t.GetPublicId()))) + } + return returnedTarget.(Target), returnedHostSources, returnedCredSources, nil +} + +// UpdateTarget will update a target in the repository and return the written +// target. 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 WorkerFilter are the only +// updatable fields. If no updatable fields are included in the fieldMaskPaths, +// then an error is returned. +func (r *Repository) UpdateTarget(ctx context.Context, target Target, version uint32, fieldMaskPaths []string, _ ...Option) (Target, []HostSource, []CredentialSource, int, error) { + const op = "target.(Repository).UpdateTarget" + if target == nil { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target") + } + vet, ok := subtypeRegistry.vetFunc(target.GetType()) + if !ok { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType())) + } + if err := vet(ctx, target); err != nil { + return nil, nil, nil, db.NoRowsAffected, err + } + + if target.GetPublicId() == "" { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target public id") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold("name", f): + case strings.EqualFold("description", f): + case strings.EqualFold("defaultport", f): + case strings.EqualFold("sessionmaxseconds", f): + case strings.EqualFold("sessionconnectionlimit", f): + case strings.EqualFold("workerfilter", f): + default: + return nil, nil, 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{}{ + "Name": target.GetName(), + "Description": target.GetDescription(), + "DefaultPort": target.GetDefaultPort(), + "SessionMaxSeconds": target.GetSessionMaxSeconds(), + "SessionConnectionLimit": target.GetSessionConnectionLimit(), + "WorkerFilter": target.GetWorkerFilter(), + }, + fieldMaskPaths, + []string{"SessionMaxSeconds", "SessionConnectionLimit"}, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask") + } + var returnedTarget Target + var rowsUpdated int + var hostSources []HostSource + var credSources []CredentialSource + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + var err error + t := target.Clone() + returnedTarget, hostSources, credSources, rowsUpdated, err = r.update(ctx, t, version, dbMask, nullFields) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("target %s already exists in scope %s", target.GetName(), target.GetScopeId())) + } + return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", target.GetPublicId()))) + } + return returnedTarget, hostSources, credSources, rowsUpdated, nil +} diff --git a/internal/target/repository_credential_source.go b/internal/target/repository_credential_source.go index 2ebb5449f..31284bf1e 100644 --- a/internal/target/repository_credential_source.go +++ b/internal/target/repository_credential_source.go @@ -43,19 +43,18 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", targetId))) } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) @@ -143,18 +142,16 @@ func (r *Repository) DeleteTargetCredentialSources(ctx context.Context, targetId return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", targetId))) } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { @@ -257,17 +254,16 @@ func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId st return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", targetId))) } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) diff --git a/internal/target/repository_host_source.go b/internal/target/repository_host_source.go index de02b7911..013097c76 100644 --- a/internal/target/repository_host_source.go +++ b/internal/target/repository_host_source.go @@ -40,18 +40,18 @@ func (r *Repository) AddTargetHostSources(ctx context.Context, targetId string, return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", targetId))) } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) @@ -137,18 +137,18 @@ func (r *Repository) DeleteTargetHostSources(ctx context.Context, targetId strin } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) @@ -260,17 +260,17 @@ func (r *Repository) SetTargetHostSources(ctx context.Context, targetId string, } var metadata oplog.Metadata - var target interface{} - switch t.Type { - case TcpTargetType.String(): - tcpT := allocTcpTarget() - tcpT.PublicId = t.PublicId - tcpT.Version = targetVersion + 1 - target = &tcpT - metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) - default: + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { return nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", t.PublicId, t.Type)) } + + target := alloc() + target.SetPublicId(ctx, t.PublicId) + target.SetVersion(targetVersion + 1) + metadata = target.Oplog(oplog.OpType_OP_TYPE_UPDATE) + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) diff --git a/internal/target/repository_tcp_target.go b/internal/target/repository_tcp_target.go deleted file mode 100644 index 1f6fe63a2..000000000 --- a/internal/target/repository_tcp_target.go +++ /dev/null @@ -1,198 +0,0 @@ -package target - -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/oplog" -) - -// CreateTcpTarget inserts into the repository and returns the new Target with -// its list of host sets and credential libraries. -// WithHostSources and WithCredentialSources are the only supported option. -func (r *Repository) CreateTcpTarget(ctx context.Context, target *TcpTarget, opt ...Option) (Target, []HostSource, []CredentialSource, error) { - const op = "target.(Repository).CreateTcpTarget" - opts := getOpts(opt...) - if target == nil { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing target") - } - if target.TcpTarget == nil { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing target store") - } - if target.ScopeId == "" { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") - } - if target.Name == "" { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing name") - } - if target.PublicId != "" { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") - } - - t := target.Clone().(*TcpTarget) - - if opts.withPublicId != "" { - if !strings.HasPrefix(opts.withPublicId, TcpTargetPrefix+"_") { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("passed-in public ID %q has wrong prefix, should be %q", opts.withPublicId, TcpTargetPrefix)) - } - t.PublicId = opts.withPublicId - } else { - id, err := newTcpTargetId() - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op) - } - t.PublicId = id - } - - oplogWrapper, err := r.kms.GetWrapper(ctx, target.ScopeId, kms.KeyPurposeOplog) - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) - } - - newHostSets := make([]interface{}, 0, len(opts.withHostSources)) - for _, hsId := range opts.withHostSources { - hostSet, err := NewTargetHostSet(t.PublicId, hsId) - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory target host set")) - } - newHostSets = append(newHostSets, hostSet) - } - - newCredLibs := make([]interface{}, 0, len(opts.withCredentialSources)) - for _, clId := range opts.withCredentialSources { - credLib, err := NewCredentialLibrary(t.PublicId, clId) - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory target credential library")) - } - newCredLibs = append(newCredLibs, credLib) - } - - metadata := t.oplog(oplog.OpType_OP_TYPE_CREATE) - var returnedTarget interface{} - var returnedHostSources []HostSource - var returnedCredSources []CredentialSource - _, err = r.writer.DoTx( - ctx, - db.StdRetryCnt, - db.ExpBackoff{}, - func(read db.Reader, w db.Writer) error { - targetTicket, err := w.GetTicket(t) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) - } - msgs := make([]*oplog.Message, 0, 2) - var targetOplogMsg oplog.Message - returnedTarget = t.Clone() - if err := w.Create(ctx, returnedTarget, db.NewOplogMsg(&targetOplogMsg)); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target")) - } - msgs = append(msgs, &targetOplogMsg) - if len(newHostSets) > 0 { - hostSetOplogMsgs := make([]*oplog.Message, 0, len(newHostSets)) - if err := w.CreateItems(ctx, newHostSets, db.NewOplogMsgs(&hostSetOplogMsgs)); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add host sets")) - } - if returnedHostSources, err = fetchHostSources(ctx, read, t.PublicId); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to read host sources")) - } - msgs = append(msgs, hostSetOplogMsgs...) - } - if len(newCredLibs) > 0 { - credLibOplogMsgs := make([]*oplog.Message, 0, len(newCredLibs)) - if err := w.CreateItems(ctx, newCredLibs, db.NewOplogMsgs(&credLibOplogMsgs)); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add credential sources")) - } - if returnedCredSources, err = fetchCredentialSources(ctx, read, t.PublicId); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to read credential sources")) - } - msgs = append(msgs, credLibOplogMsgs...) - } - if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) - } - - return nil - }, - ) - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s target id", t.PublicId))) - } - return returnedTarget.(*TcpTarget), returnedHostSources, returnedCredSources, nil -} - -// UpdateTcpTarget will update a target in the repository and return the written -// target. 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 WorkerFilter are the only -// updatable fields. If no updatable fields are included in the fieldMaskPaths, -// then an error is returned. -func (r *Repository) UpdateTcpTarget(ctx context.Context, target *TcpTarget, version uint32, fieldMaskPaths []string, _ ...Option) (Target, []HostSource, []CredentialSource, int, error) { - const op = "target.(Repository).UpdateTcpTarget" - if target == nil { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target") - } - if target.TcpTarget == nil { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target store") - } - if target.PublicId == "" { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target public id") - } - for _, f := range fieldMaskPaths { - switch { - case strings.EqualFold("name", f): - case strings.EqualFold("description", f): - case strings.EqualFold("defaultport", f): - case strings.EqualFold("sessionmaxseconds", f): - case strings.EqualFold("sessionconnectionlimit", f): - case strings.EqualFold("workerfilter", f): - default: - return nil, nil, 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{}{ - "Name": target.Name, - "Description": target.Description, - "DefaultPort": target.DefaultPort, - "SessionMaxSeconds": target.SessionMaxSeconds, - "SessionConnectionLimit": target.SessionConnectionLimit, - "WorkerFilter": target.WorkerFilter, - }, - fieldMaskPaths, - []string{"SessionMaxSeconds", "SessionConnectionLimit"}, - ) - if len(dbMask) == 0 && len(nullFields) == 0 { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask") - } - var returnedTarget Target - var rowsUpdated int - var hostSources []HostSource - var credSources []CredentialSource - _, err := r.writer.DoTx( - ctx, - db.StdRetryCnt, - db.ExpBackoff{}, - func(read db.Reader, w db.Writer) error { - var err error - t := target.Clone().(*TcpTarget) - returnedTarget, hostSources, credSources, rowsUpdated, err = r.update(ctx, t, version, dbMask, nullFields) - if err != nil { - return errors.Wrap(ctx, err, op) - } - return nil - }, - ) - if err != nil { - if errors.IsUniqueError(err) { - return nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("target %s already exists in scope %s", target.Name, target.ScopeId)) - } - return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", target.PublicId))) - } - return returnedTarget, hostSources, credSources, rowsUpdated, nil -} diff --git a/internal/target/repository_test.go b/internal/target/repository_test.go index 7530bd73a..223bdf72f 100644 --- a/internal/target/repository_test.go +++ b/internal/target/repository_test.go @@ -1,17 +1,11 @@ -package target +package target_test import ( - "context" - "fmt" - "strconv" "testing" - "time" "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -28,9 +22,9 @@ func TestNewRepository(t *testing.T) { kms *kms.Kms } tests := []struct { - name string - args args - want *Repository + name string + args args + // want *target.Repository wantErr bool wantErrString string }{ @@ -41,12 +35,12 @@ func TestNewRepository(t *testing.T) { w: rw, kms: testKms, }, - want: &Repository{ - reader: rw, - writer: rw, - kms: testKms, - defaultLimit: db.DefaultLimit, - }, + // want: &target.Repository{ + // reader: rw, + // writer: rw, + // kms: testKms, + // defaultLimit: db.DefaultLimit, + // }, wantErr: false, }, { @@ -56,7 +50,7 @@ func TestNewRepository(t *testing.T) { w: rw, kms: nil, }, - want: nil, + // want: nil, wantErr: true, wantErrString: "target.NewRepository: nil kms: parameter violation: error #100", }, @@ -67,7 +61,7 @@ func TestNewRepository(t *testing.T) { w: nil, kms: testKms, }, - want: nil, + // want: nil, wantErr: true, wantErrString: "target.NewRepository: nil writer: parameter violation: error #100", }, @@ -78,7 +72,7 @@ func TestNewRepository(t *testing.T) { w: rw, kms: testKms, }, - want: nil, + // want: nil, wantErr: true, wantErrString: "target.NewRepository: nil reader: parameter violation: error #100", }, @@ -86,343 +80,343 @@ func TestNewRepository(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms) + got, err := target.NewRepository(tt.args.r, tt.args.w, tt.args.kms) if tt.wantErr { require.Error(err) assert.Equal(tt.wantErrString, err.Error()) return } require.NoError(err) - assert.Equal(tt.want, got) + assert.NotNil(got) + // assert.Equal(tt.want, got) }) } } -func TestRepository_LookupTarget(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - testKms := kms.TestKms(t, conn, wrapper) - iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj := iam.TestScopes(t, iamRepo) - proj.Name = "project-name" - _, _, err := iamRepo.UpdateScope(context.Background(), proj, 1, []string{"name"}) - require.NoError(t, err) - rw := db.New(conn) - repo, err := NewRepository(rw, rw, testKms) - require.NoError(t, err) - tgt := TestTcpTarget(t, conn, proj.PublicId, "target-name") - - tests := []struct { - testName string - id string - name string - scopeId string - scopeName string - wantErr bool - }{ - { - testName: "id", - id: tgt.PublicId, - wantErr: false, - }, - { - testName: "name only", - name: tgt.Name, - wantErr: true, - }, - { - testName: "scope id only", - scopeId: proj.PublicId, - wantErr: true, - }, - { - testName: "scope name only", - scopeName: proj.Name, - wantErr: true, - }, - { - testName: "scope name and id", - scopeId: proj.PublicId, - scopeName: proj.Name, - wantErr: true, - }, - { - testName: "everything", - name: tgt.Name, - scopeId: proj.PublicId, - scopeName: proj.Name, - wantErr: true, - }, - { - testName: "name and scope name", - name: tgt.Name, - scopeName: proj.Name, - wantErr: false, - }, - { - testName: "name and scope id", - name: tgt.Name, - scopeId: proj.PublicId, - wantErr: false, - }, - { - testName: "id and name", - id: tgt.PublicId, - name: tgt.Name, - scopeId: proj.PublicId, - wantErr: true, - }, - { - testName: "id and scope name", - id: tgt.PublicId, - scopeName: proj.Name, - wantErr: true, - }, - { - testName: "id and scope id", - id: tgt.PublicId, - scopeId: proj.PublicId, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - id := tt.id - if tt.name != "" && tt.id == "" { - id = tt.name - } - var opts []Option - if tt.name != "" { - opts = append(opts, WithName(tt.name)) - } - if tt.scopeId != "" { - opts = append(opts, WithScopeId(tt.scopeId)) - } - if tt.scopeName != "" { - opts = append(opts, WithScopeName(tt.scopeName)) - } - got, _, _, err := repo.LookupTarget(context.Background(), id, opts...) - if tt.wantErr { - require.Error(err) - return - } - require.NoError(err) - assert.Equal(tgt.PublicId, got.GetPublicId()) - }) - } -} - -func TestRepository_ListTargets(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - const testLimit = 10 - wrapper := db.TestWrapper(t) - testKms := kms.TestKms(t, conn, wrapper) - iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj := iam.TestScopes(t, iamRepo) - rw := db.New(conn) - repo, err := NewRepository(rw, rw, testKms) - require.NoError(t, err) - repo.defaultLimit = testLimit - - type args struct { - opt []Option - } - tests := []struct { - name string - createCnt int - createScopeId string - createScopeId2 string - grantUserId string - args args - wantCnt int - wantErr bool - }{ - { - name: "tcp-target", - createCnt: 5, - createScopeId: proj.PublicId, - args: args{ - opt: []Option{WithTargetType(TcpTargetType), WithScopeIds([]string{proj.PublicId})}, - }, - wantCnt: 5, - wantErr: false, - }, - { - name: "no-limit", - createCnt: testLimit + 1, - createScopeId: proj.PublicId, - args: args{ - opt: []Option{WithLimit(-1), WithScopeIds([]string{proj.PublicId})}, - }, - wantCnt: testLimit + 1, - wantErr: false, - }, - { - name: "default-limit", - createCnt: testLimit + 1, - createScopeId: proj.PublicId, - args: args{ - opt: []Option{WithScopeIds([]string{proj.PublicId})}, - }, - wantCnt: testLimit, - wantErr: false, - }, - { - name: "custom-limit", - createCnt: testLimit + 1, - createScopeId: proj.PublicId, - args: args{ - opt: []Option{WithLimit(3), WithScopeIds([]string{proj.PublicId})}, - }, - wantCnt: 3, - wantErr: false, - }, - { - name: "bad-org", - createCnt: 1, - createScopeId: proj.PublicId, - args: args{ - opt: []Option{WithScopeIds([]string{"bad-id"})}, - }, - wantCnt: 0, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - require.NoError(conn.Where("1=1").Delete(allocTcpTarget()).Error) - testGroups := []*TcpTarget{} - for i := 0; i < tt.createCnt; i++ { - switch { - case tt.createScopeId2 != "" && i%2 == 0: - testGroups = append(testGroups, TestTcpTarget(t, conn, tt.createScopeId2, strconv.Itoa(i))) - default: - testGroups = append(testGroups, TestTcpTarget(t, conn, tt.createScopeId, strconv.Itoa(i))) - } - } - assert.Equal(tt.createCnt, len(testGroups)) - got, err := repo.ListTargets(context.Background(), tt.args.opt...) - if tt.wantErr { - require.Error(err) - return - } - require.NoError(err) - assert.Equal(tt.wantCnt, len(got)) - }) - } -} - -func TestRepository_ListRoles_Multiple_Scopes(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - testKms := kms.TestKms(t, conn, wrapper) - iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj1 := iam.TestScopes(t, iamRepo) - _, proj2 := iam.TestScopes(t, iamRepo) - rw := db.New(conn) - repo, err := NewRepository(rw, rw, testKms) - require.NoError(t, err) - - require.NoError(t, conn.Where("1=1").Delete(allocTcpTarget()).Error) - - const numPerScope = 10 - var total int - for i := 0; i < numPerScope; i++ { - TestTcpTarget(t, conn, proj1.GetPublicId(), fmt.Sprintf("proj1-%d", i)) - total++ - TestTcpTarget(t, conn, proj2.GetPublicId(), fmt.Sprintf("proj2-%d", i)) - total++ - } - - got, err := repo.ListTargets(context.Background(), WithScopeIds([]string{"global", proj1.GetPublicId(), proj2.GetPublicId()})) - require.NoError(t, err) - assert.Equal(t, total, len(got)) -} - -func TestRepository_DeleteTarget(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - testKms := kms.TestKms(t, conn, wrapper) - iamRepo := iam.TestRepo(t, conn, wrapper) - _, proj := iam.TestScopes(t, iamRepo) - repo, err := NewRepository(rw, rw, testKms) - require.NoError(t, err) - - type args struct { - target Target - opt []Option - } - tests := []struct { - name string - args args - wantRowsDeleted int - wantErr bool - wantErrMsg string - }{ - { - name: "valid", - args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "valid"), - }, - wantRowsDeleted: 1, - wantErr: false, - }, - { - name: "no-public-id", - args: args{ - target: func() Target { - target := allocTcpTarget() - return &target - }(), - }, - wantRowsDeleted: 0, - wantErr: true, - wantErrMsg: "target.(Repository).DeleteTarget: missing public id: parameter violation: error #100", - }, - { - name: "not-found", - args: args{ - target: func() Target { - id, err := newTcpTargetId() - require.NoError(t, err) - target := allocTcpTarget() - target.PublicId = id - return &target - }(), - }, - wantRowsDeleted: 0, - wantErr: true, - wantErrMsg: "db.LookupById: record not found, search issue: error #1100", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert := assert.New(t) - deletedRows, err := repo.DeleteTarget(context.Background(), tt.args.target.GetPublicId(), tt.args.opt...) - if tt.wantErr { - assert.Error(err) - assert.Equal(0, deletedRows) - assert.Contains(err.Error(), tt.wantErrMsg) - err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) - assert.Error(err) - assert.True(errors.IsNotFoundError(err)) - return - } - assert.NoError(err) - assert.Equal(tt.wantRowsDeleted, deletedRows) - foundGroup, _, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) - assert.NoError(err) - assert.Nil(foundGroup) - - err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) - assert.NoError(err) - }) - } -} +// func TestRepository_LookupTarget(t *testing.T) { +// t.Parallel() +// conn, _ := db.TestSetup(t, "postgres") +// wrapper := db.TestWrapper(t) +// testKms := kms.TestKms(t, conn, wrapper) +// iamRepo := iam.TestRepo(t, conn, wrapper) +// _, proj := iam.TestScopes(t, iamRepo) +// proj.Name = "project-name" +// _, _, err := iamRepo.UpdateScope(context.Background(), proj, 1, []string{"name"}) +// require.NoError(t, err) +// rw := db.New(conn) +// repo, err := target.NewRepository(rw, rw, testKms) +// require.NoError(t, err) +// tgt := target.TestTcpTarget(t, conn, proj.PublicId, "target-name") +// +// tests := []struct { +// testName string +// id string +// name string +// scopeId string +// scopeName string +// wantErr bool +// }{ +// { +// testName: "id", +// id: tgt.PublicId, +// wantErr: false, +// }, +// { +// testName: "name only", +// name: tgt.Name, +// wantErr: true, +// }, +// { +// testName: "scope id only", +// scopeId: proj.PublicId, +// wantErr: true, +// }, +// { +// testName: "scope name only", +// scopeName: proj.Name, +// wantErr: true, +// }, +// { +// testName: "scope name and id", +// scopeId: proj.PublicId, +// scopeName: proj.Name, +// wantErr: true, +// }, +// { +// testName: "everything", +// name: tgt.Name, +// scopeId: proj.PublicId, +// scopeName: proj.Name, +// wantErr: true, +// }, +// { +// testName: "name and scope name", +// name: tgt.Name, +// scopeName: proj.Name, +// wantErr: false, +// }, +// { +// testName: "name and scope id", +// name: tgt.Name, +// scopeId: proj.PublicId, +// wantErr: false, +// }, +// { +// testName: "id and name", +// id: tgt.PublicId, +// name: tgt.Name, +// scopeId: proj.PublicId, +// wantErr: true, +// }, +// { +// testName: "id and scope name", +// id: tgt.PublicId, +// scopeName: proj.Name, +// wantErr: true, +// }, +// { +// testName: "id and scope id", +// id: tgt.PublicId, +// scopeId: proj.PublicId, +// wantErr: true, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.testName, func(t *testing.T) { +// assert, require := assert.New(t), require.New(t) +// id := tt.id +// if tt.name != "" && tt.id == "" { +// id = tt.name +// } +// var opts []target.Option +// if tt.name != "" { +// opts = append(opts, target.WithName(tt.name)) +// } +// if tt.scopeId != "" { +// opts = append(opts, target.WithScopeId(tt.scopeId)) +// } +// if tt.scopeName != "" { +// opts = append(opts, target.WithScopeName(tt.scopeName)) +// } +// got, _, _, err := repo.LookupTarget(context.Background(), id, opts...) +// if tt.wantErr { +// require.Error(err) +// return +// } +// require.NoError(err) +// assert.Equal(tgt.PublicId, got.GetPublicId()) +// }) +// } +// } +// +// func TestRepository_ListTargets(t *testing.T) { +// t.Parallel() +// conn, _ := db.TestSetup(t, "postgres") +// const testLimit = 10 +// wrapper := db.TestWrapper(t) +// testKms := kms.TestKms(t, conn, wrapper) +// iamRepo := iam.TestRepo(t, conn, wrapper) +// _, proj := iam.TestScopes(t, iamRepo) +// rw := db.New(conn) +// repo, err := target.NewRepository(rw, rw, testKms, target.WithLimit(testLimit)) +// require.NoError(t, err) +// +// type args struct { +// opt []target.Option +// } +// tests := []struct { +// name string +// createCnt int +// createScopeId string +// createScopeId2 string +// grantUserId string +// args args +// wantCnt int +// wantErr bool +// }{ +// { +// name: "tcp-target", +// createCnt: 5, +// createScopeId: proj.PublicId, +// args: args{ +// opt: []target.Option{target.WithTargetType(target.TcpTargetType), target.WithScopeIds([]string{proj.PublicId})}, +// }, +// wantCnt: 5, +// wantErr: false, +// }, +// { +// name: "no-limit", +// createCnt: testLimit + 1, +// createScopeId: proj.PublicId, +// args: args{ +// opt: []target.Option{target.WithLimit(-1), target.WithScopeIds([]string{proj.PublicId})}, +// }, +// wantCnt: testLimit + 1, +// wantErr: false, +// }, +// { +// name: "default-limit", +// createCnt: testLimit + 1, +// createScopeId: proj.PublicId, +// args: args{ +// opt: []target.Option{target.WithScopeIds([]string{proj.PublicId})}, +// }, +// wantCnt: testLimit, +// wantErr: false, +// }, +// { +// name: "custom-limit", +// createCnt: testLimit + 1, +// createScopeId: proj.PublicId, +// args: args{ +// opt: []target.Option{target.WithLimit(3), target.WithScopeIds([]string{proj.PublicId})}, +// }, +// wantCnt: 3, +// wantErr: false, +// }, +// { +// name: "bad-org", +// createCnt: 1, +// createScopeId: proj.PublicId, +// args: args{ +// opt: []target.Option{target.WithScopeIds([]string{"bad-id"})}, +// }, +// wantCnt: 0, +// wantErr: false, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// assert, require := assert.New(t), require.New(t) +// require.NoError(conn.Where("1=1").Delete(target.NewTestTcpTarget("")).Error) +// testGroups := []*target.TcpTarget{} +// for i := 0; i < tt.createCnt; i++ { +// switch { +// case tt.createScopeId2 != "" && i%2 == 0: +// testGroups = append(testGroups, target.TestTcpTarget(t, conn, tt.createScopeId2, strconv.Itoa(i))) +// default: +// testGroups = append(testGroups, target.TestTcpTarget(t, conn, tt.createScopeId, strconv.Itoa(i))) +// } +// } +// assert.Equal(tt.createCnt, len(testGroups)) +// got, err := repo.ListTargets(context.Background(), tt.args.opt...) +// if tt.wantErr { +// require.Error(err) +// return +// } +// require.NoError(err) +// assert.Equal(tt.wantCnt, len(got)) +// }) +// } +// } +// +// func TestRepository_ListRoles_Multiple_Scopes(t *testing.T) { +// t.Parallel() +// conn, _ := db.TestSetup(t, "postgres") +// wrapper := db.TestWrapper(t) +// testKms := kms.TestKms(t, conn, wrapper) +// iamRepo := iam.TestRepo(t, conn, wrapper) +// _, proj1 := iam.TestScopes(t, iamRepo) +// _, proj2 := iam.TestScopes(t, iamRepo) +// rw := db.New(conn) +// repo, err := target.NewRepository(rw, rw, testKms) +// require.NoError(t, err) +// +// require.NoError(t, conn.Where("1=1").Delete(target.NewTestTcpTarget("")).Error) +// +// const numPerScope = 10 +// var total int +// for i := 0; i < numPerScope; i++ { +// target.TestTcpTarget(t, conn, proj1.GetPublicId(), fmt.Sprintf("proj1-%d", i)) +// total++ +// target.TestTcpTarget(t, conn, proj2.GetPublicId(), fmt.Sprintf("proj2-%d", i)) +// total++ +// } +// +// got, err := repo.ListTargets(context.Background(), target.WithScopeIds([]string{"global", proj1.GetPublicId(), proj2.GetPublicId()})) +// require.NoError(t, err) +// assert.Equal(t, total, len(got)) +// } +// +// func TestRepository_DeleteTarget(t *testing.T) { +// t.Parallel() +// conn, _ := db.TestSetup(t, "postgres") +// rw := db.New(conn) +// wrapper := db.TestWrapper(t) +// testKms := kms.TestKms(t, conn, wrapper) +// iamRepo := iam.TestRepo(t, conn, wrapper) +// _, proj := iam.TestScopes(t, iamRepo) +// repo, err := target.NewRepository(rw, rw, testKms) +// require.NoError(t, err) +// +// type args struct { +// target target.Target +// opt []target.Option +// } +// tests := []struct { +// name string +// args args +// wantRowsDeleted int +// wantErr bool +// wantErrMsg string +// }{ +// { +// name: "valid", +// args: args{ +// target: target.TestTcpTarget(t, conn, proj.PublicId, "valid"), +// }, +// wantRowsDeleted: 1, +// wantErr: false, +// }, +// { +// name: "no-public-id", +// args: args{ +// target: func() target.Target { +// tar, _ := target.NewTcpTarget(proj.PublicId) +// return tar +// }(), +// }, +// wantRowsDeleted: 0, +// wantErr: true, +// wantErrMsg: "target.(Repository).DeleteTarget: missing public id: parameter violation: error #100", +// }, +// { +// name: "not-found", +// args: args{ +// target: func() target.Target { +// id, err := target.NewTcpTargetId() +// require.NoError(t, err) +// tar, _ := target.NewTcpTarget(proj.PublicId) +// tar.PublicId = id +// return tar +// }(), +// }, +// wantRowsDeleted: 0, +// wantErr: true, +// wantErrMsg: "db.LookupById: record not found, search issue: error #1100", +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// assert := assert.New(t) +// deletedRows, err := repo.DeleteTarget(context.Background(), tt.args.target.GetPublicId(), tt.args.opt...) +// if tt.wantErr { +// assert.Error(err) +// assert.Equal(0, deletedRows) +// assert.Contains(err.Error(), tt.wantErrMsg) +// err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) +// assert.Error(err) +// assert.True(errors.IsNotFoundError(err)) +// return +// } +// assert.NoError(err) +// assert.Equal(tt.wantRowsDeleted, deletedRows) +// foundGroup, _, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) +// assert.NoError(err) +// assert.Nil(foundGroup) +// +// err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) +// assert.NoError(err) +// }) +// } +// } diff --git a/internal/target/store/target.pb.go b/internal/target/store/target.pb.go index e248b8cd8..c3e8acdfd 100644 --- a/internal/target/store/target.pb.go +++ b/internal/target/store/target.pb.go @@ -253,157 +253,6 @@ func (x *TargetHostSet) GetCreateTime() *timestamp.Timestamp { return nil } -type TcpTarget struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // public_id is used to access the TargetTcp via an 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"` - // scope id for the TargetTcp - // @inject_tag: `gorm:"default:null"` - ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` - // name is the optional friendly name used to - // access the TargetTcp via an API - // @inject_tag: `gorm:"default:null"` - Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` - // description of the TargetTcp - // @inject_tag: `gorm:"default:null"` - Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` - // create_time from the RDBMS - // @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"` - // update_time from the RDBMS - // @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"` - // version allows optimistic locking of the TargetTcp when modifying the - // TargetTcp - // @inject_tag: `gorm:"default:null"` - Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` - // default port of the TargetTcp - // @inject_tag: `gorm:"default:null"` - DefaultPort uint32 `protobuf:"varint,80,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty" gorm:"default:null"` - // Maximum total lifetime of a created session, in seconds - // @inject_tag: `gorm:"default:null"` - SessionMaxSeconds uint32 `protobuf:"varint,100,opt,name=session_max_seconds,json=sessionMaxSeconds,proto3" json:"session_max_seconds,omitempty" gorm:"default:null"` - // Maximum number of connections in a session - // @inject_tag: `gorm:"default:null"` - SessionConnectionLimit int32 `protobuf:"varint,110,opt,name=session_connection_limit,json=sessionConnectionLimit,proto3" json:"session_connection_limit,omitempty" gorm:"default:null"` - // A boolean expression that allows filtering the workers that can handle a session - // @inject_tag: `gorm:"default:null"` - WorkerFilter string `protobuf:"bytes,120,opt,name=worker_filter,json=workerFilter,proto3" json:"worker_filter,omitempty" gorm:"default:null"` -} - -func (x *TcpTarget) Reset() { - *x = TcpTarget{} - if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TcpTarget) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TcpTarget) ProtoMessage() {} - -func (x *TcpTarget) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_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 TcpTarget.ProtoReflect.Descriptor instead. -func (*TcpTarget) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{2} -} - -func (x *TcpTarget) GetPublicId() string { - if x != nil { - return x.PublicId - } - return "" -} - -func (x *TcpTarget) GetScopeId() string { - if x != nil { - return x.ScopeId - } - return "" -} - -func (x *TcpTarget) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TcpTarget) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *TcpTarget) GetCreateTime() *timestamp.Timestamp { - if x != nil { - return x.CreateTime - } - return nil -} - -func (x *TcpTarget) GetUpdateTime() *timestamp.Timestamp { - if x != nil { - return x.UpdateTime - } - return nil -} - -func (x *TcpTarget) GetVersion() uint32 { - if x != nil { - return x.Version - } - return 0 -} - -func (x *TcpTarget) GetDefaultPort() uint32 { - if x != nil { - return x.DefaultPort - } - return 0 -} - -func (x *TcpTarget) GetSessionMaxSeconds() uint32 { - if x != nil { - return x.SessionMaxSeconds - } - return 0 -} - -func (x *TcpTarget) GetSessionConnectionLimit() int32 { - if x != nil { - return x.SessionConnectionLimit - } - return 0 -} - -func (x *TcpTarget) GetWorkerFilter() string { - if x != nil { - return x.WorkerFilter - } - return "" -} - type CredentialLibrary struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -426,7 +275,7 @@ type CredentialLibrary struct { func (x *CredentialLibrary) Reset() { *x = CredentialLibrary{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -439,7 +288,7 @@ func (x *CredentialLibrary) String() string { func (*CredentialLibrary) ProtoMessage() {} func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[3] + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -452,7 +301,7 @@ func (x *CredentialLibrary) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialLibrary.ProtoReflect.Descriptor instead. func (*CredentialLibrary) Descriptor() ([]byte, []int) { - return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{3} + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{2} } func (x *CredentialLibrary) GetTargetId() string { @@ -538,70 +387,25 @@ var file_controller_storage_target_store_v1_target_proto_rawDesc = []byte{ 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, 0x22, 0xc6, 0x05, 0x0a, 0x09, 0x54, 0x63, 0x70, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 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, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x1e, 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, 0x28, 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, 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, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, - 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x2a, 0xc2, - 0xdd, 0x29, 0x26, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, - 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x5c, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0d, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x11, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x13, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, - 0x73, 0x52, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, - 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x70, 0x0a, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x18, 0x6e, 0x20, 0x01, 0x28, 0x05, 0x42, 0x36, 0xc2, 0xdd, 0x29, 0x32, 0x0a, 0x16, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, - 0x69, 0x6d, 0x69, 0x74, 0x12, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x16, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x46, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, - 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x42, 0x21, 0xc2, - 0xdd, 0x29, 0x1d, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x12, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x52, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xe0, - 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, - 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, - 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, - 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, - 0x70, 0x6f, 0x73, 0x65, 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, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, - 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x72, 0x70, 0x6f, + 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 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, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -616,26 +420,23 @@ func file_controller_storage_target_store_v1_target_proto_rawDescGZIP() []byte { return file_controller_storage_target_store_v1_target_proto_rawDescData } -var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_controller_storage_target_store_v1_target_proto_goTypes = []interface{}{ (*TargetView)(nil), // 0: controller.storage.target.store.v1.TargetView (*TargetHostSet)(nil), // 1: controller.storage.target.store.v1.TargetHostSet - (*TcpTarget)(nil), // 2: controller.storage.target.store.v1.TcpTarget - (*CredentialLibrary)(nil), // 3: controller.storage.target.store.v1.CredentialLibrary - (*timestamp.Timestamp)(nil), // 4: controller.storage.timestamp.v1.Timestamp + (*CredentialLibrary)(nil), // 2: controller.storage.target.store.v1.CredentialLibrary + (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_target_store_v1_target_proto_depIdxs = []int32{ - 4, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 4, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 4, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 4, // 3: controller.storage.target.store.v1.TcpTarget.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 4, // 4: controller.storage.target.store.v1.TcpTarget.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 4, // 5: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 3, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 3: controller.storage.target.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 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 } func init() { file_controller_storage_target_store_v1_target_proto_init() } @@ -669,18 +470,6 @@ func file_controller_storage_target_store_v1_target_proto_init() { } } file_controller_storage_target_store_v1_target_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TcpTarget); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_controller_storage_target_store_v1_target_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CredentialLibrary); i { case 0: return &v.state @@ -699,7 +488,7 @@ func file_controller_storage_target_store_v1_target_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_target_store_v1_target_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/target/subtype.go b/internal/target/subtype.go deleted file mode 100644 index 79539757d..000000000 --- a/internal/target/subtype.go +++ /dev/null @@ -1,41 +0,0 @@ -package target - -import ( - "strings" -) - -type Subtype int - -const ( - UnknownSubtype Subtype = iota - TcpSubtype -) - -func (t Subtype) String() string { - switch t { - case TcpSubtype: - return "tcp" - } - return "unknown" -} - -// SubtypeFromType converts a string to a Subtype. -// returns UnknownSubtype if no Subtype with that name is found. -func SubtypeFromType(t string) Subtype { - switch { - case strings.EqualFold(strings.TrimSpace(t), TcpSubtype.String()): - return TcpSubtype - } - return UnknownSubtype -} - -// SubtypeFromId takes any public id in the target subsystem and uses the prefix to determine -// what subtype the id is for. -// Returns UnknownSubtype if no Subtype with this id's prefix is found. -func SubtypeFromId(id string) Subtype { - switch { - case strings.HasPrefix(strings.TrimSpace(id), TcpTargetPrefix): - return TcpSubtype - } - return UnknownSubtype -} diff --git a/internal/target/target.go b/internal/target/target.go index 7b27117d9..fc459a389 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -1,12 +1,14 @@ package target import ( + "context" "fmt" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/target/store" + "github.com/hashicorp/boundary/internal/types/subtypes" ) // Target is a commmon interface for all target subtypes @@ -17,13 +19,25 @@ type Target interface { GetName() string GetDescription() string GetVersion() uint32 - GetType() string + GetType() subtypes.Subtype GetCreateTime() *timestamp.Timestamp GetUpdateTime() *timestamp.Timestamp GetSessionMaxSeconds() uint32 GetSessionConnectionLimit() int32 GetWorkerFilter() string - oplog(op oplog.OpType) oplog.Metadata + Clone() Target + SetPublicId(context.Context, string) error + SetScopeId(string) + SetName(string) + SetDescription(string) + SetVersion(uint32) + SetDefaultPort(uint32) + SetCreateTime(*timestamp.Timestamp) + SetUpdateTime(*timestamp.Timestamp) + SetSessionMaxSeconds(uint32) + SetSessionConnectionLimit(int32) + SetWorkerFilter(string) + Oplog(op oplog.OpType) oplog.Metadata } // TargetType defines the possible types for targets. @@ -79,24 +93,32 @@ func (t *targetView) SetTableName(n string) { } } -// targetSubtype converts the target view to the concrete subtype -func (t *targetView) targetSubtype() (Target, error) { - const op = "target.targetView.targetSubtype" - switch t.Type { - case TcpTargetType.String(): - tcpTarget := allocTcpTarget() - tcpTarget.PublicId = t.PublicId - tcpTarget.ScopeId = t.ScopeId - tcpTarget.Name = t.Name - tcpTarget.Description = t.Description - tcpTarget.DefaultPort = t.DefaultPort - tcpTarget.CreateTime = t.CreateTime - tcpTarget.UpdateTime = t.UpdateTime - tcpTarget.Version = t.Version - tcpTarget.SessionMaxSeconds = t.SessionMaxSeconds - tcpTarget.SessionConnectionLimit = t.SessionConnectionLimit - tcpTarget.WorkerFilter = t.WorkerFilter - return &tcpTarget, nil - } - return nil, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("%s is an unknown target subtype of %s", t.PublicId, t.Type)) +func (t *targetView) Subtype() subtypes.Subtype { + return subtypes.Subtype(t.Type) +} + +// targetSubtype converts the target view to the concrete subtype +func (t *targetView) targetSubtype(ctx context.Context) (Target, error) { + const op = "target.targetView.targetSubtype" + + alloc, ok := subtypeRegistry.allocFunc(t.Subtype()) + if !ok { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unknown target subtype of %s", t.PublicId, t.Type)) + } + + tt := alloc() + if err := tt.SetPublicId(ctx, t.PublicId); err != nil { + return nil, err + } + tt.SetVersion(t.Version) + tt.SetScopeId(t.ScopeId) + tt.SetName(t.Name) + tt.SetDescription(t.Description) + tt.SetDefaultPort(t.DefaultPort) + tt.SetCreateTime(t.CreateTime) + tt.SetUpdateTime(t.UpdateTime) + tt.SetSessionMaxSeconds(t.SessionMaxSeconds) + tt.SetSessionConnectionLimit(t.SessionConnectionLimit) + tt.SetWorkerFilter(t.WorkerFilter) + return tt, nil } diff --git a/internal/target/target_test.go b/internal/target/target_test.go index d3a287c38..a5366905d 100644 --- a/internal/target/target_test.go +++ b/internal/target/target_test.go @@ -1,15 +1,16 @@ -package target +package target_test import ( "testing" + "github.com/hashicorp/boundary/internal/target" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTarget_SetTableName(t *testing.T) { t.Parallel() - defaultTableName := targetsViewDefaultTable + defaultTableName := target.TargetsViewDefaultTable tests := []struct { name string setNameTo string @@ -29,9 +30,9 @@ func TestTarget_SetTableName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - def := allocTargetView() + def := target.AllocTargetView() require.Equal(defaultTableName, def.TableName()) - s := allocTargetView() + s := target.AllocTargetView() s.SetTableName(tt.setNameTo) assert.Equal(tt.want, s.TableName()) }) diff --git a/internal/target/tcp/exports_test.go b/internal/target/tcp/exports_test.go new file mode 100644 index 000000000..023d2683c --- /dev/null +++ b/internal/target/tcp/exports_test.go @@ -0,0 +1,19 @@ +package tcp + +import "github.com/hashicorp/boundary/internal/target" + +// Expose functions and variables for tests. +var ( + TestId = testId + TestTargetName = testTargetName + DefaultTableName = defaultTableName +) + +// NewTestTarget is a test helper that bypasses the scopeId checks +// performed by NewTarget, allowing tests to create Targets with +// nil scopeIds for more robust testing. +func NewTestTarget(scopeId string, opt ...target.Option) *Target { + t, _ := New("testScope", opt...) + t.ScopeId = scopeId + return t +} diff --git a/internal/target/immutable_fields_test.go b/internal/target/tcp/immutable_fields_test.go similarity index 75% rename from internal/target/immutable_fields_test.go rename to internal/target/tcp/immutable_fields_test.go index 865f1d3a1..f268806e1 100644 --- a/internal/target/immutable_fields_test.go +++ b/internal/target/tcp/immutable_fields_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -9,6 +9,8 @@ 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/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -24,17 +26,17 @@ func TestTarget_ImmutableFields(t *testing.T) { ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - new := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + new := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestId(t)) tests := []struct { name string - update *TcpTarget + update *tcp.Target fieldMask []string }{ { name: "public_id", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.PublicId = "p_thisIsNotAValidId" return target }(), @@ -42,8 +44,8 @@ func TestTarget_ImmutableFields(t *testing.T) { }, { name: "create time", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.CreateTime = &ts return target }(), @@ -51,8 +53,8 @@ func TestTarget_ImmutableFields(t *testing.T) { }, { name: "scope_id", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.ScopeId = "o_thisIsNotAValidId" return target }(), @@ -65,7 +67,7 @@ func TestTarget_ImmutableFields(t *testing.T) { assert, require := assert.New(t), require.New(t) orig := new.Clone() - orig.(*TcpTarget).SetTableName("target") + orig.(*tcp.Target).SetTableName("target") err := rw.LookupById(context.Background(), orig) require.NoError(err) @@ -75,11 +77,11 @@ func TestTarget_ImmutableFields(t *testing.T) { assert.Equal(0, rowsUpdated) after := new.Clone() - after.(*TcpTarget).SetTableName("target") + after.(*tcp.Target).SetTableName("target") err = rw.LookupById(context.Background(), after) require.NoError(err) - assert.True(proto.Equal(orig.(*TcpTarget), after.(*TcpTarget))) + assert.True(proto.Equal(orig.(*tcp.Target), after.(*tcp.Target))) }) } } @@ -93,17 +95,17 @@ func TestTcpTarget_ImmutableFields(t *testing.T) { ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - new := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + new := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestId(t)) tests := []struct { name string - update *TcpTarget + update *tcp.Target fieldMask []string }{ { name: "public_id", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.PublicId = "p_thisIsNotAValidId" return target }(), @@ -111,8 +113,8 @@ func TestTcpTarget_ImmutableFields(t *testing.T) { }, { name: "create time", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.CreateTime = &ts return target }(), @@ -120,8 +122,8 @@ func TestTcpTarget_ImmutableFields(t *testing.T) { }, { name: "scope_id", - update: func() *TcpTarget { - target := new.Clone().(*TcpTarget) + update: func() *tcp.Target { + target := new.Clone().(*tcp.Target) target.ScopeId = "o_thisIsNotAValidId" return target }(), @@ -144,7 +146,7 @@ func TestTcpTarget_ImmutableFields(t *testing.T) { err = rw.LookupById(context.Background(), after) require.NoError(err) - assert.True(proto.Equal(orig.(*TcpTarget), after.(*TcpTarget))) + assert.True(proto.Equal(orig.(*tcp.Target), after.(*tcp.Target))) }) } } @@ -155,35 +157,35 @@ func TestTargetHostSet_ImmutableFields(t *testing.T) { wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - projTarget := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + projTarget := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestId(t)) testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 2) require.Equal(t, 2, len(hsets)) - updateTarget := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + updateTarget := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestId(t)) updateHset := hsets[1] _, gotHostSources, _, err := repo.AddTargetHostSources(context.Background(), projTarget.PublicId, 1, []string{hsets[0].PublicId}) require.NoError(t, err) require.Equal(t, 1, len(gotHostSources)) - new, err := NewTargetHostSet(projTarget.PublicId, gotHostSources[0].Id()) + new, err := target.NewTargetHostSet(projTarget.PublicId, gotHostSources[0].Id()) require.NoError(t, err) tests := []struct { name string - update *TargetHostSet + update *target.TargetHostSet fieldMask []string }{ { name: "target_id", - update: func() *TargetHostSet { - target := new.Clone().(*TargetHostSet) + update: func() *target.TargetHostSet { + target := new.Clone().(*target.TargetHostSet) target.TargetId = updateTarget.PublicId return target }(), @@ -191,8 +193,8 @@ func TestTargetHostSet_ImmutableFields(t *testing.T) { }, { name: "create time", - update: func() *TargetHostSet { - target := new.Clone().(*TargetHostSet) + update: func() *target.TargetHostSet { + target := new.Clone().(*target.TargetHostSet) target.CreateTime = &ts return target }(), @@ -200,8 +202,8 @@ func TestTargetHostSet_ImmutableFields(t *testing.T) { }, { name: "host_set_id", - update: func() *TargetHostSet { - target := new.Clone().(*TargetHostSet) + update: func() *target.TargetHostSet { + target := new.Clone().(*target.TargetHostSet) target.HostSetId = updateHset.PublicId return target }(), @@ -223,7 +225,7 @@ func TestTargetHostSet_ImmutableFields(t *testing.T) { after := new.Clone() err = rw.LookupWhere(context.Background(), after, "target_id = ? and host_set_id = ?", new.TargetId, new.HostSetId) require.NoError(err) - assert.True(proto.Equal(orig.(*TargetHostSet), after.(*TargetHostSet))) + assert.True(proto.Equal(orig.(*target.TargetHostSet), after.(*target.TargetHostSet))) }) } } diff --git a/internal/target/tcp/register.go b/internal/target/tcp/register.go new file mode 100644 index 000000000..68508d430 --- /dev/null +++ b/internal/target/tcp/register.go @@ -0,0 +1,37 @@ +package tcp + +import ( + "context" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/target" +) + +func init() { + target.Register(Subtype, allocTarget, vet, TargetPrefix) +} + +const ( + // TargetPrefix is the prefix for public ids of a tcp.Target. + TargetPrefix = "ttcp" +) + +// vet validates that the given target.Target is a tcp.Target and that it +// has a Target store. +func vet(ctx context.Context, t target.Target) error { + const op = "tcp.vet" + + tt, ok := t.(*Target) + if !ok { + return errors.New(ctx, errors.InvalidParameter, op, "target is not a tcp.Target") + } + + if tt == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing target") + } + + if tt.Target == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing target store") + } + return nil +} diff --git a/internal/target/repository_credential_source_test.go b/internal/target/tcp/repository_credential_source_test.go similarity index 89% rename from internal/target/repository_credential_source_test.go rename to internal/target/tcp/repository_credential_source_test.go index 4a0a39cc1..ba0720773 100644 --- a/internal/target/repository_credential_source_test.go +++ b/internal/target/tcp/repository_credential_source_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "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" @@ -25,7 +27,7 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) _, staticProj := iam.TestScopes(t, iamRepo) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) cs := vault.TestCredentialStores(t, conn, wrapper, staticProj.GetPublicId(), 1)[0] @@ -103,7 +105,7 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - projTarget := TestTcpTarget(t, conn, staticProj.PublicId, tt.name) + projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, tt.name) gotTarget, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, tt.args.targetVersion, tt.args.credLibIds) if tt.wantErr { require.Error(err) @@ -112,7 +114,7 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { } require.NoError(err) assert.Len(gotCredSources, len(tt.wantCredLibIds)) - gotCredSourcesMap := map[string]CredentialSource{} + gotCredSourcesMap := map[string]target.CredentialSource{} for _, s := range gotCredSources { gotCredSourcesMap[s.Id()] = s } @@ -124,26 +126,22 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { err = db.TestVerifyOplog(t, rw, projTarget.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) - foundCredSources, err := fetchCredentialSources(context.Background(), rw, projTarget.PublicId) + tar, _, lookupCredSources, err := repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) - assert.Len(foundCredSources, len(gotCredSourcesMap)) - for _, s := range foundCredSources { + assert.Equal(tt.args.targetVersion+1, tar.GetVersion()) + assert.Equal(projTarget.GetVersion(), tar.GetVersion()-1) + assert.True(proto.Equal(gotTarget.(*tcp.Target), tar.(*tcp.Target))) + assert.Equal(gotCredSources, lookupCredSources) + for _, s := range lookupCredSources { assert.NotEmpty(gotCredSourcesMap[s.Id()]) assert.Equal(projTarget.PublicId, s.TargetId()) } - - target, _, lookupCredSources, err := repo.LookupTarget(context.Background(), projTarget.PublicId) - require.NoError(err) - assert.Equal(tt.args.targetVersion+1, target.GetVersion()) - assert.Equal(projTarget.GetVersion(), target.GetVersion()-1) - assert.True(proto.Equal(gotTarget.(*TcpTarget), target.(*TcpTarget))) - assert.Equal(gotCredSources, lookupCredSources) }) } t.Run("add-existing", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - projTarget := TestTcpTarget(t, conn, staticProj.PublicId, "add-existing") + projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, "add-existing") _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId}) require.NoError(err) assert.Len(gotCredSources, 1) @@ -160,10 +158,10 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { assert.True(errors.Match(errors.T(errors.NotUnique), err)) // Previous transactions should have been rolled back and only lib1 should be associated - gotCredSources, err = fetchCredentialSources(context.Background(), rw, projTarget.PublicId) + _, _, lookupCredSources, err := repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) - assert.Len(gotCredSources, 1) - assert.Equal(lib1.PublicId, gotCredSources[0].Id()) + assert.Len(lookupCredSources, 1) + assert.Equal(lib1.PublicId, lookupCredSources[0].Id()) }) t.Run("target-not-found", func(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -183,7 +181,7 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) _, proj := iam.TestScopes(t, iamRepo) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) type args struct { @@ -229,7 +227,7 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { { name: "not-found", args: args{ - targetIdOverride: func() *string { id := testId(t); return &id }(), + targetIdOverride: func() *string { id := tcp.TestId(t); return &id }(), createCnt: 5, deleteCnt: 5, }, @@ -277,7 +275,7 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { assert, require := assert.New(t), require.New(t) cs := css[i] - target := TestTcpTarget(t, conn, proj.PublicId, tt.name) + target := tcp.TestTarget(t, conn, proj.PublicId, tt.name) clIds := make([]string, 0, tt.args.createCnt) if tt.args.createCnt > 0 { @@ -334,7 +332,7 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { lib2 := libs[1] lib3 := libs[2] - projTarget := TestTcpTarget(t, conn, proj.PublicId, "add-existing") + projTarget := tcp.TestTarget(t, conn, proj.PublicId, "add-existing") _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId, lib2.PublicId}) require.NoError(err) assert.Len(gotCredSources, 2) @@ -352,9 +350,9 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { assert.Equal(0, delCount) // Previous transactions should have been rolled back and only lib1 should be associated - gotCredSources, err = fetchCredentialSources(context.Background(), rw, projTarget.PublicId) + _, _, lookupCredSources, err := repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) - assert.Len(gotCredSources, 2) + assert.Len(lookupCredSources, 2) }) } @@ -364,7 +362,7 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) iamRepo := iam.TestRepo(t, conn, wrapper) @@ -375,7 +373,7 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { lib1 := credLibs[0] lib2 := credLibs[1] - setupFn := func(target Target) []CredentialSource { + setupFn := func(target target.Target) []target.CredentialSource { credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 10) clIds := make([]string, 0, len(credLibs)) for _, cl := range credLibs { @@ -394,7 +392,7 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { } tests := []struct { name string - setup func(Target) []CredentialSource + setup func(target.Target) []target.CredentialSource args args wantAffectedRows int wantErr bool @@ -470,11 +468,11 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - target := TestTcpTarget(t, conn, proj.PublicId, tt.name) + tar := tcp.TestTarget(t, conn, proj.PublicId, tt.name) - var origCredSources []CredentialSource + var origCredSources []target.CredentialSource if tt.setup != nil { - origCredSources = tt.setup(target) + origCredSources = tt.setup(tar) } if tt.args.addToOrigLibs { origIds := make([]string, 0, len(origCredSources)) @@ -484,11 +482,11 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { tt.args.clIds = append(tt.args.clIds, origIds...) } - origTarget, _, lookupCredSources, err := repo.LookupTarget(context.Background(), target.GetPublicId()) + origTarget, _, lookupCredSources, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) require.NoError(err) assert.Equal(origCredSources, lookupCredSources) - _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), target.GetPublicId(), tt.args.targetVersion, tt.args.clIds) + _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), tar.GetPublicId(), tt.args.targetVersion, tt.args.clIds) if tt.wantErr { require.Error(err) assert.Equal(0, affectedRows) @@ -514,7 +512,7 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { sort.Strings(gotIds) assert.Equal(wantIds, gotIds) - foundTarget, _, _, err := repo.LookupTarget(context.Background(), target.GetPublicId()) + foundTarget, _, _, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) require.NoError(err) if tt.name != "no-change" { assert.Equalf(tt.args.targetVersion+1, foundTarget.GetVersion(), "%s unexpected version: %d/%d", tt.name, tt.args.targetVersion+1, foundTarget.GetVersion()) diff --git a/internal/target/repository_host_source_test.go b/internal/target/tcp/repository_host_source_test.go similarity index 85% rename from internal/target/repository_host_source_test.go rename to internal/target/tcp/repository_host_source_test.go index b5edc579d..d0a2b3fa9 100644 --- a/internal/target/repository_host_source_test.go +++ b/internal/target/tcp/repository_host_source_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -12,11 +12,20 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target" + targetstore "github.com/hashicorp/boundary/internal/target/store" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) +func allocTargetHostSet() target.TargetHostSet { + return target.TargetHostSet{ + TargetHostSet: &targetstore.TargetHostSet{}, + } +} + func TestRepository_AddTargetHostSets(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -25,7 +34,7 @@ func TestRepository_AddTargetHostSets(t *testing.T) { testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) staticOrg, staticProj := iam.TestScopes(t, iamRepo) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) createHostSetsFn := func(orgs, projects []string) []string { @@ -46,7 +55,7 @@ func TestRepository_AddTargetHostSets(t *testing.T) { type args struct { targetVersion uint32 wantTargetIds bool - opt []Option + opt []target.Option } tests := []struct { name string @@ -90,9 +99,9 @@ func TestRepository_AddTargetHostSets(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) require.NoError(conn.Where("1=1").Delete(allocTargetHostSet()).Error) - require.NoError(conn.Where("1=1").Delete(allocTcpTarget()).Error) + require.NoError(conn.Where("1=1").Delete(tcp.NewTestTarget("")).Error) - projTarget := TestTcpTarget(t, conn, staticProj.PublicId, "static-proj") + projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, "static-proj") var hostSourceIds []string origTarget, origHostSet, _, err := repo.LookupTarget(context.Background(), projTarget.PublicId) @@ -124,7 +133,7 @@ func TestRepository_AddTargetHostSets(t *testing.T) { return } require.NoError(err) - gotHostSet := map[string]HostSource{} + gotHostSet := map[string]target.HostSource{} for _, s := range gotHostSources { gotHostSet[s.Id()] = s } @@ -141,18 +150,15 @@ func TestRepository_AddTargetHostSets(t *testing.T) { err = db.TestVerifyOplog(t, rw, projTarget.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) - foundHostSources, err := fetchHostSources(context.Background(), rw, projTarget.PublicId) + tar, ths, _, err := repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) - for _, s := range foundHostSources { + assert.Equal(tt.args.targetVersion+1, tar.GetVersion()) + assert.Equal(origTarget.GetVersion(), tar.GetVersion()-1) + assert.Equal(gotHostSources, ths) + assert.True(proto.Equal(gotTarget.(*tcp.Target), tar.(*tcp.Target))) + for _, s := range ths { assert.NotEmpty(gotHostSet[s.Id()]) } - - target, ths, _, err := repo.LookupTarget(context.Background(), projTarget.PublicId) - require.NoError(err) - assert.Equal(tt.args.targetVersion+1, target.GetVersion()) - assert.Equal(origTarget.GetVersion(), target.GetVersion()-1) - assert.Equal(gotHostSources, ths) - assert.True(proto.Equal(gotTarget.(*TcpTarget), target.(*TcpTarget))) }) } t.Run("add-existing", func(t *testing.T) { @@ -164,7 +170,7 @@ func TestRepository_AddTargetHostSets(t *testing.T) { hs2 := hsets[1] hs3 := hsets[2] - projTarget := TestTcpTarget(t, conn, staticProj.PublicId, "add-existing") + projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, "add-existing") _, gotHostSources, _, err := repo.AddTargetHostSources(context.Background(), projTarget.PublicId, 1, []string{hs1.PublicId}) require.NoError(err) assert.Len(gotHostSources, 1) @@ -181,7 +187,7 @@ func TestRepository_AddTargetHostSets(t *testing.T) { assert.True(errors.Match(errors.T(errors.NotUnique), err)) // Previous transactions should have been rolled back and only hs1 should be associated - gotHostSources, err = fetchHostSources(context.Background(), rw, projTarget.PublicId) + _, gotHostSources, _, err = repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) assert.Len(gotHostSources, 1) assert.Equal(hs1.PublicId, gotHostSources[0].Id()) @@ -196,16 +202,16 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) _, proj := iam.TestScopes(t, iamRepo) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) type args struct { - target Target + target target.Target targetIdOverride *string targetVersionOverride *uint32 createCnt int deleteCnt int - opt []Option + opt []target.Option } tests := []struct { name string @@ -217,7 +223,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "valid", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "valid"), + target: tcp.TestTarget(t, conn, proj.PublicId, "valid"), createCnt: 5, deleteCnt: 5, }, @@ -227,7 +233,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "valid-keeping-some", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "valid-keeping-some"), + target: tcp.TestTarget(t, conn, proj.PublicId, "valid-keeping-some"), createCnt: 5, deleteCnt: 2, }, @@ -237,7 +243,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "no-deletes", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "no-deletes"), + target: tcp.TestTarget(t, conn, proj.PublicId, "no-deletes"), createCnt: 5, }, wantRowsDeleted: 0, @@ -247,8 +253,8 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "not-found", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "not-found"), - targetIdOverride: func() *string { id := testId(t); return &id }(), + target: tcp.TestTarget(t, conn, proj.PublicId, "not-found"), + targetIdOverride: func() *string { id := tcp.TestId(t); return &id }(), createCnt: 5, deleteCnt: 5, }, @@ -258,7 +264,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "missing-target-id", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "missing-target-id"), + target: tcp.TestTarget(t, conn, proj.PublicId, "missing-target-id"), targetIdOverride: func() *string { id := ""; return &id }(), createCnt: 5, deleteCnt: 5, @@ -270,7 +276,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "zero-version", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "zero-version"), + target: tcp.TestTarget(t, conn, proj.PublicId, "zero-version"), targetVersionOverride: func() *uint32 { v := uint32(0); return &v }(), createCnt: 5, deleteCnt: 5, @@ -282,7 +288,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { { name: "bad-version", args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "bad-version"), + target: tcp.TestTarget(t, conn, proj.PublicId, "bad-version"), targetVersionOverride: func() *uint32 { v := uint32(1000); return &v }(), createCnt: 5, deleteCnt: 5, @@ -367,7 +373,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { hs2 := hsets[1] hs3 := hsets[2] - projTarget := TestTcpTarget(t, conn, proj.PublicId, "delete-unassociated") + projTarget := tcp.TestTarget(t, conn, proj.PublicId, "delete-unassociated") _, gotHostSources, _, err := repo.AddTargetHostSources(context.Background(), projTarget.PublicId, 1, []string{hs1.PublicId, hs2.PublicId}) require.NoError(err) assert.Len(gotHostSources, 2) @@ -386,7 +392,7 @@ func TestRepository_DeleteTargetHosts(t *testing.T) { assert.Equal(0, delCount) // Previous transactions should have been rolled back - gotHostSources, err = fetchHostSources(context.Background(), rw, projTarget.PublicId) + _, gotHostSources, _, err = repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) assert.Len(gotHostSources, 2) }) @@ -398,7 +404,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) iamRepo := iam.TestRepo(t, conn, wrapper) @@ -421,7 +427,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { return results } - setupFn := func(target Target) []HostSource { + setupFn := func(target target.Target) []target.HostSource { hs := createHostSetsFn() _, created, _, err := repo.AddTargetHostSources(context.Background(), target.GetPublicId(), 1, hs) require.NoError(t, err) @@ -429,15 +435,15 @@ func TestRepository_SetTargetHostSets(t *testing.T) { return created } type args struct { - target Target + target target.Target targetVersion uint32 hostSourceIds []string addToOrigHostSources bool - opt []Option + opt []target.Option } tests := []struct { name string - setup func(Target) []HostSource + setup func(target.Target) []target.HostSource args args wantAffectedRows int wantErr bool @@ -446,7 +452,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { name: "clear", setup: setupFn, args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "clear"), + target: tcp.TestTarget(t, conn, proj.PublicId, "clear"), targetVersion: 2, // yep, since setupFn will increment it to 2 hostSourceIds: []string{}, }, @@ -457,7 +463,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { name: "no-change", setup: setupFn, args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "no-change"), + target: tcp.TestTarget(t, conn, proj.PublicId, "no-change"), targetVersion: 2, // yep, since setupFn will increment it to 2 hostSourceIds: []string{}, addToOrigHostSources: true, @@ -469,7 +475,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { name: "add-sets", setup: setupFn, args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "add-sets"), + target: tcp.TestTarget(t, conn, proj.PublicId, "add-sets"), targetVersion: 2, // yep, since setupFn will increment it to 2 hostSourceIds: []string{testHostSetIds[0], testHostSetIds[1]}, addToOrigHostSources: true, @@ -481,7 +487,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { name: "add host sets with zero version", setup: setupFn, args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "add host sets with zero version"), + target: tcp.TestTarget(t, conn, proj.PublicId, "add host sets with zero version"), targetVersion: 0, hostSourceIds: []string{testHostSetIds[0], testHostSetIds[1]}, addToOrigHostSources: true, @@ -492,7 +498,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { name: "remove existing and add users and grps", setup: setupFn, args: args{ - target: TestTcpTarget(t, conn, proj.PublicId, "remove existing and add host sets"), + target: tcp.TestTarget(t, conn, proj.PublicId, "remove existing and add host sets"), targetVersion: 2, // yep, since setupFn will increment it to 2 hostSourceIds: []string{testHostSetIds[0], testHostSetIds[1]}, addToOrigHostSources: false, @@ -504,7 +510,7 @@ func TestRepository_SetTargetHostSets(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - var origHostSources []HostSource + var origHostSources []target.HostSource if tt.setup != nil { origHostSources = tt.setup(tt.args.target) } diff --git a/internal/target/repository_tcp_target_test.go b/internal/target/tcp/repository_tcp_target_test.go similarity index 68% rename from internal/target/repository_tcp_target_test.go rename to internal/target/tcp/repository_tcp_target_test.go index 89b34dfce..21b5806ed 100644 --- a/internal/target/repository_tcp_target_test.go +++ b/internal/target/tcp/repository_tcp_target_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -13,18 +13,20 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "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 TestRepository_CreateTcpTarget(t *testing.T) { +func TestRepository_CreateTarget(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) @@ -43,8 +45,8 @@ func TestRepository_CreateTcpTarget(t *testing.T) { } type args struct { - target *TcpTarget - opt []Option + target *tcp.Target + opt []target.Option } tests := []struct { name string @@ -57,11 +59,11 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "valid-org", args: args{ - target: func() *TcpTarget { - target, err := NewTcpTarget(proj.PublicId, - WithName("valid-org"), - WithDescription("valid-org"), - WithDefaultPort(uint32(22))) + target: func() *tcp.Target { + target, err := tcp.New(proj.PublicId, + target.WithName("valid-org"), + target.WithDescription("valid-org"), + target.WithDefaultPort(uint32(22))) require.NoError(t, err) return target }(), @@ -73,15 +75,15 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "valid-org-with-host-sets", args: args{ - target: func() *TcpTarget { - target, err := NewTcpTarget(proj.PublicId, - WithName("valid-org-with-host-sets"), - WithDescription("valid-org"), - WithDefaultPort(uint32(22))) + target: func() *tcp.Target { + target, err := tcp.New(proj.PublicId, + target.WithName("valid-org-with-host-sets"), + target.WithDescription("valid-org"), + target.WithDefaultPort(uint32(22))) require.NoError(t, err) return target }(), - opt: []Option{WithHostSources(sets)}, + opt: []target.Option{target.WithHostSources(sets)}, }, wantErr: false, wantHostSources: sets, @@ -90,15 +92,15 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "valid-org-with-cred-libs", args: args{ - target: func() *TcpTarget { - target, err := NewTcpTarget(proj.PublicId, - WithName("valid-org-with-cred-libs"), - WithDescription("valid-org"), - WithDefaultPort(uint32(22))) + target: func() *tcp.Target { + target, err := tcp.New(proj.PublicId, + target.WithName("valid-org-with-cred-libs"), + target.WithDescription("valid-org"), + target.WithDefaultPort(uint32(22))) require.NoError(t, err) return target }(), - opt: []Option{WithCredentialSources(clIds)}, + opt: []target.Option{target.WithCredentialSources(clIds)}, }, wantErr: false, wantCredLibs: clIds, @@ -107,17 +109,17 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "valid-org-with-cred-libs-and-host-sets", args: args{ - target: func() *TcpTarget { - target, err := NewTcpTarget(proj.PublicId, - WithName("valid-org-with-cred-libs-and-host-sets"), - WithDescription("valid-org"), - WithDefaultPort(uint32(22))) + target: func() *tcp.Target { + target, err := tcp.New(proj.PublicId, + target.WithName("valid-org-with-cred-libs-and-host-sets"), + target.WithDescription("valid-org"), + target.WithDefaultPort(uint32(22))) require.NoError(t, err) return target }(), - opt: []Option{ - WithHostSources(sets), - WithCredentialSources(clIds), + opt: []target.Option{ + target.WithHostSources(sets), + target.WithCredentialSources(clIds), }, }, wantErr: false, @@ -135,8 +137,8 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "nil-target-store", args: args{ - target: func() *TcpTarget { - target := &TcpTarget{} + target: func() *tcp.Target { + target := &tcp.Target{} return target }(), }, @@ -146,13 +148,18 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "public-id-not-empty", args: args{ - target: func() *TcpTarget { - target, err := NewTcpTarget(proj.PublicId, WithName("valid-org"), WithDescription("valid-org"), WithDefaultPort(uint32(22))) + target: func() *tcp.Target { + tar, err := tcp.New( + proj.PublicId, + target.WithName("valid-org"), + target.WithDescription("valid-org"), + target.WithDefaultPort(uint32(22)), + ) require.NoError(t, err) - id, err := newTcpTargetId() + id, err := db.NewPublicId(tcp.TargetPrefix) require.NoError(t, err) - target.PublicId = id - return target + tar.PublicId = id + return tar }(), }, wantErr: true, @@ -161,11 +168,14 @@ func TestRepository_CreateTcpTarget(t *testing.T) { { name: "empty-scope-id", args: args{ - target: func() *TcpTarget { - target := allocTcpTarget() - target.Name = "empty-scope-id" + target: func() *tcp.Target { + tar, err := tcp.New( + proj.PublicId, + target.WithName("empty-scope-id"), + ) require.NoError(t, err) - return &target + tar.ScopeId = "" + return tar }(), }, wantErr: true, @@ -175,15 +185,15 @@ func TestRepository_CreateTcpTarget(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - target, hostSources, credSources, err := repo.CreateTcpTarget(context.Background(), tt.args.target, tt.args.opt...) + tar, hostSources, credSources, err := repo.CreateTarget(context.Background(), tt.args.target, tt.args.opt...) if tt.wantErr { assert.Error(err) - assert.Nil(target) + assert.Nil(tar) assert.True(errors.Match(errors.T(tt.wantIsError), err)) return } require.NoError(err) - assert.NotNil(target.GetPublicId()) + assert.NotNil(tar.GetPublicId()) hsIds := make([]string, 0, len(hostSources)) for _, s := range hostSources { hsIds = append(hsIds, s.Id()) @@ -196,20 +206,20 @@ func TestRepository_CreateTcpTarget(t *testing.T) { } assert.Equal(tt.wantCredLibs, clIds) - foundTarget, foundHostSources, foundCredLibs, err := repo.LookupTarget(context.Background(), target.GetPublicId()) + foundTarget, foundHostSources, foundCredLibs, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) assert.NoError(err) - assert.True(proto.Equal(target.(*TcpTarget), foundTarget.(*TcpTarget))) + assert.True(proto.Equal(tar.(*tcp.Target), foundTarget.(*tcp.Target))) assert.Equal(hostSources, foundHostSources) assert.Equal(credSources, foundCredLibs) - err = db.TestVerifyOplog(t, rw, target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, tar.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) // TODO (jimlambrt 9/2020) - unfortunately, we can currently // test to make sure that the oplog entry for a target host sets // create exist because the db.TestVerifyOplog doesn't really // support that level of testing and the previous call to - // CreateTcpTarget would create an oplog entry for the + // CreateTarget would create an oplog entry for the // create on the target even if no host sets were added. Once // TestVerifyOplog supports the appropriate granularity, we should // add an appropriate assert. @@ -225,9 +235,9 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) - repo, err := NewRepository(rw, rw, testKms) + repo, err := target.NewRepository(rw, rw, testKms) require.NoError(t, err) - id := testId(t) + id := tcp.TestId(t) iamRepo := iam.TestRepo(t, conn, wrapper) _, proj := iam.TestScopes(t, iamRepo) @@ -238,7 +248,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { description string port uint32 fieldMaskPaths []string - opt []Option + opt []target.Option ScopeId string PublicId *string } @@ -246,7 +256,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { name string newScopeId string newName string - newTargetOpts []Option + newTargetOpts []target.Option args args wantRowsUpdate int wantErr bool @@ -288,7 +298,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newScopeId: proj.PublicId, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTcpTarget: failed for 1: db.DoTx: target.(Repository).UpdateTcpTarget: target.(Repository).update: db.DoTx: target.(Repository).update: db.Update: db.lookupAfterWrite: db.LookupById: record not found, search issue: error #1100", + wantErrMsg: "target.(Repository).UpdateTarget: failed for 1: db.DoTx: target.(Repository).UpdateTarget: target.(Repository).update: db.DoTx: target.(Repository).update: db.Update: db.lookupAfterWrite: db.LookupById: record not found, search issue: error #1100", wantIsError: errors.RecordNotFound, }, { @@ -302,7 +312,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newName: "null-name" + id, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "db.DoTx: target.(Repository).UpdateTcpTarget: target.(Repository).update: db.DoTx: target.(Repository).update: db.Update: name must not be empty: not null constraint violated: integrity violation: error #1001", + wantErrMsg: "db.DoTx: target.(Repository).UpdateTarget: target.(Repository).update: db.DoTx: target.(Repository).update: db.Update: name must not be empty: not null constraint violated: integrity violation: error #1001", }, { name: "null-description", @@ -312,7 +322,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { ScopeId: proj.PublicId, }, newScopeId: proj.PublicId, - newTargetOpts: []Option{WithDescription("null-description" + id)}, + newTargetOpts: []target.Option{target.WithDescription("null-description" + id)}, wantErr: false, wantRowsUpdate: 1, }, @@ -326,7 +336,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newScopeId: proj.PublicId, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTcpTarget: empty field mask: parameter violation: error #104", + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", wantIsError: errors.EmptyFieldMask, }, { @@ -339,7 +349,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newScopeId: proj.PublicId, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTcpTarget: empty field mask: parameter violation: error #104", + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", wantIsError: errors.EmptyFieldMask, }, { @@ -352,7 +362,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newScopeId: proj.PublicId, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTcpTarget: invalid field mask: CreateTime: parameter violation: error #103", + wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: CreateTime: parameter violation: error #103", wantIsError: errors.InvalidFieldMask, }, { @@ -365,7 +375,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { newScopeId: proj.PublicId, wantErr: true, wantRowsUpdate: 0, - wantErrMsg: "target.(Repository).UpdateTcpTarget: invalid field mask: Alice: parameter violation: error #103", + wantErrMsg: "target.(Repository).UpdateTarget: invalid field mask: Alice: parameter violation: error #103", wantIsError: errors.InvalidFieldMask, }, { @@ -378,7 +388,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { }, newScopeId: proj.PublicId, wantErr: true, - wantErrMsg: "target.(Repository).UpdateTcpTarget: missing target public id: parameter violation: error #100", + wantErrMsg: "target.(Repository).UpdateTarget: missing target public id: parameter violation: error #100", wantIsError: errors.InvalidParameter, wantRowsUpdate: 0, }, @@ -390,7 +400,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { }, newScopeId: proj.PublicId, wantErr: true, - wantErrMsg: "target.(Repository).UpdateTcpTarget: empty field mask: parameter violation: error #104", + wantErrMsg: "target.(Repository).UpdateTarget: empty field mask: parameter violation: error #104", wantIsError: errors.EmptyFieldMask, }, { @@ -424,7 +434,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { cs := css[i] require, assert := require.New(t), assert.New(t) if tt.wantDup { - _ = TestTcpTarget(t, conn, proj.PublicId, tt.args.name) + _ = tcp.TestTarget(t, conn, proj.PublicId, tt.args.name) } testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) @@ -440,30 +450,31 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { testClIds = append(testClIds, cl.PublicId) } - tt.newTargetOpts = append(tt.newTargetOpts, WithHostSources(testHostSetIds), WithCredentialSources(testClIds)) + tt.newTargetOpts = append(tt.newTargetOpts, target.WithHostSources(testHostSetIds), target.WithCredentialSources(testClIds)) name := tt.newName if name == "" { - name = testId(t) + name = tcp.TestId(t) } - target := TestTcpTarget(t, conn, tt.newScopeId, name, tt.newTargetOpts...) - updateTarget := allocTcpTarget() - updateTarget.PublicId = target.PublicId + tar := tcp.TestTarget(t, conn, tt.newScopeId, name, tt.newTargetOpts...) + updateTarget := tcp.NewTestTarget( + tt.args.ScopeId, + target.WithName(tt.args.name), + target.WithDescription(tt.args.description), + target.WithDefaultPort(tt.args.port), + ) + updateTarget.PublicId = tar.PublicId if tt.args.PublicId != nil { updateTarget.PublicId = *tt.args.PublicId } - updateTarget.ScopeId = tt.args.ScopeId - updateTarget.Name = tt.args.name - updateTarget.Description = tt.args.description - updateTarget.DefaultPort = tt.args.port - targetAfterUpdate, hostSources, credSources, updatedRows, err := repo.UpdateTcpTarget(context.Background(), &updateTarget, target.Version, tt.args.fieldMaskPaths, tt.args.opt...) + targetAfterUpdate, hostSources, credSources, updatedRows, err := repo.UpdateTarget(context.Background(), updateTarget, tar.Version, tt.args.fieldMaskPaths, tt.args.opt...) if tt.wantErr { assert.Error(err) assert.True(errors.Match(errors.T(tt.wantIsError), err)) assert.Nil(targetAfterUpdate) assert.Equal(0, updatedRows) assert.Contains(err.Error(), tt.wantErrMsg) - err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, tar.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.Error(err) assert.True(errors.IsNotFoundError(err)) return @@ -485,13 +496,13 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { switch tt.name { case "valid-no-op": - assert.Equal(target.UpdateTime, targetAfterUpdate.(*TcpTarget).UpdateTime) + assert.Equal(tar.UpdateTime, targetAfterUpdate.(*tcp.Target).UpdateTime) default: - assert.NotEqual(target.UpdateTime, targetAfterUpdate.(*TcpTarget).UpdateTime) + assert.NotEqual(tar.UpdateTime, targetAfterUpdate.(*tcp.Target).UpdateTime) } - foundTarget, _, _, err := repo.LookupTarget(context.Background(), target.PublicId) + foundTarget, _, _, err := repo.LookupTarget(context.Background(), tar.PublicId) assert.NoError(err) - assert.True(proto.Equal(targetAfterUpdate.((*TcpTarget)), foundTarget.((*TcpTarget)))) + assert.True(proto.Equal(targetAfterUpdate.((*tcp.Target)), foundTarget.((*tcp.Target)))) underlyingDB, err := conn.SqlDB(ctx) require.NoError(err) dbassert := dbassert.New(t, underlyingDB) @@ -499,7 +510,7 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { assert.Equal(foundTarget.GetDescription(), "") dbassert.IsNull(foundTarget, "description") } - err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, tar.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) }) } diff --git a/internal/target/tcp/repository_test.go b/internal/target/tcp/repository_test.go new file mode 100644 index 000000000..ffc76fca6 --- /dev/null +++ b/internal/target/tcp/repository_test.go @@ -0,0 +1,347 @@ +package tcp_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_LookupTarget(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + proj.Name = "project-name" + _, _, err := iamRepo.UpdateScope(context.Background(), proj, 1, []string{"name"}) + require.NoError(t, err) + rw := db.New(conn) + repo, err := target.NewRepository(rw, rw, testKms) + require.NoError(t, err) + tgt := tcp.TestTarget(t, conn, proj.PublicId, "target-name") + + tests := []struct { + testName string + id string + name string + scopeId string + scopeName string + wantErr bool + }{ + { + testName: "id", + id: tgt.PublicId, + wantErr: false, + }, + { + testName: "name only", + name: tgt.Name, + wantErr: true, + }, + { + testName: "scope id only", + scopeId: proj.PublicId, + wantErr: true, + }, + { + testName: "scope name only", + scopeName: proj.Name, + wantErr: true, + }, + { + testName: "scope name and id", + scopeId: proj.PublicId, + scopeName: proj.Name, + wantErr: true, + }, + { + testName: "everything", + name: tgt.Name, + scopeId: proj.PublicId, + scopeName: proj.Name, + wantErr: true, + }, + { + testName: "name and scope name", + name: tgt.Name, + scopeName: proj.Name, + wantErr: false, + }, + { + testName: "name and scope id", + name: tgt.Name, + scopeId: proj.PublicId, + wantErr: false, + }, + { + testName: "id and name", + id: tgt.PublicId, + name: tgt.Name, + scopeId: proj.PublicId, + wantErr: true, + }, + { + testName: "id and scope name", + id: tgt.PublicId, + scopeName: proj.Name, + wantErr: true, + }, + { + testName: "id and scope id", + id: tgt.PublicId, + scopeId: proj.PublicId, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + id := tt.id + if tt.name != "" && tt.id == "" { + id = tt.name + } + var opts []target.Option + if tt.name != "" { + opts = append(opts, target.WithName(tt.name)) + } + if tt.scopeId != "" { + opts = append(opts, target.WithScopeId(tt.scopeId)) + } + if tt.scopeName != "" { + opts = append(opts, target.WithScopeName(tt.scopeName)) + } + got, _, _, err := repo.LookupTarget(context.Background(), id, opts...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tgt.PublicId, got.GetPublicId()) + }) + } +} + +func TestRepository_ListTargets(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + const testLimit = 10 + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + rw := db.New(conn) + repo, err := target.NewRepository(rw, rw, testKms, target.WithLimit(testLimit)) + require.NoError(t, err) + + type args struct { + opt []target.Option + } + tests := []struct { + name string + createCnt int + createScopeId string + createScopeId2 string + grantUserId string + args args + wantCnt int + wantErr bool + }{ + { + name: "tcp-target", + createCnt: 5, + createScopeId: proj.PublicId, + args: args{ + opt: []target.Option{target.WithTargetType(target.TcpTargetType), target.WithScopeIds([]string{proj.PublicId})}, + }, + wantCnt: 5, + wantErr: false, + }, + { + name: "no-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []target.Option{target.WithLimit(-1), target.WithScopeIds([]string{proj.PublicId})}, + }, + wantCnt: testLimit + 1, + wantErr: false, + }, + { + name: "default-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []target.Option{target.WithScopeIds([]string{proj.PublicId})}, + }, + wantCnt: testLimit, + wantErr: false, + }, + { + name: "custom-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []target.Option{target.WithLimit(3), target.WithScopeIds([]string{proj.PublicId})}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + createScopeId: proj.PublicId, + args: args{ + opt: []target.Option{target.WithScopeIds([]string{"bad-id"})}, + }, + wantCnt: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(tcp.NewTestTarget("")).Error) + testGroups := []*tcp.Target{} + for i := 0; i < tt.createCnt; i++ { + switch { + case tt.createScopeId2 != "" && i%2 == 0: + testGroups = append(testGroups, tcp.TestTarget(t, conn, tt.createScopeId2, strconv.Itoa(i))) + default: + testGroups = append(testGroups, tcp.TestTarget(t, conn, tt.createScopeId, strconv.Itoa(i))) + } + } + assert.Equal(tt.createCnt, len(testGroups)) + got, err := repo.ListTargets(context.Background(), tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} + +func TestRepository_ListRoles_Multiple_Scopes(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj1 := iam.TestScopes(t, iamRepo) + _, proj2 := iam.TestScopes(t, iamRepo) + rw := db.New(conn) + repo, err := target.NewRepository(rw, rw, testKms) + require.NoError(t, err) + + require.NoError(t, conn.Where("1=1").Delete(tcp.NewTestTarget("")).Error) + + const numPerScope = 10 + var total int + for i := 0; i < numPerScope; i++ { + tcp.TestTarget(t, conn, proj1.GetPublicId(), fmt.Sprintf("proj1-%d", i)) + total++ + tcp.TestTarget(t, conn, proj2.GetPublicId(), fmt.Sprintf("proj2-%d", i)) + total++ + } + + got, err := repo.ListTargets(context.Background(), target.WithScopeIds([]string{"global", proj1.GetPublicId(), proj2.GetPublicId()})) + require.NoError(t, err) + assert.Equal(t, total, len(got)) +} + +func TestRepository_DeleteTarget(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + repo, err := target.NewRepository(rw, rw, testKms) + require.NoError(t, err) + + type args struct { + target target.Target + opt []target.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + args: args{ + target: tcp.TestTarget(t, conn, proj.PublicId, "valid"), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-public-id", + args: args{ + target: func() target.Target { + tar, _ := tcp.New(proj.PublicId) + return tar + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "target.(Repository).DeleteTarget: missing public id: parameter violation: error #100", + }, + { + name: "not-found", + args: args{ + target: func() target.Target { + id, err := db.NewPublicId(tcp.TargetPrefix) + require.NoError(t, err) + tar, _ := tcp.New(proj.PublicId) + tar.PublicId = id + return tar + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "db.LookupById: record not found, search issue: error #1100", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + deletedRows, err := repo.DeleteTarget(context.Background(), tt.args.target.GetPublicId(), tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Equal(0, deletedRows) + assert.Contains(err.Error(), tt.wantErrMsg) + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.IsNotFoundError(err)) + return + } + assert.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundGroup, _, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) + assert.NoError(err) + assert.Nil(foundGroup) + + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) + } +} diff --git a/internal/target/tcp/store/target.pb.go b/internal/target/tcp/store/target.pb.go new file mode 100644 index 000000000..903fe7f15 --- /dev/null +++ b/internal/target/tcp/store/target.pb.go @@ -0,0 +1,306 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/storage/target/tcp/store/v1/target.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) +) + +type Target struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is used to access the tcp.Target via an 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"` + // scope id for the tcp.Target + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the tcp.Target via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the tcp.Target + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @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"` + // update_time from the RDBMS + // @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"` + // version allows optimistic locking of the tcp.Target when modifying the + // tcp.Target + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // default port of the tcp.Target + // @inject_tag: `gorm:"default:null"` + DefaultPort uint32 `protobuf:"varint,80,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty" gorm:"default:null"` + // Maximum total lifetime of a created session, in seconds + // @inject_tag: `gorm:"default:null"` + SessionMaxSeconds uint32 `protobuf:"varint,100,opt,name=session_max_seconds,json=sessionMaxSeconds,proto3" json:"session_max_seconds,omitempty" gorm:"default:null"` + // Maximum number of connections in a session + // @inject_tag: `gorm:"default:null"` + SessionConnectionLimit int32 `protobuf:"varint,110,opt,name=session_connection_limit,json=sessionConnectionLimit,proto3" json:"session_connection_limit,omitempty" gorm:"default:null"` + // A boolean expression that allows filtering the workers that can handle a session + // @inject_tag: `gorm:"default:null"` + WorkerFilter string `protobuf:"bytes,120,opt,name=worker_filter,json=workerFilter,proto3" json:"worker_filter,omitempty" gorm:"default:null"` +} + +func (x *Target) Reset() { + *x = Target{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_tcp_store_v1_target_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Target) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Target) ProtoMessage() {} + +func (x *Target) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_tcp_store_v1_target_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 Target.ProtoReflect.Descriptor instead. +func (*Target) Descriptor() ([]byte, []int) { + return file_controller_storage_target_tcp_store_v1_target_proto_rawDescGZIP(), []int{0} +} + +func (x *Target) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Target) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Target) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Target) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Target) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Target) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Target) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Target) GetDefaultPort() uint32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + +func (x *Target) GetSessionMaxSeconds() uint32 { + if x != nil { + return x.SessionMaxSeconds + } + return 0 +} + +func (x *Target) GetSessionConnectionLimit() int32 { + if x != nil { + return x.SessionConnectionLimit + } + return 0 +} + +func (x *Target) GetWorkerFilter() string { + if x != nil { + return x.WorkerFilter + } + return "" +} + +var File_controller_storage_target_tcp_store_v1_target_proto protoreflect.FileDescriptor + +var file_controller_storage_target_tcp_store_v1_target_proto_rawDesc = []byte{ + 0x0a, 0x33, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x74, 0x63, 0x70, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x26, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x2e, 0x74, 0x63, 0x70, 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, 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, 0x22, 0xc3, 0x05, 0x0a, 0x06, 0x54, + 0x61, 0x72, 0x67, 0x65, 0x74, 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, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1e, 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, 0x28, 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, 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, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x42, + 0x2a, 0xc2, 0xdd, 0x29, 0x26, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x0b, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x5c, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x11, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x13, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x73, 0x52, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x70, 0x0a, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x05, 0x42, 0x36, 0xc2, 0xdd, 0x29, 0x32, 0x0a, 0x16, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x52, 0x16, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x46, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x21, 0xc2, 0xdd, 0x29, 0x1d, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x52, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x42, 0x3f, 0x5a, 0x3d, 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, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x2f, 0x74, 0x63, 0x70, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_target_tcp_store_v1_target_proto_rawDescOnce sync.Once + file_controller_storage_target_tcp_store_v1_target_proto_rawDescData = file_controller_storage_target_tcp_store_v1_target_proto_rawDesc +) + +func file_controller_storage_target_tcp_store_v1_target_proto_rawDescGZIP() []byte { + file_controller_storage_target_tcp_store_v1_target_proto_rawDescOnce.Do(func() { + file_controller_storage_target_tcp_store_v1_target_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_target_tcp_store_v1_target_proto_rawDescData) + }) + return file_controller_storage_target_tcp_store_v1_target_proto_rawDescData +} + +var file_controller_storage_target_tcp_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_target_tcp_store_v1_target_proto_goTypes = []interface{}{ + (*Target)(nil), // 0: controller.storage.target.tcp.store.v1.Target + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_target_tcp_store_v1_target_proto_depIdxs = []int32{ + 1, // 0: controller.storage.target.tcp.store.v1.Target.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.target.tcp.store.v1.Target.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_target_tcp_store_v1_target_proto_init() } +func file_controller_storage_target_tcp_store_v1_target_proto_init() { + if File_controller_storage_target_tcp_store_v1_target_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_target_tcp_store_v1_target_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Target); 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_target_tcp_store_v1_target_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_target_tcp_store_v1_target_proto_goTypes, + DependencyIndexes: file_controller_storage_target_tcp_store_v1_target_proto_depIdxs, + MessageInfos: file_controller_storage_target_tcp_store_v1_target_proto_msgTypes, + }.Build() + File_controller_storage_target_tcp_store_v1_target_proto = out.File + file_controller_storage_target_tcp_store_v1_target_proto_rawDesc = nil + file_controller_storage_target_tcp_store_v1_target_proto_goTypes = nil + file_controller_storage_target_tcp_store_v1_target_proto_depIdxs = nil +} diff --git a/internal/target/tcp/target.go b/internal/target/tcp/target.go new file mode 100644 index 000000000..e98a7f329 --- /dev/null +++ b/internal/target/tcp/target.go @@ -0,0 +1,173 @@ +// Package tcp provides a Target subtype for a TCP Target. +// Importing this package will register it with the target package and +// allow the target.Repository to support tcp.Targets. +package tcp + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp/store" + "github.com/hashicorp/boundary/internal/types/subtypes" + "google.golang.org/protobuf/proto" +) + +const ( + defaultTableName = "target_tcp" + Subtype = subtypes.Subtype("tcp") +) + +// Target is a resources that represets a networked service +// that can be accessed via TCP. It is a subtype of target.Target. +type Target struct { + *store.Target + tableName string `gorm:"-"` +} + +// Ensure Target implements interfaces +var ( + _ target.Target = (*Target)(nil) + _ db.VetForWriter = (*Target)(nil) + _ oplog.ReplayableMessage = (*Target)(nil) +) + +// New creates a new in memory tcp target. WithName, WithDescription and +// WithDefaultPort options are supported +func New(scopeId string, opt ...target.Option) (*Target, error) { + const op = "tcp.NewTarget" + opts := target.GetOpts(opt...) + if scopeId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing scope id") + } + t := &Target{ + Target: &store.Target{ + ScopeId: scopeId, + Name: opts.WithName, + Description: opts.WithDescription, + DefaultPort: opts.WithDefaultPort, + SessionConnectionLimit: opts.WithSessionConnectionLimit, + SessionMaxSeconds: opts.WithSessionMaxSeconds, + WorkerFilter: opts.WithWorkerFilter, + }, + } + return t, nil +} + +// allocTarget will allocate a tcp target +func allocTarget() target.Target { + return &Target{ + Target: &store.Target{}, + } +} + +// Clone creates a clone of the Target +func (t *Target) Clone() target.Target { + cp := proto.Clone(t.Target) + return &Target{ + Target: cp.(*store.Target), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the tcp target +// before it's written. +func (t *Target) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error { + const op = "tcp.(Target).VetForWrite" + if t.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + if opType == db.CreateOp { + if t.ScopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if t.Name == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing name") + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (t *Target) TableName() string { + if t.tableName != "" { + return t.tableName + } + return defaultTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (t *Target) SetTableName(n string) { + t.tableName = n +} + +// Oplog provides the oplog.Metadata for recording operations taken on a Target. +func (t *Target) Oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{t.PublicId}, + "resource-type": []string{"tcp target"}, + "op-type": []string{op.String()}, + "scope-id": []string{t.ScopeId}, + } + return metadata +} + +func (t *Target) GetType() subtypes.Subtype { + return Subtype +} + +func (t *Target) SetPublicId(ctx context.Context, publicId string) error { + const op = "tcp.(Target).SetPublicId" + if !strings.HasPrefix(publicId, TargetPrefix+"_") { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("passed-in public ID %q has wrong prefix, should be %q", publicId, TargetPrefix)) + } + + t.PublicId = publicId + return nil +} + +func (t *Target) SetScopeId(scopeId string) { + t.ScopeId = scopeId +} + +func (t *Target) SetName(name string) { + t.Name = name +} + +func (t *Target) SetDescription(description string) { + t.Description = description +} + +func (t *Target) SetVersion(v uint32) { + t.Version = v +} + +func (t *Target) SetDefaultPort(port uint32) { + t.DefaultPort = port +} + +func (t *Target) SetCreateTime(ts *timestamp.Timestamp) { + t.CreateTime = ts +} + +func (t *Target) SetUpdateTime(ts *timestamp.Timestamp) { + t.UpdateTime = ts +} + +func (t *Target) SetSessionMaxSeconds(s uint32) { + t.SessionMaxSeconds = s +} + +func (t *Target) SetSessionConnectionLimit(limit int32) { + t.SessionConnectionLimit = limit +} + +func (t *Target) SetWorkerFilter(filter string) { + t.WorkerFilter = filter +} diff --git a/internal/target/tcp_target_test.go b/internal/target/tcp/target_test.go similarity index 71% rename from internal/target/tcp_target_test.go rename to internal/target/tcp/target_test.go index faa059033..82ec6b46c 100644 --- a/internal/target/tcp_target_test.go +++ b/internal/target/tcp/target_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -10,24 +10,26 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/oplog" + "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 TestTcpTarget_Create(t *testing.T) { +func TestTarget_Create(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) type args struct { scopeId string - opt []Option + opt []target.Option } tests := []struct { name string args args - want *TcpTarget + want *tcp.Target wantErr bool wantIsErr errors.Code create bool @@ -43,15 +45,16 @@ func TestTcpTarget_Create(t *testing.T) { name: "valid-proj-scope", args: args{ scopeId: prj.PublicId, - opt: []Option{WithName("valid-proj-scope")}, + opt: []target.Option{target.WithName("valid-proj-scope")}, }, - want: func() *TcpTarget { - t := allocTcpTarget() - t.ScopeId = prj.PublicId - t.Name = "valid-proj-scope" - t.SessionMaxSeconds = uint32((8 * time.Hour).Seconds()) - t.SessionConnectionLimit = 1 - return &t + want: func() *tcp.Target { + t, _ := tcp.New( + prj.PublicId, + target.WithName("valid-proj-scope"), + target.WithSessionMaxSeconds(uint32((8 * time.Hour).Seconds())), + target.WithSessionConnectionLimit(1), + ) + return t }(), create: true, }, @@ -59,7 +62,7 @@ func TestTcpTarget_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := NewTcpTarget(tt.args.scopeId, tt.args.opt...) + got, err := tcp.New(tt.args.scopeId, tt.args.opt...) if tt.wantErr { require.Error(err) assert.True(errors.Match(errors.T(tt.wantIsErr), err)) @@ -68,22 +71,22 @@ func TestTcpTarget_Create(t *testing.T) { require.NoError(err) assert.Equal(tt.want, got) if tt.create { - id, err := newTcpTargetId() + id, err := db.NewPublicId(tcp.TargetPrefix) require.NoError(err) got.PublicId = id err = db.New(conn).Create(context.Background(), got) if tt.wantCreateErr { assert.Error(err) return - } else { - assert.NoError(err) } + + assert.NoError(err) } }) } } -func TestTcpTarget_Delete(t *testing.T) { +func TestTarget_Delete(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -92,27 +95,27 @@ func TestTcpTarget_Delete(t *testing.T) { tests := []struct { name string - target *TcpTarget + target *tcp.Target wantRowsDeleted int wantErr bool wantErrMsg string }{ { name: "valid", - target: TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)), + target: tcp.TestTarget(t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId)), wantErr: false, wantRowsDeleted: 1, }, { name: "bad-id", - target: func() *TcpTarget { - target := allocTcpTarget() - id, err := newTcpTargetId() + target: func() *tcp.Target { + tar, _ := tcp.New(proj.PublicId) + + id, err := db.NewPublicId(tcp.TargetPrefix) require.NoError(t, err) - target.PublicId = id - target.ScopeId = proj.PublicId - target.Name = testTargetName(t, proj.PublicId) - return &target + tar.PublicId = id + tar.Name = tcp.TestTargetName(t, proj.PublicId) + return tar }(), wantErr: false, wantRowsDeleted: 0, @@ -121,9 +124,9 @@ func TestTcpTarget_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - deleteTarget := allocTcpTarget() + deleteTarget := tcp.NewTestTarget("") deleteTarget.PublicId = tt.target.PublicId - deletedRows, err := rw.Delete(context.Background(), &deleteTarget) + deletedRows, err := rw.Delete(context.Background(), deleteTarget) if tt.wantErr { require.Error(err) return @@ -134,18 +137,18 @@ func TestTcpTarget_Delete(t *testing.T) { return } assert.Equal(tt.wantRowsDeleted, deletedRows) - foundTarget := allocTcpTarget() + foundTarget := tcp.NewTestTarget("") foundTarget.PublicId = tt.target.PublicId - err = rw.LookupById(context.Background(), &foundTarget) + err = rw.LookupById(context.Background(), foundTarget) require.Error(err) assert.True(errors.IsNotFoundError(err)) }) } } -func TestTcpTarget_Update(t *testing.T) { +func TestTarget_Update(t *testing.T) { t.Parallel() - id := testId(t) + id := tcp.TestId(t) ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -246,37 +249,36 @@ func TestTcpTarget_Update(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) if tt.wantDup { - target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) + target := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId)) target.Name = tt.args.name _, err := rw.Update(context.Background(), target, tt.args.fieldMaskPaths, tt.args.nullPaths) require.NoError(err) } - id := testId(t) - target := TestTcpTarget(t, conn, proj.PublicId, id, WithDescription(id)) + id := tcp.TestId(t) + tar := tcp.TestTarget(t, conn, proj.PublicId, id, target.WithDescription(id)) - updateTarget := allocTcpTarget() - updateTarget.PublicId = target.PublicId - updateTarget.ScopeId = tt.args.ScopeId + updateTarget := tcp.NewTestTarget(tt.args.ScopeId) + updateTarget.PublicId = tar.PublicId updateTarget.Name = tt.args.name updateTarget.Description = tt.args.description - updatedRows, err := rw.Update(context.Background(), &updateTarget, tt.args.fieldMaskPaths, tt.args.nullPaths) + updatedRows, err := rw.Update(context.Background(), updateTarget, tt.args.fieldMaskPaths, tt.args.nullPaths) if tt.wantErr { require.Error(err) assert.Equal(0, updatedRows) assert.Equal(tt.wantErrMsg, err.Error()) - err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, tar.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.Error(err) assert.Contains(err.Error(), "record not found") return } require.NoError(err) assert.Equal(tt.wantRowsUpdate, updatedRows) - assert.NotEqual(target.UpdateTime, updateTarget.UpdateTime) - foundTarget := allocTcpTarget() - foundTarget.PublicId = target.GetPublicId() - err = rw.LookupByPublicId(context.Background(), &foundTarget) + assert.NotEqual(tar.UpdateTime, updateTarget.UpdateTime) + foundTarget := tcp.NewTestTarget(tt.args.ScopeId) + foundTarget.PublicId = tar.GetPublicId() + err = rw.LookupByPublicId(context.Background(), foundTarget) require.NoError(err) assert.True(proto.Equal(updateTarget, foundTarget)) if len(tt.args.nullPaths) != 0 { @@ -291,49 +293,49 @@ func TestTcpTarget_Update(t *testing.T) { } t.Run("update dup names in diff scopes", func(t *testing.T) { assert, require := assert.New(t), require.New(t) - id := testId(t) + id := tcp.TestId(t) _, proj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - _ = TestTcpTarget(t, conn, proj2.PublicId, id, WithDescription(id)) - projTarget := TestTcpTarget(t, conn, proj.PublicId, id) + _ = tcp.TestTarget(t, conn, proj2.PublicId, id, target.WithDescription(id)) + projTarget := tcp.TestTarget(t, conn, proj.PublicId, id) projTarget.Name = id updatedRows, err := rw.Update(context.Background(), projTarget, []string{"Name"}, nil) require.NoError(err) assert.Equal(1, updatedRows) - foundTarget := allocTcpTarget() + foundTarget, _ := tcp.New(proj2.PublicId) foundTarget.PublicId = projTarget.GetPublicId() - err = rw.LookupByPublicId(context.Background(), &foundTarget) + err = rw.LookupByPublicId(context.Background(), foundTarget) require.NoError(err) assert.Equal(id, projTarget.Name) }) } -func TestTcpTarget_Clone(t *testing.T) { +func TestTarget_Clone(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) t.Run("valid", func(t *testing.T) { assert := assert.New(t) _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) - cp := target.Clone() - assert.True(proto.Equal(cp.(*TcpTarget).TcpTarget, target.TcpTarget)) + tar := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId)) + cp := tar.Clone() + assert.True(proto.Equal(cp.(*tcp.Target).Target, tar.Target)) }) t.Run("not-equal", func(t *testing.T) { assert := assert.New(t) _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) _, proj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) - target2 := TestTcpTarget(t, conn, proj2.PublicId, testTargetName(t, proj2.PublicId)) + target1 := tcp.TestTarget(t, conn, proj.PublicId, tcp.TestTargetName(t, proj.PublicId)) + target2 := tcp.TestTarget(t, conn, proj2.PublicId, tcp.TestTargetName(t, proj2.PublicId)) - cp := target.Clone() - assert.True(!proto.Equal(cp.(*TcpTarget).TcpTarget, target2.TcpTarget)) + cp := target1.Clone() + assert.True(!proto.Equal(cp.(*tcp.Target).Target, target2.Target)) }) } -func TestTcpTable_SetTableName(t *testing.T) { +func TestTable_SetTableName(t *testing.T) { t.Parallel() - defaultTableName := DefaultTcpTableName + defaultTableName := tcp.DefaultTableName tests := []struct { name string setNameTo string @@ -353,30 +355,29 @@ func TestTcpTable_SetTableName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - def := allocTcpTarget() + def, _ := tcp.New("testScope") require.Equal(defaultTableName, def.TableName()) - s := allocTcpTarget() + s, _ := tcp.New("testScope") s.SetTableName(tt.setNameTo) assert.Equal(tt.want, s.TableName()) }) } } -func TestTcpTarget_oplog(t *testing.T) { - id := testId(t) +func TestTarget_oplog(t *testing.T) { + id := tcp.TestId(t) tests := []struct { name string - target *TcpTarget + target *tcp.Target op oplog.OpType want oplog.Metadata }{ { name: "simple", - target: func() *TcpTarget { - t := allocTcpTarget() + target: func() *tcp.Target { + t, _ := tcp.New(id) t.PublicId = id - t.ScopeId = id - return &t + return t }(), op: oplog.OpType_OP_TYPE_CREATE, want: oplog.Metadata{ @@ -390,7 +391,7 @@ func TestTcpTarget_oplog(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) - got := tt.target.oplog(tt.op) + got := tt.target.Oplog(tt.op) assert.Equal(got, tt.want) }) } diff --git a/internal/target/tcp/testing.go b/internal/target/tcp/testing.go new file mode 100644 index 000000000..f78f61e9b --- /dev/null +++ b/internal/target/tcp/testing.go @@ -0,0 +1,62 @@ +package tcp + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/go-uuid" + "github.com/stretchr/testify/require" +) + +// TestTarget is used to create a Target that can be used by tests in other packages. +func TestTarget(t *testing.T, conn *db.DB, scopeId, name string, opt ...target.Option) *Target { + t.Helper() + opt = append(opt, target.WithName(name)) + opts := target.GetOpts(opt...) + require := require.New(t) + rw := db.New(conn) + tar, err := New(scopeId, opt...) + require.NoError(err) + id, err := db.NewPublicId(TargetPrefix) + require.NoError(err) + tar.PublicId = id + err = rw.Create(context.Background(), tar) + require.NoError(err) + + if len(opts.WithHostSources) > 0 { + newHostSets := make([]interface{}, 0, len(opts.WithHostSources)) + for _, s := range opts.WithHostSources { + hostSet, err := target.NewTargetHostSet(tar.PublicId, s) + require.NoError(err) + newHostSets = append(newHostSets, hostSet) + } + err := rw.CreateItems(context.Background(), newHostSets) + require.NoError(err) + } + if len(opts.WithCredentialSources) > 0 { + newCredLibs := make([]interface{}, 0, len(opts.WithCredentialSources)) + for _, cl := range opts.WithCredentialSources { + newCl, err := target.NewCredentialLibrary(tar.PublicId, cl) + require.NoError(err) + newCredLibs = append(newCredLibs, newCl) + } + err := rw.CreateItems(context.Background(), newCredLibs) + require.NoError(err) + } + return tar +} + +func testTargetName(t *testing.T, scopeId string) string { + t.Helper() + return fmt.Sprintf("%s-%s", scopeId, testId(t)) +} + +func testId(t *testing.T) string { + t.Helper() + id, err := uuid.GenerateUUID() + require.NoError(t, err) + return id +} diff --git a/internal/target/testing_test.go b/internal/target/tcp/testing_test.go similarity index 58% rename from internal/target/testing_test.go rename to internal/target/tcp/testing_test.go index 1d8c53863..7faeb2c14 100644 --- a/internal/target/testing_test.go +++ b/internal/target/tcp/testing_test.go @@ -1,4 +1,4 @@ -package target +package tcp_test import ( "context" @@ -8,6 +8,9 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,22 +18,27 @@ import ( func Test_TestTcpTarget(t *testing.T) { require := require.New(t) conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) wrapper := db.TestWrapper(t) - _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + repo, err := target.NewRepository(rw, rw, testKms) + require.NoError(err) + cats := static.TestCatalogs(t, conn, proj.PublicId, 1) hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 2) var sets []string for _, s := range hsets { sets = append(sets, s.PublicId) } - name := testTargetName(t, proj.PublicId) - target := TestTcpTarget(t, conn, proj.PublicId, name, WithHostSources(sets)) + name := tcp.TestTargetName(t, proj.PublicId) + tar := tcp.TestTarget(t, conn, proj.PublicId, name, target.WithHostSources(sets)) require.NotNil(t) - require.NotEmpty(target.PublicId) - require.Equal(name, target.Name) + require.NotEmpty(tar.PublicId) + require.Equal(name, tar.Name) - rw := db.New(conn) - foundSources, err := fetchHostSources(context.Background(), rw, target.PublicId) + _, foundSources, _, err := repo.LookupTarget(context.Background(), tar.PublicId) require.NoError(err) foundIds := make([]string, 0, len(foundSources)) for _, s := range foundSources { @@ -42,25 +50,29 @@ func Test_TestTcpTarget(t *testing.T) { func Test_TestCredentialLibrary(t *testing.T) { assert, require := assert.New(t), require.New(t) conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) wrapper := db.TestWrapper(t) - _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + repo, err := target.NewRepository(rw, rw, testKms) + require.NoError(err) - target := TestTcpTarget(t, conn, proj.PublicId, t.Name()) + tar := tcp.TestTarget(t, conn, proj.PublicId, t.Name()) store := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] vlibs := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 2) var libIds []string - var libs []*CredentialLibrary + var libs []*target.CredentialLibrary for _, v := range vlibs { libIds = append(libIds, v.GetPublicId()) - lib := TestCredentialLibrary(t, conn, target.GetPublicId(), v.GetPublicId()) + lib := target.TestCredentialLibrary(t, conn, tar.GetPublicId(), v.GetPublicId()) require.NotNil(lib) libs = append(libs, lib) } assert.Len(libs, 2) - rw := db.New(conn) - foundSources, err := fetchCredentialSources(context.Background(), rw, target.PublicId) + _, _, foundSources, err := repo.LookupTarget(context.Background(), tar.PublicId) require.NoError(err) foundIds := make([]string, 0, len(foundSources)) for _, s := range foundSources { diff --git a/internal/target/tcp_target.go b/internal/target/tcp_target.go deleted file mode 100644 index b43d94aa3..000000000 --- a/internal/target/tcp_target.go +++ /dev/null @@ -1,110 +0,0 @@ -package target - -import ( - "context" - - "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/oplog" - "github.com/hashicorp/boundary/internal/target/store" - "google.golang.org/protobuf/proto" -) - -const ( - DefaultTcpTableName = "target_tcp" -) - -type TcpTarget struct { - *store.TcpTarget - tableName string `gorm:"-"` -} - -var ( - _ Target = (*TcpTarget)(nil) - _ db.VetForWriter = (*TcpTarget)(nil) - _ oplog.ReplayableMessage = (*TcpTarget)(nil) -) - -// NewTcpTarget creates a new in memory tcp target. WithName, WithDescription and -// WithDefaultPort options are supported -func NewTcpTarget(scopeId string, opt ...Option) (*TcpTarget, error) { - const op = "target.NewTcpTarget" - opts := getOpts(opt...) - if scopeId == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing scope id") - } - t := &TcpTarget{ - TcpTarget: &store.TcpTarget{ - ScopeId: scopeId, - Name: opts.withName, - Description: opts.withDescription, - DefaultPort: opts.withDefaultPort, - SessionConnectionLimit: opts.withSessionConnectionLimit, - SessionMaxSeconds: opts.withSessionMaxSeconds, - WorkerFilter: opts.withWorkerFilter, - }, - } - return t, nil -} - -// allocTcpTarget will allocate a tcp target -func allocTcpTarget() TcpTarget { - return TcpTarget{ - TcpTarget: &store.TcpTarget{}, - } -} - -// Clone creates a clone of the TcpTarget -func (t *TcpTarget) Clone() interface{} { - cp := proto.Clone(t.TcpTarget) - return &TcpTarget{ - TcpTarget: cp.(*store.TcpTarget), - } -} - -// VetForWrite implements db.VetForWrite() interface and validates the tcp target -// before it's written. -func (t *TcpTarget) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error { - const op = "target.(TcpTarget).VetForWrite" - if t.PublicId == "" { - return errors.New(ctx, errors.InvalidParameter, op, "missing public id") - } - if opType == db.CreateOp { - if t.ScopeId == "" { - return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") - } - if t.Name == "" { - return errors.New(ctx, errors.InvalidParameter, op, "missing name") - } - } - return nil -} - -// TableName returns the tablename to override the default gorm table name -func (t *TcpTarget) TableName() string { - if t.tableName != "" { - return t.tableName - } - return DefaultTcpTableName -} - -// SetTableName sets the tablename and satisfies the ReplayableMessage -// interface. If the caller attempts to set the name to "" the name will be -// reset to the default name. -func (t *TcpTarget) SetTableName(n string) { - t.tableName = n -} - -func (t *TcpTarget) oplog(op oplog.OpType) oplog.Metadata { - metadata := oplog.Metadata{ - "resource-public-id": []string{t.PublicId}, - "resource-type": []string{"tcp target"}, - "op-type": []string{op.String()}, - "scope-id": []string{t.ScopeId}, - } - return metadata -} - -func (t TcpTarget) GetType() string { - return "tcp" -} diff --git a/internal/target/testing.go b/internal/target/testing.go index a1761a659..b436a7963 100644 --- a/internal/target/testing.go +++ b/internal/target/testing.go @@ -2,63 +2,12 @@ package target import ( "context" - "fmt" "testing" "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/require" ) -func TestTcpTarget(t *testing.T, conn *db.DB, scopeId, name string, opt ...Option) *TcpTarget { - t.Helper() - opt = append(opt, WithName(name)) - opts := getOpts(opt...) - require := require.New(t) - rw := db.New(conn) - target, err := NewTcpTarget(scopeId, opt...) - require.NoError(err) - id, err := newTcpTargetId() - require.NoError(err) - target.PublicId = id - err = rw.Create(context.Background(), target) - require.NoError(err) - - if len(opts.withHostSources) > 0 { - newHostSets := make([]interface{}, 0, len(opts.withHostSources)) - for _, s := range opts.withHostSources { - hostSet, err := NewTargetHostSet(target.PublicId, s) - require.NoError(err) - newHostSets = append(newHostSets, hostSet) - } - err := rw.CreateItems(context.Background(), newHostSets) - require.NoError(err) - } - if len(opts.withCredentialSources) > 0 { - newCredLibs := make([]interface{}, 0, len(opts.withCredentialSources)) - for _, cl := range opts.withCredentialSources { - newCl, err := NewCredentialLibrary(target.PublicId, cl) - require.NoError(err) - newCredLibs = append(newCredLibs, newCl) - } - err := rw.CreateItems(context.Background(), newCredLibs) - require.NoError(err) - } - return target -} - -func testTargetName(t *testing.T, scopeId string) string { - t.Helper() - return fmt.Sprintf("%s-%s", scopeId, testId(t)) -} - -func testId(t *testing.T) string { - t.Helper() - id, err := uuid.GenerateUUID() - require.NoError(t, err) - return id -} - // TestCredentialLibrary creates a CredentialLibrary for targetId and // libraryId. func TestCredentialLibrary(t *testing.T, conn *db.DB, targetId, libraryId string) *CredentialLibrary { diff --git a/internal/tests/api/targets/target_test.go b/internal/tests/api/targets/target_test.go index d625a2c6d..a5d901228 100644 --- a/internal/tests/api/targets/target_test.go +++ b/internal/tests/api/targets/target_test.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers/controller" - "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -250,7 +250,7 @@ func TestSet_Errors(t *testing.T) { assert.NotNil(apiErr) assert.Nil(tar) - _, err = tarClient.Read(tc.Context(), target.TcpTargetPrefix+"_doesntexis") + _, err = tarClient.Read(tc.Context(), tcp.TargetPrefix+"_doesntexis") require.Error(err) apiErr = api.AsServerError(err) assert.NotNil(apiErr) From 02b2291375909aaff4df5792f7e87427d56c7a2a Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Thu, 7 Oct 2021 09:25:17 -0400 Subject: [PATCH 08/17] feat(subtypes): Make Registry thread-safe --- internal/types/subtypes/registry.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/types/subtypes/registry.go b/internal/types/subtypes/registry.go index 27423ad7a..10462cbd5 100644 --- a/internal/types/subtypes/registry.go +++ b/internal/types/subtypes/registry.go @@ -4,6 +4,7 @@ package subtypes import ( "fmt" "strings" + "sync" "github.com/hashicorp/boundary/internal/errors" ) @@ -22,13 +23,14 @@ func (t Subtype) String() string { // Registry stores a collection of boundary resource subtypes along with their // prefixes and allows for translating prefixes back to registered subtypes. -// Registry is not thread safe. type Registry struct { subtypesPrefixes map[string]Subtype knownSubtypes map[Subtype]interface{} + + sync.RWMutex } -// New Registry creates a new boundary resource subtype registry. +// NewRegistry creates a new boundary resource subtype registry. func NewRegistry() *Registry { return &Registry{ subtypesPrefixes: make(map[string]Subtype), @@ -39,6 +41,9 @@ func NewRegistry() *Registry { // SubtypeFromType returns the Subtype from the provided string or if // no Subtype was registered with that string Unknown is returned. func (r *Registry) SubtypeFromType(t string) Subtype { + r.RLock() + defer r.RUnlock() + st := Subtype(t) if _, ok := r.knownSubtypes[st]; !ok { return UnknownSubtype @@ -49,6 +54,9 @@ func (r *Registry) SubtypeFromType(t string) Subtype { // SubtypeFromId returns the Subtype from the provided id if the id's prefix // was registered with a Subtype. Otherwise Unknown is returned. func (r *Registry) SubtypeFromId(id string) Subtype { + r.RLock() + defer r.RUnlock() + i := strings.Index(id, "_") if i == -1 { return UnknownSubtype @@ -66,6 +74,9 @@ func (r *Registry) SubtypeFromId(id string) Subtype { // an error if the subtype has already been registered or if any of the // prefixes are associated with another subtype. func (r *Registry) Register(subtype Subtype, prefixes ...string) error { + r.Lock() + defer r.Unlock() + const op = "subtypes.(Registry).Register" if _, present := r.knownSubtypes[subtype]; present { return errors.NewDeprecated(errors.SubtypeAlreadyRegistered, op, fmt.Sprintf("subtype %q already registered", subtype)) From 769416c2ffbd4e630622c287cacfc04a188c27a0 Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 19 Oct 2021 17:21:41 -0400 Subject: [PATCH 09/17] refactor (controller): Use a grpc server for the grpc-gateway (#1576) * refactor (controller): Use a grpc server for the grpc-gateway This starts a grpc server for the grpc-gateway. The new gateway server uses an in-memory listener which also supports "dialing", so the http proxy can connect to the in-memory listener. This change allows us to use all grpc server features and was in part motivated by a need for an interceptor of inbound requests. With that said, having a "proper" grpc server for the gateway will provide a lot more flexibility and capabilities moving forward. The request context will still be verified via middleware in the http proxy (see wrapHandlerWithCommonFuncs). This verified context will be serialized into a protobuf and sent along to the grpc-gateway via metadata where an interceptor will add the verified request info to the request's context (see requestCtxInterceptor). The interceptor will verify the serialized request info via a shared ticket with it's companion in-memory http proxy. FYI: the ticket is simply random base62 string (see db.NewPrivateId). This change also necessitated a minor change to services. We need to return an empty response instead of nil, nil when there are no errors and the response is empty. * refactor (handlers): Rename gRPC Gateway filter Originally, this was called an OutgoingInterceptor, which is an unfortunate name since it's not an interceptor. Now that we're refactoring the gateway stuff and have proper interceptors, renaming this should remove ambiguity * refactor: Rework how 204 with empty msg is handled Recent changes to grpc gateway has necessitated a change to how 204s are handled by the gateway * tests (bats): Check status codes for delete operations --- go.mod | 2 + go.sum | 6 +- internal/auth/additional_verification_test.go | 5 +- internal/errors/code.go | 8 +- internal/errors/error.go | 44 ++ internal/errors/info.go | 4 + internal/gen/controller/api/empty_msg.pb.go | 137 ++++++ internal/gen/controller/auth/auth.pb.go | 263 ++++++++++ internal/gen/errors/errors.pb.go | 322 +++++++++++++ .../gen/testing/interceptor/greeter.pb.go | 222 +++++++++ .../testing/interceptor/greeter_grpc.pb.go | 103 ++++ .../local/controller/api/v1/empty_msg.proto | 8 + .../proto/local/controller/auth/v1/auth.proto | 42 ++ internal/proto/local/errors/v1/errors.proto | 41 ++ .../testing/interceptor/v1/greeter.proto | 22 + internal/servers/controller/auth/auth.go | 92 ++-- internal/servers/controller/auth/auth_test.go | 7 +- internal/servers/controller/auth/testing.go | 17 +- .../common/scopeids/scope_ids_test.go | 5 +- internal/servers/controller/controller.go | 11 +- internal/servers/controller/gateway.go | 67 +++ internal/servers/controller/handler.go | 301 +++++++----- .../authmethods/authmethod_service.go | 2 +- .../authmethods/authmethod_service_test.go | 2 + .../handlers/authmethods/oidc_test.go | 6 +- .../authtokens/authtoken_service_test.go | 19 +- .../credentiallibrary_service_test.go | 3 +- .../credentialstore_service_test.go | 3 +- .../servers/controller/handlers/errors.go | 194 ++++++-- .../controller/handlers/errors_test.go | 104 ++-- .../managed_group_service_test.go | 1 + .../handlers/outgoing_interceptor_test.go | 26 - ...rceptor.go => outgoing_response_filter.go} | 60 ++- .../handlers/outgoing_response_filter_test.go | 72 +++ .../handlers/sessions/session_service_test.go | 7 +- .../handlers/targets/target_service_test.go | 9 +- .../controller/handlers/verifiers_test.go | 4 +- internal/servers/controller/interceptor.go | 207 ++++++++ .../servers/controller/interceptor_test.go | 455 ++++++++++++++++++ internal/servers/controller/listeners.go | 29 +- internal/servers/controller/testing.go | 41 ++ internal/tests/cli/boundary/_helpers.bash | 8 + internal/tests/cli/boundary/groups.bats | 1 + .../tests/cli/boundary/host_catalogs.bats | 1 + internal/tests/cli/boundary/host_sets.bats | 1 + internal/tests/cli/boundary/hosts.bats | 1 + internal/tests/cli/boundary/roles.bats | 1 + internal/tests/cli/boundary/scopes.bats | 2 + internal/tests/cli/boundary/target.bats | 1 + internal/tests/cli/boundary/user.bats | 6 +- 50 files changed, 2625 insertions(+), 370 deletions(-) create mode 100644 internal/gen/controller/api/empty_msg.pb.go create mode 100644 internal/gen/controller/auth/auth.pb.go create mode 100644 internal/gen/errors/errors.pb.go create mode 100644 internal/gen/testing/interceptor/greeter.pb.go create mode 100644 internal/gen/testing/interceptor/greeter_grpc.pb.go create mode 100644 internal/proto/local/controller/api/v1/empty_msg.proto create mode 100644 internal/proto/local/controller/auth/v1/auth.proto create mode 100644 internal/proto/local/errors/v1/errors.proto create mode 100644 internal/proto/local/testing/interceptor/v1/greeter.proto create mode 100644 internal/servers/controller/gateway.go delete mode 100644 internal/servers/controller/handlers/outgoing_interceptor_test.go rename internal/servers/controller/handlers/{outgoing_interceptor.go => outgoing_response_filter.go} (55%) create mode 100644 internal/servers/controller/handlers/outgoing_response_filter_test.go create mode 100644 internal/servers/controller/interceptor.go create mode 100644 internal/servers/controller/interceptor_test.go diff --git a/go.mod b/go.mod index 72fe9c2ed..29869f0d4 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,12 @@ require ( github.com/fatih/color v1.12.0 github.com/fatih/structs v1.1.0 github.com/favadi/protoc-go-inject-tag v1.3.0 + github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/golang-migrate/migrate/v4 v4.14.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.6 + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 github.com/hashicorp/boundary/api v0.0.19 github.com/hashicorp/boundary/sdk v0.0.11 diff --git a/go.sum b/go.sum index 632cd926c..c36a7696e 100644 --- a/go.sum +++ b/go.sum @@ -322,8 +322,9 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -432,6 +433,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -1407,6 +1410,7 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index a73c21f4b..2f0b6e633 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller" @@ -92,10 +93,10 @@ func TestFetchActionSetForId(t *testing.T) { authTokenRepoFn, serversRepoFn, tc.Kms(), - auth.RequestInfo{ + &authpb.RequestInfo{ PublicId: token.Id, EncryptedToken: strings.Split(token.Token, "_")[2], - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), }) typ := resource.Target if tt.typeOverride != resource.Unknown { diff --git a/internal/errors/code.go b/internal/errors/code.go index bc4c254cf..02ea814ec 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -66,9 +66,11 @@ const ( GenKey Code = 304 // GenKey represents an error occurred during the underlying key generation process GenCert Code = 305 // GenCert represents an error occurred during the underlying certificate generation process - // General system errors are reserved Codes 400-599 and align with http client and server error codes - Forbidden Code = 403 // Forbidden represents the operation is forbidden - Internal Code = 500 // InternalError represents the system encountered an unexpected condition. + // General system errors are reserved Codes 400-599 and align with http + // client and server error codes + Unauthorized Code = 401 // Unauthorized represents the operation is unauthorized + Forbidden Code = 403 // Forbidden represents the operation is forbidden + Internal Code = 500 // InternalError represents the system encountered an unexpected condition. // DB errors are reserved Codes from 1000-1999 CheckConstraint Code = 1000 // CheckConstraint represents a check constraint error diff --git a/internal/errors/error.go b/internal/errors/error.go index c729f9e80..a4fdf3f99 100644 --- a/internal/errors/error.go +++ b/internal/errors/error.go @@ -7,6 +7,7 @@ import ( "runtime" "strings" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" "github.com/hashicorp/boundary/internal/observability/event" "github.com/jackc/pgconn" ) @@ -266,6 +267,49 @@ func (e *Err) Error() string { return s.String() } +// ToPbErrors will convert to an Err protobuf +func ToPbErrors(err *Err) *pberrors.Err { + pbErr := &pberrors.Err{ + Code: uint32(err.Code), + Msg: err.Msg, + Op: string(err.Op), + } + + var wrappedErr *Err + isWrappedErr := As(err.Wrapped, &wrappedErr) + switch { + case err.Wrapped == nil: + pbErr.Wrapped = &pberrors.Err_None{ + None: false, + } + case isWrappedErr: + pbErr.Wrapped = &pberrors.Err_Err{ + Err: ToPbErrors(wrappedErr), + } + default: + pbErr.Wrapped = &pberrors.Err_StdError{ + StdError: err.Wrapped.Error(), + } + } + return pbErr +} + +// FromPbErrors will convert from Err protobuf +func FromPbErrors(pbErr *pberrors.Err) *Err { + err := &Err{ + Code: Code(pbErr.Code), + Msg: pbErr.Msg, + Op: Op(pbErr.Op), + } + switch w := pbErr.Wrapped.(type) { + case *pberrors.Err_Err: + err.Wrapped = FromPbErrors(w.Err) + case *pberrors.Err_StdError: + err.Wrapped = errors.New(w.StdError) + } + return err +} + func join(str *strings.Builder, delim string, s string) { if str.Len() == 0 { _, _ = str.WriteString(s) diff --git a/internal/errors/info.go b/internal/errors/info.go index 3d832623a..c9ec7a77c 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -148,6 +148,10 @@ var errorCodeInfo = map[Code]Info{ Message: "forbidden", Kind: Other, }, + Unauthorized: { + Message: "unauthorized", + Kind: Other, + }, CheckConstraint: { Message: "constraint check failed", Kind: Integrity, diff --git a/internal/gen/controller/api/empty_msg.pb.go b/internal/gen/controller/api/empty_msg.pb.go new file mode 100644 index 000000000..340e4d468 --- /dev/null +++ b/internal/gen/controller/api/empty_msg.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/api/v1/empty_msg.proto + +package api + +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) +) + +// EmptyResponse represents an empty response +type EmptyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyResponse) Reset() { + *x = EmptyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_v1_empty_msg_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyResponse) ProtoMessage() {} + +func (x *EmptyResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_v1_empty_msg_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 EmptyResponse.ProtoReflect.Descriptor instead. +func (*EmptyResponse) Descriptor() ([]byte, []int) { + return file_controller_api_v1_empty_msg_proto_rawDescGZIP(), []int{0} +} + +var File_controller_api_v1_empty_msg_proto protoreflect.FileDescriptor + +var file_controller_api_v1_empty_msg_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x5f, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x22, 0x0f, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 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, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_api_v1_empty_msg_proto_rawDescOnce sync.Once + file_controller_api_v1_empty_msg_proto_rawDescData = file_controller_api_v1_empty_msg_proto_rawDesc +) + +func file_controller_api_v1_empty_msg_proto_rawDescGZIP() []byte { + file_controller_api_v1_empty_msg_proto_rawDescOnce.Do(func() { + file_controller_api_v1_empty_msg_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_v1_empty_msg_proto_rawDescData) + }) + return file_controller_api_v1_empty_msg_proto_rawDescData +} + +var file_controller_api_v1_empty_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_api_v1_empty_msg_proto_goTypes = []interface{}{ + (*EmptyResponse)(nil), // 0: controller.api.v1.EmptyResponse +} +var file_controller_api_v1_empty_msg_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_api_v1_empty_msg_proto_init() } +func file_controller_api_v1_empty_msg_proto_init() { + if File_controller_api_v1_empty_msg_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_api_v1_empty_msg_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyResponse); 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_v1_empty_msg_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_api_v1_empty_msg_proto_goTypes, + DependencyIndexes: file_controller_api_v1_empty_msg_proto_depIdxs, + MessageInfos: file_controller_api_v1_empty_msg_proto_msgTypes, + }.Build() + File_controller_api_v1_empty_msg_proto = out.File + file_controller_api_v1_empty_msg_proto_rawDesc = nil + file_controller_api_v1_empty_msg_proto_goTypes = nil + file_controller_api_v1_empty_msg_proto_depIdxs = nil +} diff --git a/internal/gen/controller/auth/auth.pb.go b/internal/gen/controller/auth/auth.pb.go new file mode 100644 index 000000000..103a2360a --- /dev/null +++ b/internal/gen/controller/auth/auth.pb.go @@ -0,0 +1,263 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/auth/v1/auth.proto + +package auth + +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) +) + +// RequestInfo contains request parameters necessary for checking authn/authz +type RequestInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // path of the request + Path string `protobuf:"bytes,10,opt,name=path,proto3" json:"path,omitempty"` + // method (http verb) of the request + Method string `protobuf:"bytes,20,opt,name=method,proto3" json:"method,omitempty"` + // public_id for the request + PublicId string `protobuf:"bytes,30,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty"` + // encrypted_token from the request + EncryptedToken string `protobuf:"bytes,40,opt,name=encrypted_token,json=encryptedToken,proto3" json:"encrypted_token,omitempty"` + // token from the request + Token string `protobuf:"bytes,50,opt,name=token,proto3" json:"token,omitempty"` + // token_format of the request's token + TokenFormat uint32 `protobuf:"varint,60,opt,name=token_format,json=tokenFormat,proto3" json:"token_format,omitempty"` + // scope_id_override for the request (helpful for tests) + ScopeIdOverride string `protobuf:"bytes,70,opt,name=scope_id_override,json=scopeIdOverride,proto3" json:"scope_id_override,omitempty"` + // user_id_override for the request (helpful for tests) + UserIdOverride string `protobuf:"bytes,80,opt,name=user_id_override,json=userIdOverride,proto3" json:"user_id_override,omitempty"` + // disable_authz_failures for the request (helpful for tests) + DisableAuthzFailures bool `protobuf:"varint,90,opt,name=disable_authz_failures,json=disableAuthzFailures,proto3" json:"disable_authz_failures,omitempty"` + // disable_auth_entirely for the request (helpful for tests) + DisableAuthEntirely bool `protobuf:"varint,100,opt,name=disable_auth_entirely,json=disableAuthEntirely,proto3" json:"disable_auth_entirely,omitempty"` + // ticket is a unique id that allows the grpc-gateway to verify that the info + // came from its companion http proxy + Ticket string `protobuf:"bytes,110,opt,name=ticket,proto3" json:"ticket,omitempty"` +} + +func (x *RequestInfo) Reset() { + *x = RequestInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_auth_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RequestInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestInfo) ProtoMessage() {} + +func (x *RequestInfo) ProtoReflect() protoreflect.Message { + mi := &file_controller_auth_v1_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 RequestInfo.ProtoReflect.Descriptor instead. +func (*RequestInfo) Descriptor() ([]byte, []int) { + return file_controller_auth_v1_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *RequestInfo) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *RequestInfo) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *RequestInfo) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *RequestInfo) GetEncryptedToken() string { + if x != nil { + return x.EncryptedToken + } + return "" +} + +func (x *RequestInfo) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *RequestInfo) GetTokenFormat() uint32 { + if x != nil { + return x.TokenFormat + } + return 0 +} + +func (x *RequestInfo) GetScopeIdOverride() string { + if x != nil { + return x.ScopeIdOverride + } + return "" +} + +func (x *RequestInfo) GetUserIdOverride() string { + if x != nil { + return x.UserIdOverride + } + return "" +} + +func (x *RequestInfo) GetDisableAuthzFailures() bool { + if x != nil { + return x.DisableAuthzFailures + } + return false +} + +func (x *RequestInfo) GetDisableAuthEntirely() bool { + if x != nil { + return x.DisableAuthEntirely + } + return false +} + +func (x *RequestInfo) GetTicket() string { + if x != nil { + return x.Ticket + } + return "" +} + +var File_controller_auth_v1_auth_proto protoreflect.FileDescriptor + +var file_controller_auth_v1_auth_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x76, 0x31, 0x22, 0x90, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0b, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x2a, + 0x0a, 0x11, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x49, 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x50, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, 0x5a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x7a, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x72, + 0x65, 0x6c, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x74, 0x69, 0x72, 0x65, 0x6c, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x42, 0x41, 0x5a, 0x3f, 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, 0x75, 0x74, 0x68, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_controller_auth_v1_auth_proto_rawDescOnce sync.Once + file_controller_auth_v1_auth_proto_rawDescData = file_controller_auth_v1_auth_proto_rawDesc +) + +func file_controller_auth_v1_auth_proto_rawDescGZIP() []byte { + file_controller_auth_v1_auth_proto_rawDescOnce.Do(func() { + file_controller_auth_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_auth_v1_auth_proto_rawDescData) + }) + return file_controller_auth_v1_auth_proto_rawDescData +} + +var file_controller_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_auth_v1_auth_proto_goTypes = []interface{}{ + (*RequestInfo)(nil), // 0: controller.auth.v1.RequestInfo +} +var file_controller_auth_v1_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_auth_v1_auth_proto_init() } +func file_controller_auth_v1_auth_proto_init() { + if File_controller_auth_v1_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_auth_v1_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RequestInfo); 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_auth_v1_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_auth_v1_auth_proto_goTypes, + DependencyIndexes: file_controller_auth_v1_auth_proto_depIdxs, + MessageInfos: file_controller_auth_v1_auth_proto_msgTypes, + }.Build() + File_controller_auth_v1_auth_proto = out.File + file_controller_auth_v1_auth_proto_rawDesc = nil + file_controller_auth_v1_auth_proto_goTypes = nil + file_controller_auth_v1_auth_proto_depIdxs = nil +} diff --git a/internal/gen/errors/errors.pb.go b/internal/gen/errors/errors.pb.go new file mode 100644 index 000000000..80d525d88 --- /dev/null +++ b/internal/gen/errors/errors.pb.go @@ -0,0 +1,322 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: errors/v1/errors.proto + +package errors + +import ( + api "github.com/hashicorp/boundary/internal/gen/controller/api" + 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) +) + +// Err is a protobuf representation of a domain error +type Err struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // code is the error's code, which can be used to get the error's + // errorCodeInfo, which contains the error's Kind and Message + Code uint32 `protobuf:"varint,10,opt,name=code,proto3" json:"code,omitempty"` + // msg for the error + Msg string `protobuf:"bytes,20,opt,name=msg,proto3" json:"msg,omitempty"` + // op represents the operation raising/propagating an error and is optional. + Op string `protobuf:"bytes,30,opt,name=op,proto3" json:"op,omitempty"` + // wrapped represents an optionally wrapped error + // + // Types that are assignable to Wrapped: + // *Err_Err + // *Err_StdError + // *Err_None + Wrapped isErr_Wrapped `protobuf_oneof:"wrapped"` +} + +func (x *Err) Reset() { + *x = Err{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_v1_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Err) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Err) ProtoMessage() {} + +func (x *Err) ProtoReflect() protoreflect.Message { + mi := &file_errors_v1_errors_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 Err.ProtoReflect.Descriptor instead. +func (*Err) Descriptor() ([]byte, []int) { + return file_errors_v1_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Err) GetCode() uint32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Err) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +func (x *Err) GetOp() string { + if x != nil { + return x.Op + } + return "" +} + +func (m *Err) GetWrapped() isErr_Wrapped { + if m != nil { + return m.Wrapped + } + return nil +} + +func (x *Err) GetErr() *Err { + if x, ok := x.GetWrapped().(*Err_Err); ok { + return x.Err + } + return nil +} + +func (x *Err) GetStdError() string { + if x, ok := x.GetWrapped().(*Err_StdError); ok { + return x.StdError + } + return "" +} + +func (x *Err) GetNone() bool { + if x, ok := x.GetWrapped().(*Err_None); ok { + return x.None + } + return false +} + +type isErr_Wrapped interface { + isErr_Wrapped() +} + +type Err_Err struct { + // err is a wrapped Err (aka a domain error) + Err *Err `protobuf:"bytes,40,opt,name=err,proto3,oneof"` +} + +type Err_StdError struct { + // std_error is a wrapped std error + StdError string `protobuf:"bytes,50,opt,name=std_error,json=stdError,proto3,oneof"` +} + +type Err_None struct { + // no error or err is wrapped + None bool `protobuf:"varint,60,opt,name=none,proto3,oneof"` +} + +func (*Err_Err) isErr_Wrapped() {} + +func (*Err_StdError) isErr_Wrapped() {} + +func (*Err_None) isErr_Wrapped() {} + +// ApiError is a protobuf representation of a controller ApiError +type ApiError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // api_error is the controller ApiError + ApiError *api.Error `protobuf:"bytes,10,opt,name=api_error,json=apiError,proto3" json:"api_error,omitempty"` + // status is the status code assocatiated with the api error + Status int32 `protobuf:"varint,20,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *ApiError) Reset() { + *x = ApiError{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_v1_errors_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ApiError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiError) ProtoMessage() {} + +func (x *ApiError) ProtoReflect() protoreflect.Message { + mi := &file_errors_v1_errors_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 ApiError.ProtoReflect.Descriptor instead. +func (*ApiError) Descriptor() ([]byte, []int) { + return file_errors_v1_errors_proto_rawDescGZIP(), []int{1} +} + +func (x *ApiError) GetApiError() *api.Error { + if x != nil { + return x.ApiError + } + return nil +} + +func (x *ApiError) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +var File_errors_v1_errors_proto protoreflect.FileDescriptor + +var file_errors_v1_errors_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x2e, 0x76, 0x31, 0x1a, 0x1d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x9f, 0x01, 0x0a, 0x03, 0x45, 0x72, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, + 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x70, + 0x12, 0x22, 0x0a, 0x03, 0x65, 0x72, 0x72, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x72, 0x72, 0x48, 0x00, 0x52, + 0x03, 0x65, 0x72, 0x72, 0x12, 0x1d, 0x0a, 0x09, 0x73, 0x74, 0x64, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x73, 0x74, 0x64, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x08, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x77, 0x72, 0x61, + 0x70, 0x70, 0x65, 0x64, 0x22, 0x59, 0x0a, 0x08, 0x41, 0x70, 0x69, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x35, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x08, 0x61, + 0x70, 0x69, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, + 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_errors_v1_errors_proto_rawDescOnce sync.Once + file_errors_v1_errors_proto_rawDescData = file_errors_v1_errors_proto_rawDesc +) + +func file_errors_v1_errors_proto_rawDescGZIP() []byte { + file_errors_v1_errors_proto_rawDescOnce.Do(func() { + file_errors_v1_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_v1_errors_proto_rawDescData) + }) + return file_errors_v1_errors_proto_rawDescData +} + +var file_errors_v1_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_v1_errors_proto_goTypes = []interface{}{ + (*Err)(nil), // 0: errors.v1.Err + (*ApiError)(nil), // 1: errors.v1.ApiError + (*api.Error)(nil), // 2: controller.api.v1.Error +} +var file_errors_v1_errors_proto_depIdxs = []int32{ + 0, // 0: errors.v1.Err.err:type_name -> errors.v1.Err + 2, // 1: errors.v1.ApiError.api_error:type_name -> controller.api.v1.Error + 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_errors_v1_errors_proto_init() } +func file_errors_v1_errors_proto_init() { + if File_errors_v1_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errors_v1_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Err); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_errors_v1_errors_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApiError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_errors_v1_errors_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*Err_Err)(nil), + (*Err_StdError)(nil), + (*Err_None)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errors_v1_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_errors_v1_errors_proto_goTypes, + DependencyIndexes: file_errors_v1_errors_proto_depIdxs, + MessageInfos: file_errors_v1_errors_proto_msgTypes, + }.Build() + File_errors_v1_errors_proto = out.File + file_errors_v1_errors_proto_rawDesc = nil + file_errors_v1_errors_proto_goTypes = nil + file_errors_v1_errors_proto_depIdxs = nil +} diff --git a/internal/gen/testing/interceptor/greeter.pb.go b/internal/gen/testing/interceptor/greeter.pb.go new file mode 100644 index 000000000..a4b9df67f --- /dev/null +++ b/internal/gen/testing/interceptor/greeter.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: testing/interceptor/v1/greeter.proto + +package interceptor + +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) +) + +// The request message containing the user's name. +type SayHelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *SayHelloRequest) Reset() { + *x = SayHelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloRequest) ProtoMessage() {} + +func (x *SayHelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_testing_interceptor_v1_greeter_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 SayHelloRequest.ProtoReflect.Descriptor instead. +func (*SayHelloRequest) Descriptor() ([]byte, []int) { + return file_testing_interceptor_v1_greeter_proto_rawDescGZIP(), []int{0} +} + +func (x *SayHelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// The response message containing the greetings +type SayHelloResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *SayHelloResponse) Reset() { + *x = SayHelloResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloResponse) ProtoMessage() {} + +func (x *SayHelloResponse) ProtoReflect() protoreflect.Message { + mi := &file_testing_interceptor_v1_greeter_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 SayHelloResponse.ProtoReflect.Descriptor instead. +func (*SayHelloResponse) Descriptor() ([]byte, []int) { + return file_testing_interceptor_v1_greeter_proto_rawDescGZIP(), []int{1} +} + +func (x *SayHelloResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_testing_interceptor_v1_greeter_proto protoreflect.FileDescriptor + +var file_testing_interceptor_v1_greeter_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, + 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x25, + 0x0a, 0x0f, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x10, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x32, 0x71, 0x0a, 0x0e, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x4c, 0x5a, 0x4a, 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, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_testing_interceptor_v1_greeter_proto_rawDescOnce sync.Once + file_testing_interceptor_v1_greeter_proto_rawDescData = file_testing_interceptor_v1_greeter_proto_rawDesc +) + +func file_testing_interceptor_v1_greeter_proto_rawDescGZIP() []byte { + file_testing_interceptor_v1_greeter_proto_rawDescOnce.Do(func() { + file_testing_interceptor_v1_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(file_testing_interceptor_v1_greeter_proto_rawDescData) + }) + return file_testing_interceptor_v1_greeter_proto_rawDescData +} + +var file_testing_interceptor_v1_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_testing_interceptor_v1_greeter_proto_goTypes = []interface{}{ + (*SayHelloRequest)(nil), // 0: testing.interceptor.v1.SayHelloRequest + (*SayHelloResponse)(nil), // 1: testing.interceptor.v1.SayHelloResponse +} +var file_testing_interceptor_v1_greeter_proto_depIdxs = []int32{ + 0, // 0: testing.interceptor.v1.GreeterService.SayHello:input_type -> testing.interceptor.v1.SayHelloRequest + 1, // 1: testing.interceptor.v1.GreeterService.SayHello:output_type -> testing.interceptor.v1.SayHelloResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] 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_testing_interceptor_v1_greeter_proto_init() } +func file_testing_interceptor_v1_greeter_proto_init() { + if File_testing_interceptor_v1_greeter_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_testing_interceptor_v1_greeter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_testing_interceptor_v1_greeter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloResponse); 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_testing_interceptor_v1_greeter_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_testing_interceptor_v1_greeter_proto_goTypes, + DependencyIndexes: file_testing_interceptor_v1_greeter_proto_depIdxs, + MessageInfos: file_testing_interceptor_v1_greeter_proto_msgTypes, + }.Build() + File_testing_interceptor_v1_greeter_proto = out.File + file_testing_interceptor_v1_greeter_proto_rawDesc = nil + file_testing_interceptor_v1_greeter_proto_goTypes = nil + file_testing_interceptor_v1_greeter_proto_depIdxs = nil +} diff --git a/internal/gen/testing/interceptor/greeter_grpc.pb.go b/internal/gen/testing/interceptor/greeter_grpc.pb.go new file mode 100644 index 000000000..7a4e8ce89 --- /dev/null +++ b/internal/gen/testing/interceptor/greeter_grpc.pb.go @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package interceptor + +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 + +// GreeterServiceClient is the client API for GreeterService 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 GreeterServiceClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) +} + +type greeterServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGreeterServiceClient(cc grpc.ClientConnInterface) GreeterServiceClient { + return &greeterServiceClient{cc} +} + +func (c *greeterServiceClient) SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) { + out := new(SayHelloResponse) + err := c.cc.Invoke(ctx, "/testing.interceptor.v1.GreeterService/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GreeterServiceServer is the server API for GreeterService service. +// All implementations must embed UnimplementedGreeterServiceServer +// for forward compatibility +type GreeterServiceServer interface { + // Sends a greeting + SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) + mustEmbedUnimplementedGreeterServiceServer() +} + +// UnimplementedGreeterServiceServer must be embedded to have forward compatible implementations. +type UnimplementedGreeterServiceServer struct { +} + +func (UnimplementedGreeterServiceServer) SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedGreeterServiceServer) mustEmbedUnimplementedGreeterServiceServer() {} + +// UnsafeGreeterServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GreeterServiceServer will +// result in compilation errors. +type UnsafeGreeterServiceServer interface { + mustEmbedUnimplementedGreeterServiceServer() +} + +func RegisterGreeterServiceServer(s grpc.ServiceRegistrar, srv GreeterServiceServer) { + s.RegisterService(&GreeterService_ServiceDesc, srv) +} + +func _GreeterService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SayHelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServiceServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testing.interceptor.v1.GreeterService/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServiceServer).SayHello(ctx, req.(*SayHelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GreeterService_ServiceDesc is the grpc.ServiceDesc for GreeterService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GreeterService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "testing.interceptor.v1.GreeterService", + HandlerType: (*GreeterServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _GreeterService_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "testing/interceptor/v1/greeter.proto", +} diff --git a/internal/proto/local/controller/api/v1/empty_msg.proto b/internal/proto/local/controller/api/v1/empty_msg.proto new file mode 100644 index 000000000..2be840d52 --- /dev/null +++ b/internal/proto/local/controller/api/v1/empty_msg.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package controller.api.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/api;api"; + +// EmptyResponse represents an empty response +message EmptyResponse {} diff --git a/internal/proto/local/controller/auth/v1/auth.proto b/internal/proto/local/controller/auth/v1/auth.proto new file mode 100644 index 000000000..840ffa12b --- /dev/null +++ b/internal/proto/local/controller/auth/v1/auth.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package controller.auth.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/auth;auth"; + +// RequestInfo contains request parameters necessary for checking authn/authz +message RequestInfo { + // path of the request + string path = 10; + + // method (http verb) of the request + string method = 20; + + // public_id for the request + string public_id = 30; + + // encrypted_token from the request + string encrypted_token = 40; + + // token from the request + string token = 50; + + // token_format of the request's token + uint32 token_format = 60; + + // scope_id_override for the request (helpful for tests) + string scope_id_override = 70; + + // user_id_override for the request (helpful for tests) + string user_id_override = 80; + + // disable_authz_failures for the request (helpful for tests) + bool disable_authz_failures = 90; + + // disable_auth_entirely for the request (helpful for tests) + bool disable_auth_entirely = 100; + + // ticket is a unique id that allows the grpc-gateway to verify that the info + // came from its companion http proxy + string ticket = 110; +} diff --git a/internal/proto/local/errors/v1/errors.proto b/internal/proto/local/errors/v1/errors.proto new file mode 100644 index 000000000..d8acb33fc --- /dev/null +++ b/internal/proto/local/errors/v1/errors.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package errors.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/errors;errors"; + +import "controller/api/v1/error.proto"; + +// Err is a protobuf representation of a domain error +message Err { + // code is the error's code, which can be used to get the error's + // errorCodeInfo, which contains the error's Kind and Message + uint32 code = 10; + + // msg for the error + string msg = 20; + + // op represents the operation raising/propagating an error and is optional. + string op = 30; + + // wrapped represents an optionally wrapped error + oneof wrapped { + // err is a wrapped Err (aka a domain error) + Err err = 40; + + // std_error is a wrapped std error + string std_error = 50; + + // no error or err is wrapped + bool none = 60; + } +} + +// ApiError is a protobuf representation of a controller ApiError +message ApiError { + // api_error is the controller ApiError + controller.api.v1.Error api_error = 10; + + // status is the status code assocatiated with the api error + int32 status = 20; +} \ No newline at end of file diff --git a/internal/proto/local/testing/interceptor/v1/greeter.proto b/internal/proto/local/testing/interceptor/v1/greeter.proto new file mode 100644 index 000000000..f1fc1b4e5 --- /dev/null +++ b/internal/proto/local/testing/interceptor/v1/greeter.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package testing.interceptor.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/testing/interceptor;interceptor"; + +// The greeting service definition. This is just a dead simple service that can +// be used to test interceptors. (See controller.startTestGreeterService) +service GreeterService { + // Sends a greeting + rpc SayHello(SayHelloRequest) returns (SayHelloResponse) {} +} + +// The request message containing the user's name. +message SayHelloRequest { + string name = 1; +} + +// The response message containing the greetings +message SayHelloResponse { + string message = 1; +} \ No newline at end of file diff --git a/internal/servers/controller/auth/auth.go b/internal/servers/controller/auth/auth.go index 6fd9f6946..b72d4b7c5 100644 --- a/internal/servers/controller/auth/auth.go +++ b/internal/servers/controller/auth/auth.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/boundary/api/recovery" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/errors" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/gen/controller/tokens" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/perms" @@ -27,7 +29,7 @@ import ( "google.golang.org/protobuf/proto" ) -type TokenFormat int +type TokenFormat uint32 const ( // We weren't given one or couldn't parse it @@ -51,22 +53,6 @@ type key int var verifierKey key -// RequestInfo contains request parameters necessary for checking authn/authz -type RequestInfo struct { - Path string - Method string - PublicId string - EncryptedToken string - Token string - TokenFormat TokenFormat - - // The following are useful for tests - scopeIdOverride string - userIdOverride string - DisableAuthzFailures bool - DisableAuthEntirely bool -} - type VerifyResults struct { UserId string AuthTokenId string @@ -108,7 +94,7 @@ type verifier struct { authTokenRepoFn common.AuthTokenRepoFactory serversRepoFn common.ServersRepoFactory kms *kms.Kms - requestInfo RequestInfo + requestInfo *authpb.RequestInfo res *perms.Resource act action.Type ctx context.Context @@ -124,7 +110,7 @@ func NewVerifierContext(ctx context.Context, authTokenRepoFn common.AuthTokenRepoFactory, serversRepoFn common.ServersRepoFactory, kms *kms.Kms, - requestInfo RequestInfo) context.Context { + requestInfo *authpb.RequestInfo) context.Context { return context.WithValue(ctx, verifierKey, &verifier{ iamRepoFn: iamRepoFn, authTokenRepoFn: authTokenRepoFn, @@ -174,7 +160,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { // without fuss if v.requestInfo.DisableAuthEntirely { const op = "auth.(disabled).lookupScope" - ret.Scope.Id = v.requestInfo.scopeIdOverride + ret.Scope.Id = v.requestInfo.ScopeIdOverride if ret.Scope.Id == "" { ret.Scope.Id = opts.withScopeId } @@ -214,7 +200,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { ParentScopeId: scp.GetParentId(), } } - ret.UserId = v.requestInfo.userIdOverride + ret.UserId = v.requestInfo.UserIdOverride if reqInfo != nil { reqInfo.UserId = ret.UserId } @@ -291,40 +277,40 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { func (v *verifier) decryptToken(ctx context.Context) { const op = "auth.(verifier).decryptToken" switch v.requestInfo.TokenFormat { - case AuthTokenTypeUnknown: + case uint32(AuthTokenTypeUnknown): // Nothing to decrypt return - case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: + case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie): if v.kms == nil { event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } tokenRepo, err := v.authTokenRepoFn() if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to get authtoken repo")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to look up auth token by public ID")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } if at == nil { event.WriteError(ctx, op, stderrors.New("nil result from looking up auth token by public ID")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("unable to get wrapper for tokens; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } @@ -333,62 +319,62 @@ func (v *verifier) decryptToken(ctx context.Context) { case globals.ServiceTokenV1: default: event.WriteError(ctx, op, stderrors.New("unknown token encryption version; continuing as anonymous user"), event.WithInfo("version", version)) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } marshaledToken, err := base58.FastBase58Decoding(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):]) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling base58 token; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } blobInfo := new(wrapping.EncryptedBlobInfo) if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error decoding encrypted token; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, []byte(v.requestInfo.PublicId)) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error decrypting encrypted token; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } var s1Info tokens.S1TokenInfo if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling token info; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } - if v.requestInfo.TokenFormat == AuthTokenTypeUnknown || s1Info.Token == "" || v.requestInfo.PublicId == "" { + if v.requestInfo.TokenFormat == uint32(AuthTokenTypeUnknown) || s1Info.Token == "" || v.requestInfo.PublicId == "" { event.WriteError(ctx, op, stderrors.New("after parsing, could not find valid token; continuing as anonymous user")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } v.requestInfo.Token = s1Info.Token return - case AuthTokenTypeRecoveryKms: + case uint32(AuthTokenTypeRecoveryKms): if v.kms == nil { event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } wrapper := v.kms.GetExternalWrappers().Recovery() if wrapper == nil { event.WriteError(ctx, op, stderrors.New("no recovery KMS is available")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error parsing and validating recovery token")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } // If we add the validity period to the creation time (which we've @@ -396,18 +382,18 @@ func (v *verifier) decryptToken(ctx context.Context) { // it's before now, it's expired and might be a replay. if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) { event.WriteError(ctx, op, stderrors.New("recovery token has expired (possible replay attack)")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } repo, err := v.serversRepoFn() if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error fetching servers repo")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error adding nonce to database (possible replay attack)")) - v.requestInfo.TokenFormat = AuthTokenTypeUnknown + v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown) return } event.WriteError(ctx, op, stderrors.New("recovery KMS was used to authorize a call"), event.WithInfo("url", v.requestInfo.Path, "method", v.requestInfo.Method)) @@ -426,15 +412,15 @@ func (v verifier) performAuthCheck(ctx context.Context) (aclResults perms.ACLRes // Validate the token and fetch the corresponding user ID switch v.requestInfo.TokenFormat { - case AuthTokenTypeUnknown: + case uint32(AuthTokenTypeUnknown): // Nothing; remain as the anonymous user - case AuthTokenTypeRecoveryKms: + case uint32(AuthTokenTypeRecoveryKms): // We validated the encrypted token in decryptToken and handled the // nonces there, so just set the user userId = "u_recovery" - case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: + case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie): if v.requestInfo.Token == "" { // This will end up staying as the anonymous user break @@ -500,7 +486,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (aclResults perms.ACLRes } // At this point we don't need to look up grants since it's automatically allowed - if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms { + if v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms) { aclResults.AuthenticationFinished = true aclResults.Authorized = true retErr = nil @@ -561,7 +547,7 @@ func (r *VerifyResults) FetchActionSetForType(ctx context.Context, typ resource. func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { switch { case r.v.requestInfo.DisableAuthEntirely, - r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: + r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms): // TODO: See if we can be better about what we return with the anonymous // user and recovery KMS, perhaps given exclusionary options for each return availableActions @@ -598,7 +584,7 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) perms.OutputFieldsMap { switch { - case r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: + case r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms): return perms.OutputFieldsMap{"*": true} case r.v.requestInfo.DisableAuthEntirely: return nil @@ -611,7 +597,7 @@ func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) p // split cookies and parses it. If it cannot be parsed successfully, the issue // is logged and we return blank, so logic will continue as the anonymous user. // The public ID and _encrypted_ token are returned along with the token format. -func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, TokenFormat) { +func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, uint32) { const op = "auth.GetTokenFromRequest" // First, get the token, either from the authorization header or from split // cookies @@ -643,21 +629,21 @@ func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Reque // We didn't find auth info or a client screwed up and put in a blank // header instead of nothing at all, so return blank which will indicate // the anonymouse user - return "", "", AuthTokenTypeUnknown + return "", "", uint32(AuthTokenTypeUnknown) } if strings.HasPrefix(fullToken, "r_") { - return "", fullToken, AuthTokenTypeRecoveryKms + return "", fullToken, uint32(AuthTokenTypeRecoveryKms) } splitFullToken := strings.Split(fullToken, "_") if len(splitFullToken) != 3 { event.WriteError(ctx, op, stderrors.New("unexpected number of segments in token; continuing as anonymous user"), event.WithInfo("expected", 3, "found", len(splitFullToken))) - return "", "", AuthTokenTypeUnknown + return "", "", uint32(AuthTokenTypeUnknown) } publicId := strings.Join(splitFullToken[0:2], "_") encryptedToken := splitFullToken[2] - return publicId, encryptedToken, receivedTokenType + return publicId, encryptedToken, uint32(receivedTokenType) } diff --git a/internal/servers/controller/auth/auth_test.go b/internal/servers/controller/auth/auth_test.go index 574ac0cb3..e90b523f2 100644 --- a/internal/servers/controller/auth/auth_test.go +++ b/internal/servers/controller/auth/auth_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" @@ -107,17 +108,17 @@ func TestAuthTokenAuthenticator(t *testing.T) { } // Add values for authn/authz checking - requestInfo := RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, } requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), kms, req) - assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat) + assert.Equal(t, uint32(tc.tokenFormat), requestInfo.TokenFormat) if tc.userId == "" { return } - ctx := NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) v, ok := ctx.Value(verifierKey).(*verifier) require.True(t, ok) diff --git a/internal/servers/controller/auth/testing.go b/internal/servers/controller/auth/testing.go index 731da8bd2..6a50b5b49 100644 --- a/internal/servers/controller/auth/testing.go +++ b/internal/servers/controller/auth/testing.go @@ -3,6 +3,7 @@ package auth import ( "context" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" ) @@ -11,16 +12,16 @@ import ( // auth checking entirely disabled. Supported options: WithScopeId an WithUserId // are used directly; WithKms is passed through into the verifier context. func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, opt ...Option) context.Context { - reqInfo := RequestInfo{DisableAuthEntirely: true} + reqInfo := authpb.RequestInfo{DisableAuthEntirely: true} opts := getOpts(opt...) - reqInfo.scopeIdOverride = opts.withScopeId - if reqInfo.scopeIdOverride == "" { - reqInfo.scopeIdOverride = scopeId + reqInfo.ScopeIdOverride = opts.withScopeId + if reqInfo.ScopeIdOverride == "" { + reqInfo.ScopeIdOverride = scopeId } - reqInfo.userIdOverride = opts.withUserId - if reqInfo.userIdOverride == "" { - reqInfo.userIdOverride = "u_auth" + reqInfo.UserIdOverride = opts.withUserId + if reqInfo.UserIdOverride == "" { + reqInfo.UserIdOverride = "u_auth" } requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) - return NewVerifierContext(requestContext, iamRepoFn, nil, nil, opts.withKms, reqInfo) + return NewVerifierContext(requestContext, iamRepoFn, nil, nil, opts.withKms, &reqInfo) } diff --git a/internal/servers/controller/common/scopeids/scope_ids_test.go b/internal/servers/controller/common/scopeids/scope_ids_test.go index 570098e9d..d0598b975 100644 --- a/internal/servers/controller/common/scopeids/scope_ids_test.go +++ b/internal/servers/controller/common/scopeids/scope_ids_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" @@ -229,9 +230,9 @@ func TestListingScopeIds(t *testing.T) { authTokenRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 0e389e3b2..6041e93aa 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -26,6 +28,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/patrickmn/go-cache" ua "go.uber.org/atomic" + "google.golang.org/grpc" ) type Controller struct { @@ -44,6 +47,12 @@ type Controller struct { // Used for testing and tracking worker health workerStatusUpdateTimes *sync.Map + // grpc gateway server + gatewayServer *grpc.Server + gatewayTicket string + gatewayListener gatewayListener + gatewayMux *runtime.ServeMux + // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory VaultCredentialRepoFn common.VaultCredentialRepoFactory @@ -172,7 +181,7 @@ func (c *Controller) Start() error { if err := c.scheduler.Start(c.baseContext, &c.schedulerWg); err != nil { return fmt.Errorf("error starting scheduler: %w", err) } - if err := c.startListeners(); err != nil { + if err := c.startListeners(c.baseContext); err != nil { return fmt.Errorf("error starting controller listeners: %w", err) } diff --git a/internal/servers/controller/gateway.go b/internal/servers/controller/gateway.go new file mode 100644 index 000000000..41f6d76a9 --- /dev/null +++ b/internal/servers/controller/gateway.go @@ -0,0 +1,67 @@ +package controller + +import ( + "context" + "math" + "net" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "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/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" +) + +// newGatewayListener will create an in-memory listener +func newGatewayListener() (gatewayListener, string) { + buffer := globals.DefaultMaxRequestSize // seems like a reasonable size for the ring buffer, but then happily change the size if more info becomes available + return bufconn.Listen(int(buffer)), "" +} + +const gatewayTarget = "" + +type gatewayListener interface { + net.Listener + Dial() (net.Conn, error) +} + +func gatewayDialOptions(lis gatewayListener) []grpc.DialOption { + return []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + } +} + +func newGatewayMux() *runtime.ServeMux { + return runtime.NewServeMux( + runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ + Marshaler: handlers.JSONMarshaler(), + }), + runtime.WithErrorHandler(handlers.ErrorHandler()), + runtime.WithForwardResponseOption(handlers.OutgoingResponseFilter), + ) +} + +func newGatewayServer(ctx context.Context, iamRepoFn common.IamRepoFactory, authTokenRepoFn common.AuthTokenRepoFactory, serversRepoFn common.ServersRepoFactory, kms *kms.Kms) (*grpc.Server, string, error) { + const op = "controller.newGatewayServer" + ticket, err := db.NewPrivateId("gwticket") + if err != nil { + return nil, "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate gateway ticket")) + } + requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, ticket) + if err != nil { + return nil, "", err + } + return grpc.NewServer( + grpc.MaxRecvMsgSize(math.MaxInt32), + grpc.MaxSendMsgSize(math.MaxInt32), + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(requestCtxInterceptor, errorInterceptor(ctx), statusCodeInterceptor(ctx))), + ), ticket, nil +} diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 0bd972914..da5f6d5ec 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -13,35 +13,36 @@ import ( "strings" "time" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/observability/event" - "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/common" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/handlers/accounts" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens" "github.com/hashicorp/boundary/internal/servers/controller/handlers/credentiallibraries" "github.com/hashicorp/boundary/internal/servers/controller/handlers/credentialstores" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/groups" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_catalogs" "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/hosts" "github.com/hashicorp/boundary/internal/servers/controller/handlers/managed_groups" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/roles" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/scopes" "github.com/hashicorp/boundary/internal/servers/controller/handlers/sessions" "github.com/hashicorp/boundary/internal/servers/controller/handlers/targets" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/users" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mr-tron/base58" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" "github.com/hashicorp/boundary/internal/servers/controller/handlers" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/groups" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_catalogs" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/hosts" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/roles" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/scopes" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/users" ) type HandlerProperties struct { @@ -78,127 +79,169 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er // Register*ServiceHandlerServer methods ignore the passed in ctx. Using it // now however in case this changes in the future. ctx := props.CancelCtx - mux := runtime.NewServeMux( - runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ - Marshaler: handlers.JSONMarshaler(), - }), - runtime.WithErrorHandler(handlers.ErrorHandler()), - runtime.WithForwardResponseOption(handlers.OutgoingInterceptor), - ) - hcs, err := host_catalogs.NewService(c.StaticHostRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host catalog handler service: %w", err) + + currentServices := c.gatewayServer.GetServiceInfo() + dialOptions := gatewayDialOptions(c.gatewayListener) + + if _, ok := currentServices[services.HostCatalogService_ServiceDesc.ServiceName]; !ok { + hcs, err := host_catalogs.NewService(c.StaticHostRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host catalog handler service: %w", err) + } + services.RegisterHostCatalogServiceServer(c.gatewayServer, hcs) + if err := services.RegisterHostCatalogServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host catalog service handler: %w", err) + } } - if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil { - return nil, fmt.Errorf("failed to register host catalog service handler: %w", err) + if _, ok := currentServices[services.HostSetService_ServiceDesc.ServiceName]; !ok { + hss, err := host_sets.NewService(c.StaticHostRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host set handler service: %w", err) + } + services.RegisterHostSetServiceServer(c.gatewayServer, hss) + if err := services.RegisterHostSetServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host set service handler: %w", err) + } } - hss, err := host_sets.NewService(c.StaticHostRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host set handler service: %w", err) + if _, ok := currentServices[services.HostService_ServiceDesc.ServiceName]; !ok { + hs, err := hosts.NewService(c.StaticHostRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host handler service: %w", err) + } + services.RegisterHostServiceServer(c.gatewayServer, hs) + if err := services.RegisterHostServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host service handler: %w", err) + } } - if err := services.RegisterHostSetServiceHandlerServer(ctx, mux, hss); err != nil { - return nil, fmt.Errorf("failed to register host set service handler: %w", err) + if _, ok := currentServices[services.AccountService_ServiceDesc.ServiceName]; !ok { + accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create account handler service: %w", err) + } + services.RegisterAccountServiceServer(c.gatewayServer, accts) + if err := services.RegisterAccountServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register account service handler: %w", err) + } } - hs, err := hosts.NewService(c.StaticHostRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host handler service: %w", err) + if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok { + authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create auth method handler service: %w", err) + } + services.RegisterAuthMethodServiceServer(c.gatewayServer, authMethods) + if err := services.RegisterAuthMethodServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register auth method service handler: %w", err) + } } - if err := services.RegisterHostServiceHandlerServer(ctx, mux, hs); err != nil { - return nil, fmt.Errorf("failed to register host service handler: %w", err) + if _, ok := currentServices[services.AuthTokenService_ServiceDesc.ServiceName]; !ok { + authtoks, err := authtokens.NewService(c.AuthTokenRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create auth token handler service: %w", err) + } + services.RegisterAuthTokenServiceServer(c.gatewayServer, authtoks) + if err := services.RegisterAuthTokenServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register auth token service handler: %w", err) + } } - accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create account handler service: %w", err) + if _, ok := currentServices[services.ScopeService_ServiceDesc.ServiceName]; !ok { + os, err := scopes.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create scope handler service: %w", err) + } + services.RegisterScopeServiceServer(c.gatewayServer, os) + if err := services.RegisterScopeServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register scope service handler: %w", err) + } } - if err := services.RegisterAccountServiceHandlerServer(ctx, mux, accts); err != nil { - return nil, fmt.Errorf("failed to register account service handler: %w", err) + if _, ok := currentServices[services.UserService_ServiceDesc.ServiceName]; !ok { + us, err := users.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create user handler service: %w", err) + } + services.RegisterUserServiceServer(c.gatewayServer, us) + if err := services.RegisterUserServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register user service handler: %w", err) + } } - authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create auth method handler service: %w", err) + if _, ok := currentServices[services.TargetService_ServiceDesc.ServiceName]; !ok { + ts, err := targets.NewService( + c.kms, + c.TargetRepoFn, + c.IamRepoFn, + c.ServersRepoFn, + c.SessionRepoFn, + c.StaticHostRepoFn, + c.VaultCredentialRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create target handler service: %w", err) + } + services.RegisterTargetServiceServer(c.gatewayServer, ts) + if err := services.RegisterTargetServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register target service handler: %w", err) + } } - if err := services.RegisterAuthMethodServiceHandlerServer(ctx, mux, authMethods); err != nil { - return nil, fmt.Errorf("failed to register auth method service handler: %w", err) + if _, ok := currentServices[services.GroupService_ServiceDesc.ServiceName]; !ok { + gs, err := groups.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create group handler service: %w", err) + } + services.RegisterGroupServiceServer(c.gatewayServer, gs) + if err := services.RegisterGroupServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register group service handler: %w", err) + } } - authtoks, err := authtokens.NewService(c.AuthTokenRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create auth token handler service: %w", err) + if _, ok := currentServices[services.RoleService_ServiceDesc.ServiceName]; !ok { + rs, err := roles.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create role handler service: %w", err) + } + services.RegisterRoleServiceServer(c.gatewayServer, rs) + if err := services.RegisterRoleServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register role service handler: %w", err) + } } - if err := services.RegisterAuthTokenServiceHandlerServer(ctx, mux, authtoks); err != nil { - return nil, fmt.Errorf("failed to register auth token service handler: %w", err) + if _, ok := currentServices[services.SessionService_ServiceDesc.ServiceName]; !ok { + ss, err := sessions.NewService(c.SessionRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create session handler service: %w", err) + } + services.RegisterSessionServiceServer(c.gatewayServer, ss) + if err := services.RegisterSessionServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register session service handler: %w", err) + } } - os, err := scopes.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create scope handler service: %w", err) + if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok { + mgs, err := managed_groups.NewService(c.OidcRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create managed groups handler service: %w", err) + } + services.RegisterManagedGroupServiceServer(c.gatewayServer, mgs) + if err := services.RegisterManagedGroupServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register managed groups service handler: %w", err) + } } - if err := services.RegisterScopeServiceHandlerServer(ctx, mux, os); err != nil { - return nil, fmt.Errorf("failed to register scope service handler: %w", err) + if _, ok := currentServices[services.CredentialStoreService_ServiceDesc.ServiceName]; !ok { + cs, err := credentialstores.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create credential store handler service: %w", err) + } + services.RegisterCredentialStoreServiceServer(c.gatewayServer, cs) + if err := services.RegisterCredentialStoreServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register credential store service handler: %w", err) + } } - us, err := users.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create user handler service: %w", err) - } - if err := services.RegisterUserServiceHandlerServer(ctx, mux, us); err != nil { - return nil, fmt.Errorf("failed to register user service handler: %w", err) - } - ts, err := targets.NewService( - c.kms, - c.TargetRepoFn, - c.IamRepoFn, - c.ServersRepoFn, - c.SessionRepoFn, - c.StaticHostRepoFn, - c.VaultCredentialRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create target handler service: %w", err) - } - if err := services.RegisterTargetServiceHandlerServer(ctx, mux, ts); err != nil { - return nil, fmt.Errorf("failed to register target service handler: %w", err) - } - gs, err := groups.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create group handler service: %w", err) - } - if err := services.RegisterGroupServiceHandlerServer(ctx, mux, gs); err != nil { - return nil, fmt.Errorf("failed to register group service handler: %w", err) - } - rs, err := roles.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create role handler service: %w", err) - } - if err := services.RegisterRoleServiceHandlerServer(ctx, mux, rs); err != nil { - return nil, fmt.Errorf("failed to register role service handler: %w", err) - } - ss, err := sessions.NewService(c.SessionRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create session handler service: %w", err) - } - if err := services.RegisterSessionServiceHandlerServer(ctx, mux, ss); err != nil { - return nil, fmt.Errorf("failed to register session service handler: %w", err) - } - mgs, err := managed_groups.NewService(c.OidcRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create managed groups handler service: %w", err) - } - if err := services.RegisterManagedGroupServiceHandlerServer(ctx, mux, mgs); err != nil { - return nil, fmt.Errorf("failed to register managed groups service handler: %w", err) - } - cs, err := credentialstores.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create credential store handler service: %w", err) - } - if err := services.RegisterCredentialStoreServiceHandlerServer(ctx, mux, cs); err != nil { - return nil, fmt.Errorf("failed to register credential store service handler: %w", err) - } - cl, err := credentiallibraries.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create credential library handler service: %w", err) - } - if err := services.RegisterCredentialLibraryServiceHandlerServer(ctx, mux, cl); err != nil { - return nil, fmt.Errorf("failed to register credential library service handler: %w", err) + if _, ok := currentServices[services.CredentialLibraryService_ServiceDesc.ServiceName]; !ok { + cl, err := credentiallibraries.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create credential library handler service: %w", err) + } + services.RegisterCredentialLibraryServiceServer(c.gatewayServer, cl) + if err := services.RegisterCredentialLibraryServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register credential library service handler: %w", err) + } } - return mux, nil + return c.gatewayMux, nil } func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProperties) http.Handler { @@ -236,29 +279,27 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp } // Add values for authn/authz checking - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: r.URL.Path, Method: r.Method, DisableAuthzFailures: disableAuthzFailures, } requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(ctx, c.kms, r) - ctx = auth.NewVerifierContext(ctx, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms, requestInfo) - // Add general request information to the context. The information from - // the auth verifier context is pretty specifically curated to - // authentication/authorization verification so this is more - // general-purpose. - // - // We could use requests.NewRequestContext but this saves an immediate - // lookup. - ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{ - Path: r.URL.Path, - Method: r.Method, - }) - - // Set the context back on the request - r = r.WithContext(ctx) + // Serialize the request info to send it across the wire to the + // grpc-gateway via an http header + requestInfo.Ticket = c.gatewayTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy + marshalledRequestInfo, err := proto.Marshal(&requestInfo) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error marshaling request info")) + w.WriteHeader(http.StatusInternalServerError) + return + } + // Use the default grpc-gateway mapping rule to pass the request info as + // metadata. + // See: https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/runtime#DefaultHeaderMatcher + r.Header.Set("Grpc-Metadata-"+requestInfoMdKey, base58.FastBase58Encoding(marshalledRequestInfo)) h.ServeHTTP(w, r) }) diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index 4fb5f0aa2..4bf43902a 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -389,7 +389,7 @@ func (s Service) DeleteAuthMethod(ctx context.Context, req *pbs.DeleteAuthMethod if err != nil { return nil, err } - return nil, nil + return &pbs.DeleteAuthMethodResponse{}, nil } // Authenticate implements the interface pbs.AuthenticationServiceServer. diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index f1d472a11..8dbd20d93 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -428,12 +428,14 @@ func TestDelete(t *testing.T) { req: &pbs.DeleteAuthMethodRequest{ Id: pwam.GetPublicId(), }, + res: &pbs.DeleteAuthMethodResponse{}, }, { name: "Delete an Existing OIDC AuthMethod", req: &pbs.DeleteAuthMethodRequest{ Id: oidcam.GetPublicId(), }, + res: &pbs.DeleteAuthMethodResponse{}, }, { name: "Delete bad auth_method id", diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go index d843c4603..ddc308af8 100644 --- a/internal/servers/controller/handlers/authmethods/oidc_test.go +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -13,6 +13,8 @@ import ( "time" "github.com/google/go-cmp/cmp" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -206,9 +208,9 @@ func TestList_FilterNonPublic(t *testing.T) { atRepoFn, serversRepoFn, kmsCache, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) }(), diff --git a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go index 784ac8a3e..cff486682 100644 --- a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go +++ b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/requests" @@ -87,15 +88,15 @@ func TestGetSelf(t *testing.T) { require, assert := require.New(t), assert.New(t) // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.readId), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.token.GetPublicId(), Token: tc.token.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{}) got, err := a.GetAuthToken(ctx, &pbs.GetAuthTokenRequest{Id: tc.readId}) if tc.err != nil { @@ -236,15 +237,15 @@ func TestList_Self(t *testing.T) { require, assert := require.New(t), assert.New(t) // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens?scope_id=%s", o.GetPublicId()), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.requester.GetPublicId(), Token: tc.requester.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := a.ListAuthTokens(ctx, &pbs.ListAuthTokensRequest{ScopeId: o.GetPublicId()}) require.NoError(err) require.Len(got.Items, 1) @@ -482,15 +483,15 @@ func TestDeleteSelf(t *testing.T) { require := require.New(t) // Setup the auth request information req := httptest.NewRequest("DELETE", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.deleteId), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.token.GetPublicId(), Token: tc.token.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := a.DeleteAuthToken(ctx, &pbs.DeleteAuthTokenRequest{Id: tc.deleteId}) if tc.err != nil { require.EqualError(err, tc.err.Error()) diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index d9dd0118b..681294a1a 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -555,6 +555,7 @@ func TestDelete(t *testing.T) { name string id string err error + res *pbs.DeleteCredentialLibraryResponse }{ { name: "success", @@ -574,7 +575,7 @@ func TestDelete(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, gErr := s.DeleteCredentialLibrary(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.DeleteCredentialLibraryRequest{Id: tc.id}) - assert.Nil(t, got) + assert.EqualValuesf(t, tc.res, got, "DeleteCredentialLibrary got response %q, wanted %q", got, tc.res) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) diff --git a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go index 9e087bd46..bfa43c54b 100644 --- a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go +++ b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go @@ -674,6 +674,7 @@ func TestDelete(t *testing.T) { name string id string err error + res *pbs.DeleteCredentialStoreResponse }{ { name: "success", @@ -693,7 +694,7 @@ func TestDelete(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, gErr := s.DeleteCredentialStore(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.DeleteCredentialStoreRequest{Id: tc.id}) - assert.Nil(t, got) + assert.EqualValuesf(t, tc.res, got, "DeleteCredentialStore got response %q, wanted %q", got, tc.res) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) diff --git a/internal/servers/controller/handlers/errors.go b/internal/servers/controller/handlers/errors.go index 1628de364..c23e59164 100644 --- a/internal/servers/controller/handlers/errors.go +++ b/internal/servers/controller/handlers/errors.go @@ -5,31 +5,46 @@ import ( "fmt" "io" "net/http" + "regexp" "sort" + "strconv" "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" "github.com/hashicorp/boundary/internal/observability/event" + "github.com/mr-tron/base58" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) const ( genericUniquenessMsg = "Invalid request. Request attempted to make second resource with the same field value that must be unique." genericNotFoundMsg = "Unable to find requested resource." + + // domainErrHeader defines an http header for encoded domain errors from the + // grpc server. + domainErrHeader = "x-domain-err" + // domainErrMetadataHeader defines an http header for encoded domain errors from the + // grpc server via metadata + domainErrMetadataHeader = "Grpc-Metadata-X-Domain-Err" + + apiErrHeader = "x-api-err" + apiErrMetadataHeader = "Grpc-Metadata-X-Api-Err" ) -type apiError struct { - status int32 - inner *pb.Error +type ApiError struct { + Status int32 + Inner *pb.Error } -func (e *apiError) Error() string { - res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.status, e.inner.GetKind(), e.inner.GetMessage()) +func (e *ApiError) Error() string { + res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.Status, e.Inner.GetKind(), e.Inner.GetMessage()) var dets []string - for _, rf := range e.inner.GetDetails().GetRequestFields() { + for _, rf := range e.Inner.GetDetails().GetRequestFields() { dets = append(dets, fmt.Sprintf("{name: %q, desc: %q}", rf.GetName(), rf.GetDescription())) } if len(dets) > 0 { @@ -39,19 +54,19 @@ func (e *apiError) Error() string { return res } -func (e *apiError) Is(target error) bool { - var tApiErr *apiError +func (e *ApiError) Is(target error) bool { + var tApiErr *ApiError if !errors.As(target, &tApiErr) { return false } - return tApiErr.inner.Kind == e.inner.Kind && tApiErr.status == e.status + return tApiErr.Inner.Kind == e.Inner.Kind && tApiErr.Status == e.Status } // ApiErrorWithCode returns an api error with the provided code. func ApiErrorWithCode(c codes.Code) error { - return &apiError{ - status: int32(runtime.HTTPStatusFromCode(c)), - inner: &pb.Error{ + return &ApiError{ + Status: int32(runtime.HTTPStatusFromCode(c)), + Inner: &pb.Error{ Kind: c.String(), }, } @@ -59,9 +74,9 @@ func ApiErrorWithCode(c codes.Code) error { // ApiErrorWithCodeAndMessage returns an api error with the provided code and message. func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) error { - return &apiError{ - status: int32(runtime.HTTPStatusFromCode(c)), - inner: &pb.Error{ + return &ApiError{ + Status: int32(runtime.HTTPStatusFromCode(c)), + Inner: &pb.Error{ Kind: c.String(), Message: fmt.Sprintf(msg, args...), }, @@ -70,9 +85,9 @@ func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) e // NotFoundError returns an ApiError indicating a resource couldn't be found. func NotFoundError() error { - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: "Resource not found.", }, @@ -80,19 +95,19 @@ func NotFoundError() error { } // NotFoundErrorf returns an ApiError indicating a resource couldn't be found. -func NotFoundErrorf(msg string, a ...interface{}) *apiError { - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ +func NotFoundErrorf(msg string, a ...interface{}) *ApiError { + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: fmt.Sprintf(msg, a...), }, } } -var unauthorizedError = &apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ +var unauthorizedError = &ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: codes.PermissionDenied.String(), Message: "Forbidden.", }, @@ -102,9 +117,9 @@ func ForbiddenError() error { return unauthorizedError } -var unauthenticatedError = &apiError{ - status: http.StatusUnauthorized, - inner: &pb.Error{ +var unauthenticatedError = &ApiError{ + Status: http.StatusUnauthorized, + Inner: &pb.Error{ Kind: codes.Unauthenticated.String(), Message: "Unauthenticated, or invalid token.", }, @@ -114,47 +129,49 @@ func UnauthenticatedError() error { return unauthenticatedError } -func InvalidArgumentErrorf(msg string, fields map[string]string) *apiError { +func InvalidArgumentErrorf(msg string, fields map[string]string) *ApiError { const op = "handlers.InvalidArgumentErrorf" ctx := context.TODO() err := ApiErrorWithCodeAndMessage(codes.InvalidArgument, msg) - var apiErr *apiError + var apiErr *ApiError if !errors.As(err, &apiErr) { event.WriteError(ctx, op, err, event.WithInfoMsg("Unable to build invalid argument api error.")) } if len(fields) > 0 { - apiErr.inner.Details = &pb.ErrorDetails{} + apiErr.Inner.Details = &pb.ErrorDetails{} } for k, v := range fields { - apiErr.inner.Details.RequestFields = append(apiErr.inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v}) + apiErr.Inner.Details.RequestFields = append(apiErr.Inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v}) } - sort.Slice(apiErr.inner.GetDetails().GetRequestFields(), func(i, j int) bool { - return apiErr.inner.Details.RequestFields[i].GetName() < apiErr.inner.Details.RequestFields[j].GetName() + sort.Slice(apiErr.Inner.GetDetails().GetRequestFields(), func(i, j int) bool { + return apiErr.Inner.Details.RequestFields[i].GetName() < apiErr.Inner.Details.RequestFields[j].GetName() }) return apiErr } +var statusRegEx = regexp.MustCompile("Status: ([0-9]+), Kind: \"(.*)\", Error: \"(.*)\"") + // Converts a known errors into an error that can presented to an end user over the API. -func backendErrorToApiError(inErr error) *apiError { +func backendErrorToApiError(inErr error) *ApiError { stErr := status.Convert(inErr) switch { case errors.Is(inErr, runtime.ErrNotMatch): // grpc gateway uses this error when the path was not matched, but the error uses codes.Unimplemented which doesn't match the intention. // Overwrite the error to match our expected behavior. - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: http.StatusText(http.StatusNotFound), }, } case status.Code(inErr) == codes.Unimplemented: // Instead of returning a 501 we always want to return a 405 when a method isn't implemented. - return &apiError{ - status: http.StatusMethodNotAllowed, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusMethodNotAllowed, + Inner: &pb.Error{ Kind: codes.Unimplemented.String(), Message: stErr.Message(), }, @@ -176,11 +193,23 @@ func backendErrorToApiError(inErr error) *apiError { statusCode = int32(domainErr.Code) } + // perhaps the error is from the grpc gateway, so match against the known + // apiError msg format. + if found := statusRegEx.FindStringSubmatch(stErr.Message()); len(found) == 4 { + u32, err := strconv.ParseInt(found[1], 10, 32) + if err == nil { // notice it's testing for NO err + return &ApiError{ + Status: int32(u32), + Inner: &pb.Error{Kind: found[2], Message: found[3]}, + } + } + } + // TODO: Don't return potentially sensitive information (like which user id an account // is already associated with when attempting to re-associate it). - return &apiError{ - status: statusCode, - inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()}, + return &ApiError{ + Status: statusCode, + Inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()}, } } @@ -189,7 +218,70 @@ func ErrorHandler() runtime.ErrorHandlerFunc { const errorFallback = `{"error": "failed to marshal error message"}` return func(ctx context.Context, _ *runtime.ServeMux, mar runtime.Marshaler, w http.ResponseWriter, r *http.Request, inErr error) { // API specified error, otherwise we need to translate repo/db errors. - var apiErr *apiError + + // the grpc server will encoded domain errors into the x-domain-err, so + // let's check there first. (see: controller.errorInterceptor) + md, ok := runtime.ServerMetadataFromContext(ctx) + if ok { + defer func() { + // make sure we don't leak the headers that were used as a comm + // channel between the grpc server and the http proxy + delete(md.HeaderMD, domainErrHeader) + delete(w.Header(), domainErrMetadataHeader) + + delete(md.HeaderMD, apiErrHeader) + delete(w.Header(), apiErrMetadataHeader) + }() + domainErrHdrs := md.HeaderMD.Get(domainErrHeader) + apiErrHdrs := md.HeaderMD.Get(apiErrHeader) + + switch { + case len(domainErrHdrs) > 0: + decoded, err := base58.FastBase58Decoding(domainErrHdrs[0]) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to decode domain err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + var pbErr pberrors.Err + if err := proto.Unmarshal(decoded, &pbErr); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to marshal domain err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + inErr = errors.FromPbErrors(&pbErr) + case len(apiErrHdrs) > 0: + decoded, err := base58.FastBase58Decoding(apiErrHdrs[0]) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to decode api err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + var pbApiErr pberrors.ApiError + if err := proto.Unmarshal(decoded, &pbApiErr); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to marshal api err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + inErr = &ApiError{ + Status: pbApiErr.Status, + Inner: pbApiErr.ApiError, + } + } + } + var apiErr *ApiError isApiErr := errors.As(inErr, &apiErr) if !isApiErr { if err := backendErrorToApiError(inErr); err != nil && !errors.As(err, &apiErr) { @@ -197,13 +289,13 @@ func ErrorHandler() runtime.ErrorHandlerFunc { } } - if apiErr.status == http.StatusInternalServerError { + if apiErr.Status == http.StatusInternalServerError { event.WriteError(ctx, op, inErr, event.WithInfoMsg("internal error returned")) } - buf, merr := mar.Marshal(apiErr.inner) + buf, merr := mar.Marshal(apiErr.Inner) if merr != nil { - event.WriteError(ctx, op, merr, event.WithInfoMsg("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.inner))) + event.WriteError(ctx, op, merr, event.WithInfoMsg("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.Inner))) w.WriteHeader(http.StatusInternalServerError) if _, err := io.WriteString(w, errorFallback); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) @@ -211,8 +303,8 @@ func ErrorHandler() runtime.ErrorHandlerFunc { return } - w.Header().Set("Content-Type", mar.ContentType(apiErr.inner)) - w.WriteHeader(int(apiErr.status)) + w.Header().Set("Content-Type", mar.ContentType(apiErr.Inner)) + w.WriteHeader(int(apiErr.Status)) if _, err := w.Write(buf); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to send response chunk")) return @@ -221,5 +313,5 @@ func ErrorHandler() runtime.ErrorHandlerFunc { } func ToApiError(e error) *pb.Error { - return backendErrorToApiError(e).inner + return backendErrorToApiError(e).Inner } diff --git a/internal/servers/controller/handlers/errors_test.go b/internal/servers/controller/handlers/errors_test.go index f4b2f34fc..f76a1a51b 100644 --- a/internal/servers/controller/handlers/errors_test.go +++ b/internal/servers/controller/handlers/errors_test.go @@ -32,14 +32,14 @@ func TestApiErrorHandler(t *testing.T) { testCases := []struct { name string err error - expected apiError + expected ApiError }{ { name: "Not Found", err: NotFoundErrorf("Test"), - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: "Test", }, @@ -51,9 +51,9 @@ func TestApiErrorHandler(t *testing.T) { "k1": "v1", "k2": "v2", }), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Test", Details: &pb.ErrorDetails{ @@ -74,9 +74,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "GrpcGateway Routing Error", err: runtime.ErrNotMatch, - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: http.StatusText(http.StatusNotFound), }, @@ -85,9 +85,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Unimplemented error", err: status.Error(codes.Unimplemented, "Test"), - expected: apiError{ - status: http.StatusMethodNotAllowed, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusMethodNotAllowed, + Inner: &pb.Error{ Kind: "Unimplemented", Message: "Test", }, @@ -96,9 +96,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Unknown error", err: stderrors.New("Some random error"), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "Some random error", }, @@ -107,9 +107,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid public id", err: errors.E(ctx, errors.WithCode(errors.InvalidPublicId)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "invalid public id, parameter violation: error #102", }, @@ -118,9 +118,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid parameter", err: errors.E(ctx, errors.WithCode(errors.InvalidParameter)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "invalid parameter, parameter violation: error #100", }, @@ -129,9 +129,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid field mask", err: errors.E(ctx, errors.WithCode(errors.InvalidFieldMask)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Error in provided request", Details: &pb.ErrorDetails{RequestFields: []*pb.FieldError{{Name: "update_mask", Description: "Invalid update mask provided."}}}, @@ -141,9 +141,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db empty field mask", err: errors.E(ctx, errors.WithCode(errors.EmptyFieldMask)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Error in provided request", Details: &pb.ErrorDetails{RequestFields: []*pb.FieldError{{Name: "update_mask", Description: "Invalid update mask provided."}}}, @@ -153,9 +153,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db not unqiue", err: errors.E(ctx, errors.WithCode(errors.NotUnique)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: genericUniquenessMsg, }, @@ -164,9 +164,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db record not found", err: errors.E(ctx, errors.WithCode(errors.RecordNotFound)), - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: genericNotFoundMsg, }, @@ -175,9 +175,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db multiple records", err: errors.E(ctx, errors.WithCode(errors.MultipleRecords)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "multiple records, search issue: error #1101", }, @@ -186,9 +186,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error account already associated", err: errors.E(ctx, errors.WithCode(errors.AccountAlreadyAssociated)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "account already associated with another user, parameter violation: error #114", }, @@ -197,9 +197,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Wrapped domain error", err: errors.E(ctx, errors.WithCode(errors.InvalidAddress), errors.WithMsg("test msg"), errors.WithWrap(errors.E(ctx, errors.WithCode(errors.NotNull), errors.WithMsg("inner msg")))), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "test msg: parameter violation: error #101: inner msg: integrity violation: error #1001", }, @@ -208,9 +208,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Forbidden domain error", err: errors.E(ctx, errors.WithCode(errors.Forbidden), errors.WithMsg("test msg")), - expected: apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: "Internal", Message: "test msg: unknown: error #403", }, @@ -219,9 +219,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Wrapped forbidden domain error", err: fmt.Errorf("got error: %w", errors.E(ctx, errors.WithCode(errors.Forbidden), errors.WithMsg("test msg"))), - expected: apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: "Internal", Message: "got error: test msg: unknown: error #403", }, @@ -234,7 +234,7 @@ func TestApiErrorHandler(t *testing.T) { w := httptest.NewRecorder() tested(ctx, mux, outMarsh, w, req, tc.err) resp := w.Result() - assert.EqualValues(tc.expected.status, resp.StatusCode) + assert.EqualValues(tc.expected.Status, resp.StatusCode) got, err := ioutil.ReadAll(resp.Body) require.NoError(err) @@ -243,8 +243,8 @@ func TestApiErrorHandler(t *testing.T) { err = inMarsh.Unmarshal(got, gotErr) require.NoError(err) - assert.Equal(tc.expected.status, int32(resp.StatusCode)) - assert.Empty(cmp.Diff(tc.expected.inner, gotErr, protocmp.Transform())) + assert.Equal(tc.expected.Status, int32(resp.StatusCode)) + assert.Empty(cmp.Diff(tc.expected.Inner, gotErr, protocmp.Transform())) }) } } diff --git a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go index 7c468be33..1ca2cda25 100644 --- a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go +++ b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go @@ -417,6 +417,7 @@ func TestDelete_twice(t *testing.T) { o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) oidcAm := oidc.TestAuthMethod( t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, diff --git a/internal/servers/controller/handlers/outgoing_interceptor_test.go b/internal/servers/controller/handlers/outgoing_interceptor_test.go deleted file mode 100644 index 798368596..000000000 --- a/internal/servers/controller/handlers/outgoing_interceptor_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package handlers - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" - pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" -) - -func TestOutgoingSplitCookie(t *testing.T) { - rec := httptest.NewRecorder() - attrs, err := ProtoToStruct(&pb.AuthToken{Token: "t_abc_1234567890"}) - require.NoError(t, err) - attrs.GetFields()["token_type"] = structpb.NewStringValue("cookie") - require.NoError(t, OutgoingInterceptor(context.Background(), rec, &pbs.AuthenticateResponse{Attributes: attrs})) - assert.ElementsMatch(t, rec.Result().Cookies(), []*http.Cookie{ - {Name: HttpOnlyCookieName, Value: "34567890", HttpOnly: true, Raw: "wt-http-token-cookie=34567890; HttpOnly"}, - {Name: JsVisibleCookieName, Value: "t_abc_12", Raw: "wt-js-token-cookie=t_abc_12"}, - }) -} diff --git a/internal/servers/controller/handlers/outgoing_interceptor.go b/internal/servers/controller/handlers/outgoing_response_filter.go similarity index 55% rename from internal/servers/controller/handlers/outgoing_interceptor.go rename to internal/servers/controller/handlers/outgoing_response_filter.go index 2814c2b10..cf6242002 100644 --- a/internal/servers/controller/handlers/outgoing_interceptor.go +++ b/internal/servers/controller/handlers/outgoing_response_filter.go @@ -4,30 +4,66 @@ import ( "context" "fmt" "net/http" + "strconv" "strings" + "github.com/hashicorp/boundary/internal/errors" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/observability/event" authtokenpb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" ) const ( - HttpOnlyCookieName = "wt-http-token-cookie" - JsVisibleCookieName = "wt-js-token-cookie" - tokenTypeField = "token_type" - finalRedirectUrlField = "final_redirect_url" - statusField = "status" + HttpOnlyCookieName = "wt-http-token-cookie" + JsVisibleCookieName = "wt-js-token-cookie" + tokenTypeField = "token_type" + finalRedirectUrlField = "final_redirect_url" + statusField = "status" + StatusCodeHeader = "x-http-code" + statusCodeMetadataHeader = "Grpc-Metadata-X-Http-Code" ) -func OutgoingInterceptor(ctx context.Context, w http.ResponseWriter, m proto.Message) error { - const op = "handlers.OutgoingInterceptor" - m = m.ProtoReflect().Interface() - if !m.ProtoReflect().IsValid() { - // This would be the case if it's a nil pointer - w.WriteHeader(http.StatusNoContent) - return nil +// SetStatusCode allows a grpc service handler to set the outgoing http status +// code +func SetStatusCode(ctx context.Context, code int) error { + const op = "handlers.SetHttpStatusCode" + if http.StatusText(code) == "" { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown status code: %d", code)) + } + if err := grpc.SetHeader(ctx, metadata.Pairs(StatusCodeHeader, strconv.Itoa(code))); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Internal)) + } + return nil +} + +// OutgoingResponseFilter is a gRPC gateway WithForwardResponseOption. It's +// basically a filter that can manipulate the http response and has acesss to +// the outgoing proto msg +func OutgoingResponseFilter(ctx context.Context, w http.ResponseWriter, m proto.Message) error { + const op = "handlers.OutgoingResponseFilter" + + if md, ok := runtime.ServerMetadataFromContext(ctx); ok { + + // set http status codes based on metadata set by the grpc service + if statusCodes := md.HeaderMD.Get(StatusCodeHeader); len(statusCodes) > 0 { + defer func() { + // delete the headers to not expose any grpc-metadata in http response + delete(md.HeaderMD, StatusCodeHeader) + delete(w.Header(), statusCodeMetadataHeader) + }() + lastStatus, err := strconv.Atoi(statusCodes[len(statusCodes)-1]) + if err != nil { + return fmt.Errorf("%s: unable to convert status code %s: %w", op, statusCodes[len(statusCodes)-1], err) + } + w.WriteHeader(lastStatus) + return nil + } } // add the outgoing resp proto. See: common.flushGatedEvents is where the diff --git a/internal/servers/controller/handlers/outgoing_response_filter_test.go b/internal/servers/controller/handlers/outgoing_response_filter_test.go new file mode 100644 index 000000000..2d0932f56 --- /dev/null +++ b/internal/servers/controller/handlers/outgoing_response_filter_test.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + emptypb "github.com/hashicorp/boundary/internal/gen/controller/api" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestOutgoingSplitCookie(t *testing.T) { + rec := httptest.NewRecorder() + attrs, err := ProtoToStruct(&pb.AuthToken{Token: "t_abc_1234567890"}) + require.NoError(t, err) + attrs.GetFields()["token_type"] = structpb.NewStringValue("cookie") + require.NoError(t, OutgoingResponseFilter(context.Background(), rec, &pbs.AuthenticateResponse{Attributes: attrs})) + assert.ElementsMatch(t, rec.Result().Cookies(), []*http.Cookie{ + {Name: HttpOnlyCookieName, Value: "34567890", HttpOnly: true, Raw: "wt-http-token-cookie=34567890; HttpOnly"}, + {Name: JsVisibleCookieName, Value: "t_abc_12", Raw: "wt-js-token-cookie=t_abc_12"}, + }) +} + +func TestOutgoingResponseFilter_StatusCode(t *testing.T) { + tests := []struct { + name string + md runtime.ServerMetadata + msg proto.Message + wantErr bool + wantStatusCode int + wantContent bool + }{ + { + name: "204", + md: runtime.ServerMetadata{HeaderMD: metadata.Pairs(StatusCodeHeader, "204")}, + wantStatusCode: 204, + }, + { + name: "418", + md: runtime.ServerMetadata{HeaderMD: metadata.Pairs(StatusCodeHeader, "418")}, + wantStatusCode: 418, + wantContent: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + rec := httptest.NewRecorder() + ctx := runtime.NewServerMetadataContext(context.Background(), tt.md) + err := OutgoingResponseFilter(ctx, rec, &emptypb.EmptyResponse{}) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + resp := rec.Result() + if tt.wantStatusCode != 0 { + assert.Equal(tt.wantStatusCode, resp.StatusCode) + } + }) + } + +} diff --git a/internal/servers/controller/handlers/sessions/session_service_test.go b/internal/servers/controller/handlers/sessions/session_service_test.go index a997d8d65..4fa363167 100644 --- a/internal/servers/controller/handlers/sessions/session_service_test.go +++ b/internal/servers/controller/handlers/sessions/session_service_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" @@ -211,15 +212,15 @@ func TestList_Self(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/sessions?scope_id=%s", pWithSessions.GetPublicId()), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.requester.GetPublicId(), Token: tc.requester.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := s.ListSessions(ctx, &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId()}) require.NoError(t, err) assert.Equal(t, tc.count, len(got.GetItems()), got.GetItems()) diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index f5b087d50..7dd69c1ed 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/credential/vault" "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" spbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -2500,9 +2501,9 @@ func TestAuthorizeSession(t *testing.T) { atRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) @@ -2674,9 +2675,9 @@ func TestAuthorizeSession_Errors(t *testing.T) { atRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) r := iam.TestRole(t, conn, proj.GetPublicId()) diff --git a/internal/servers/controller/handlers/verifiers_test.go b/internal/servers/controller/handlers/verifiers_test.go index d1717c2d6..7fd291b12 100644 --- a/internal/servers/controller/handlers/verifiers_test.go +++ b/internal/servers/controller/handlers/verifiers_test.go @@ -15,10 +15,10 @@ import ( func errorIncludesFields(t *testing.T, err error, wantFields []string) { t.Helper() - var apiErr *apiError + var apiErr *ApiError require.True(t, errors.As(err, &apiErr)) var gotFields []string - for _, d := range apiErr.inner.GetDetails().GetRequestFields() { + for _, d := range apiErr.Inner.GetDetails().GetRequestFields() { gotFields = append(gotFields, d.GetName()) } assert.ElementsMatch(t, gotFields, wantFields) diff --git a/internal/servers/controller/interceptor.go b/internal/servers/controller/interceptor.go new file mode 100644 index 000000000..4dfaeab98 --- /dev/null +++ b/internal/servers/controller/interceptor.go @@ -0,0 +1,207 @@ +package controller + +import ( + "context" + "fmt" + "net/http" + "reflect" + + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/requests" + "github.com/hashicorp/boundary/internal/servers/controller/auth" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + + "github.com/mr-tron/base58" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +const ( + requestInfoMdKey = "request-info" + + // domainErrHeader defines an http header for encoded domain errors from the + // grpc server. + domainErrHeader = "x-domain-err" + + // apiErrHeader defines an http header for encoded api errors from the + // grpc server. + apiErrHeader = "x-api-err" +) + +// requestCtxInterceptor creates an unary server interceptor that pulls grpc +// metadata into a ctx for the request. The metadata must be set in an upstream +// http handler/middleware by marshalling a RequestInfo protobuf into the +// requestInfoMdKey header (see: controller.wrapHandlerWithCommonFuncs). +func requestCtxInterceptor( + ctx context.Context, + iamRepoFn common.IamRepoFactory, + authTokenRepoFn common.AuthTokenRepoFactory, + serversRepoFn common.ServersRepoFactory, + kms *kms.Kms, + ticket string, +) (grpc.UnaryServerInterceptor, error) { + const op = "controller.requestCtxInterceptor" + if iamRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing iam repo function") + } + if authTokenRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token repo function") + } + if serversRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing servers repo function") + } + if kms == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms") + } + if ticket == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing ticket") + } + // Authorization unary interceptor function to handle authorize per RPC call + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + md, ok := metadata.FromIncomingContext(interceptorCtx) + if !ok { + return nil, errors.New(interceptorCtx, errors.Internal, op, "No metadata") + } + + values := md.Get(requestInfoMdKey) + if len(values) == 0 { + return nil, errors.New(interceptorCtx, errors.Internal, op, "Missing request metadata") + } + if len(values) > 1 { + return nil, errors.New(interceptorCtx, errors.Internal, op, fmt.Sprintf("expected 1 value for %s metadata and got %d", requestInfoMdKey, len(values))) + } + + decoded, err := base58.FastBase58Decoding(values[0]) + if err != nil { + return nil, errors.Wrap(interceptorCtx, err, op, errors.WithCode(errors.Internal), errors.WithMsg("unable to decode request info")) + } + var requestInfo authpb.RequestInfo + if err := proto.Unmarshal(decoded, &requestInfo); err != nil { + return nil, errors.Wrap(interceptorCtx, err, op, errors.WithCode(errors.Internal), errors.WithMsg("unable to unmarshal request info")) + } + switch { + case requestInfo.Ticket == "": + return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (missing ticket)") + case requestInfo.Ticket != ticket: + return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (bad ticket)") + } + + interceptorCtx = auth.NewVerifierContext(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, &requestInfo) + + // Add general request information to the context. The information from + // the auth verifier context is pretty specifically curated to + // authentication/authorization verification so this is more + // general-purpose. + // + // We could use requests.NewRequestContext but this saves an immediate + // lookup. + interceptorCtx = context.WithValue(interceptorCtx, requests.ContextRequestInformationKey, &requests.RequestContext{ + Path: requestInfo.Path, + Method: requestInfo.Method, + }) + + // Calls the handler + h, err := handler(interceptorCtx, req) + + return h, err // not convinced we want to wrap every error and turn them into domain errors... + }, nil +} + +func errorInterceptor( + _ context.Context, +) grpc.UnaryServerInterceptor { + const op = "controller.errorInterceptor" + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + + // call the handler... + h, handlerErr := handler(interceptorCtx, req) + + // if there's an error and it's a domain err, then encode it into the + if handlerErr != nil { + var domainErr *errors.Err + isDomainErr := errors.As(handlerErr, &domainErr) + if isDomainErr { + pbErr := errors.ToPbErrors(domainErr) + var buf []byte + var err error + if buf, err = proto.Marshal(pbErr); err != nil { + return h, status.Errorf(codes.Internal, "unable to marshal domain error: %s", err) + } + if err := grpc.SetHeader(interceptorCtx, metadata.Pairs(domainErrHeader, base58.FastBase58Encoding(buf))); err != nil { + return h, status.Errorf(codes.Internal, "unable to set domain error header: %s", err) + } + return h, handlerErr + } + + var apiErr *handlers.ApiError + isApiError := errors.As(handlerErr, &apiErr) + if isApiError { + pbErr := &pberrors.ApiError{ + ApiError: apiErr.Inner, + Status: apiErr.Status, + } + var buf []byte + var err error + if buf, err = proto.Marshal(pbErr); err != nil { + return h, status.Errorf(codes.Internal, "unable to marshal api error: %s", err) + } + if err := grpc.SetHeader(interceptorCtx, metadata.Pairs(apiErrHeader, base58.FastBase58Encoding(buf))); err != nil { + return h, status.Errorf(codes.Internal, "unable to set api error header: %s", err) + } + return h, handlerErr + } + } + return h, handlerErr + } +} + +func statusCodeInterceptor( + _ context.Context, +) grpc.UnaryServerInterceptor { + const op = "controller.statusCodeInterceptor" + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + + // call the handler... + h, handlerErr := handler(interceptorCtx, req) + + // if a service handler returns nil, nil then we want to single a 204 + // response to the proxy with no resp msg + if isNil(h) && handlerErr == nil { + if err := handlers.SetStatusCode(interceptorCtx, http.StatusNoContent); err != nil { + return &pb.EmptyResponse{}, err + } + return &pb.EmptyResponse{}, nil + } + + return h, handlerErr + } +} + +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/controller/interceptor_test.go b/internal/servers/controller/interceptor_test.go new file mode 100644 index 000000000..8553f2498 --- /dev/null +++ b/internal/servers/controller/interceptor_test.go @@ -0,0 +1,455 @@ +package controller + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + pb_api "github.com/hashicorp/boundary/internal/gen/controller/api" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" + "github.com/hashicorp/boundary/internal/gen/testing/interceptor" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/controller/auth" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" +) + +func Test_requestCtxInterceptor(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + serversRepoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, kmsCache) + } + + validGatewayTicket := "valid-ticket" + + o, _ := iam.TestScopes(t, iamRepo) + at := authtoken.TestAuthToken(t, conn, kmsCache, o.GetPublicId()) + encToken, err := authtoken.EncryptToken(context.Background(), kmsCache, o.GetPublicId(), at.GetPublicId(), at.GetToken()) + require.NoError(t, err) + tokValue := at.GetPublicId() + "_" + encToken + + newReqCtx := func(gwTicket string) context.Context { + req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/o_1", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokValue)) + // Add values for authn/authz checking + requestInfo := authpb.RequestInfo{ + Path: req.URL.Path, + Method: req.Method, + } + requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(context.TODO(), kmsCache, req) + requestInfo.Ticket = gwTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy + marshalledRequestInfo, err := proto.Marshal(&requestInfo) + require.NoError(t, err) + md := metadata.Pairs(requestInfoMdKey, base58.FastBase58Encoding(marshalledRequestInfo)) + mdCtx := metadata.NewIncomingContext(context.Background(), md) + + md, ok := metadata.FromIncomingContext(mdCtx) + require.True(t, ok) + require.NotNil(t, md) + + return mdCtx + } + + factoryCtx := context.Background() + + returnCtxHandler := func(ctx context.Context, req interface{}) (interface{}, error) { + return ctx, nil + } + + tests := []struct { + name string + requestCtx context.Context + iamRepoFn common.IamRepoFactory + authTokenRepoFn common.AuthTokenRepoFactory + serversRepoFn common.ServersRepoFactory + kms *kms.Kms + ticket string + wantFactoryErr bool + wantFactoryErrMatch *errors.Template + wantFactoryErrContains string + wantRequestErr bool + wantRequestErrMatch *errors.Template + wantRequestErrContains string + }{ + { + name: "missing-iam-repo", + requestCtx: newReqCtx(validGatewayTicket), + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing iam repo", + }, + { + name: "missing-at-repo", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing auth token repo", + }, + { + name: "missing-servers-repo", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + kms: kmsCache, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing servers repo function", + }, + { + name: "missing-kms", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing kms", + }, + { + name: "missing-factory-ticket", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing ticket", + }, + { + name: "missing-metadata", + requestCtx: context.Background(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "No metadata", + }, + { + name: "missing-request-info-metadata", + requestCtx: func() context.Context { + md := metadata.Pairs("greeter-md", "hello") + return metadata.NewIncomingContext(context.Background(), md) + }(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "Missing request metadata", + }, + { + name: "too-many-request-info-metadata", + requestCtx: func() context.Context { + md := metadata.Pairs(requestInfoMdKey, "first", requestInfoMdKey, "second") + return metadata.NewIncomingContext(context.Background(), md) + }(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "expected 1 value", + }, + { + name: "request-info-metadata-not-encoded", + requestCtx: func() context.Context { + md := metadata.Pairs(requestInfoMdKey, "hello") + return metadata.NewIncomingContext(context.Background(), md) + }(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "unable to decode request info", + }, + { + name: "request-info-metadata-not-proto", + requestCtx: func() context.Context { + md := metadata.Pairs(requestInfoMdKey, base58.FastBase58Encoding([]byte("hello"))) + return metadata.NewIncomingContext(context.Background(), md) + }(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "unable to unmarshal request info", + }, + { + name: "request-info-metadata-not-proto", + requestCtx: func() context.Context { + md := metadata.Pairs(requestInfoMdKey, base58.FastBase58Encoding([]byte("hello"))) + return metadata.NewIncomingContext(context.Background(), md) + }(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "unable to unmarshal request info", + }, + { + name: "missing-request-ticket", + requestCtx: newReqCtx(""), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: "validGatewayTicket", + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "Invalid context (missing ticket)", + }, + { + name: "bad-ticket", + requestCtx: newReqCtx("bad-ticket"), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: "validGatewayTicket", + wantRequestErr: true, + wantRequestErrMatch: errors.T(errors.Internal), + wantRequestErrContains: "Invalid context (bad ticket)", + }, + { + name: "valid", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + ticket: validGatewayTicket, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, tt.kms, tt.ticket) + if tt.wantFactoryErr { + require.Error(err) + assert.Nil(interceptor) + if tt.wantFactoryErrMatch != nil { + assert.Truef(errors.Match(tt.wantFactoryErrMatch, err), "want err code: %q got: %q", tt.wantFactoryErrMatch.Code, err) + } + if tt.wantFactoryErrContains != "" { + assert.Contains(err.Error(), tt.wantFactoryErrContains) + } + return + } + require.NoError(err) + assert.NotNil(interceptor) + + info := &grpc.UnaryServerInfo{ + FullMethod: "FakeMethod", + } + retCtx, err := interceptor(tt.requestCtx, nil, info, returnCtxHandler) + if tt.wantRequestErr { + require.Error(err) + assert.Nil(retCtx) + if tt.wantRequestErrMatch != nil { + assert.Truef(errors.Match(tt.wantRequestErrMatch, err), "want err code: %q got: %q", tt.wantRequestErrMatch.Code, err) + } + if tt.wantRequestErrContains != "" { + assert.Contains(err.Error(), tt.wantRequestErrContains) + } + return + } + require.NoError(err) + verifyResults := auth.Verify(retCtx.(context.Context)) + assert.NotEmpty(verifyResults) + }) + } +} + +func Test_errorInterceptor(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + factoryCtx context.Context + wantRequestErr bool + wantDomainErr bool + wantApiErr bool + }{ + { + name: "domain-error", + factoryCtx: ctx, + wantRequestErr: true, + wantDomainErr: true, + }, + { + name: "api-error", + factoryCtx: ctx, + wantRequestErr: true, + wantApiErr: true, + }, + { + name: "success", + factoryCtx: ctx, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.Falsef(tt.wantApiErr && tt.wantDomainErr, "tests can't require both wantApiErr and wantDomainErr") + errInterceptor := errorInterceptor(tt.factoryCtx) + client := startTestGreeterService(t, &testGreeter{}, errInterceptor) + var header metadata.MD + resp, err := client.SayHello( + context.Background(), + &interceptor.SayHelloRequest{Name: tt.name}, + grpc.Header(&header), + ) + if tt.wantRequestErr { + require.Error(err) + domainHdr := header.Get(domainErrHeader) + apiErrHdr := header.Get(apiErrHeader) + require.Error(err) + assert.Nil(resp) + if tt.wantDomainErr { + require.Len(domainHdr, 1) + assert.Len(apiErrHdr, 0) + decoded, err := base58.FastBase58Decoding(domainHdr[0]) + require.NoError(err) + var pbErr pberrors.Err + err = proto.Unmarshal(decoded, &pbErr) + require.NoError(err) + } + if tt.wantApiErr { + require.Len(apiErrHdr, 1) + assert.Len(domainHdr, 0) + decoded, err := base58.FastBase58Decoding(apiErrHdr[0]) + require.NoError(err) + var pbErr pberrors.ApiError + err = proto.Unmarshal(decoded, &pbErr) + require.NoError(err) + } + return + } + require.NoError(err) + }) + } + +} + +func Test_statusCodeInterceptor(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + wantStatusCode int + wantErr bool + }{ + { + name: "nil-nil", + wantStatusCode: http.StatusNoContent, + }, + { + name: "nil-err", + wantErr: true, + }, + { + name: "hello", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + statusInterceptor := statusCodeInterceptor(ctx) + client := startTestGreeterService(t, &testGreeter{}, statusInterceptor) + var header metadata.MD + _, err := client.SayHello( + context.Background(), + &interceptor.SayHelloRequest{Name: tt.name}, + grpc.Header(&header), + ) + + if tt.wantErr { + assert.Error(err) + } + statusHdr := header.Get(handlers.StatusCodeHeader) + if tt.wantStatusCode > 0 { + require.Len(statusHdr, 1) + code, err := strconv.Atoi(statusHdr[0]) + require.NoError(err) + assert.Equal(tt.wantStatusCode, code) + } else { + require.Len(statusHdr, 0) + } + }) + } + +} + +type testGreeter struct { + interceptor.UnimplementedGreeterServiceServer +} + +func (g *testGreeter) SayHello(ctx context.Context, req *interceptor.SayHelloRequest) (*interceptor.SayHelloResponse, error) { + const op = "SayHello" + switch req.GetName() { + case "api-error": + return &interceptor.SayHelloResponse{}, &handlers.ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb_api.Error{ + Kind: "internal", + Op: op, + Message: "api error msg", + }, + } + case "domain-error": + return &interceptor.SayHelloResponse{Message: "hello"}, errors.New(ctx, errors.Internal, op, "domain error msg") + case "nil-nil": + return nil, nil + case "nil-err": + return nil, errors.New(ctx, errors.Internal, op, "nil response error msg") + default: + return &interceptor.SayHelloResponse{Message: "hello"}, nil + } +} diff --git a/internal/servers/controller/listeners.go b/internal/servers/controller/listeners.go index 9a0d2feae..ad62e2822 100644 --- a/internal/servers/controller/listeners.go +++ b/internal/servers/controller/listeners.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/libs/alpnmux" @@ -20,10 +21,16 @@ import ( "google.golang.org/grpc" ) -func (c *Controller) startListeners() error { +func (c *Controller) startListeners(ctx context.Context) error { servers := make([]func(), 0, len(c.conf.Listeners)) configureForAPI := func(ln *base.ServerListener) error { + var err error + if c.gatewayServer, c.gatewayTicket, err = newGatewayServer(ctx, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms); err != nil { + return err + } + c.gatewayMux = newGatewayMux() + handler, err := c.handler(HandlerProperties{ ListenerConfig: ln.Config, CancelCtx: c.baseContext, @@ -134,6 +141,11 @@ func (c *Controller) startListeners() error { return nil } + c.gatewayListener, _ = newGatewayListener() + servers = append(servers, func() { + go c.gatewayServer.Serve(c.gatewayListener) + }) + for _, ln := range c.conf.Listeners { var err error for _, purpose := range ln.Config.Purpose { @@ -184,6 +196,21 @@ func (c *Controller) stopListeners(serversOnly bool) error { } }() } + + if c.gatewayServer != nil { + serverWg.Add(1) + go func() { + defer serverWg.Done() + shutdownKill, shutdownKillCancel := context.WithTimeout(c.baseContext, globals.DefaultMaxRequestDuration) + defer shutdownKillCancel() + go func() { + <-shutdownKill.Done() + c.gatewayServer.Stop() + }() + c.gatewayServer.GracefulStop() + }() + } + serverWg.Wait() if serversOnly { return nil diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 85c62ace7..1d48a6461 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -10,6 +10,7 @@ import ( "testing" "time" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/api/authtokens" @@ -18,6 +19,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/internal/gen/testing/interceptor" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/kms" @@ -27,6 +29,9 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" ) const ( @@ -716,3 +721,39 @@ func (tc *TestController) WaitForNextWorkerStatusUpdate(workerId string) error { event.WriteSysEvent(ctx, op, "waiting for next status report from worker received successfully", "worker", workerId) return nil } + +// startTestGreeterService is intended to facilitate the testing of +// interceptors. You provide a greeter service that produces appropriate +// responses to test your interceptor(s). This test function will start up a +// test greeter service wrapped with the specified interceptors and return an +// initialized client for the service. The test service will be stopped/cleaned +// up after the test (or subtests) have completed. +func startTestGreeterService(t *testing.T, greeter interceptor.GreeterServiceServer, interceptors ...grpc.UnaryServerInterceptor) interceptor.GreeterServiceClient { + t.Helper() + require := require.New(t) + dialCtx := context.Background() + + buffer := 1024 * 1024 + listener := bufconn.Listen(buffer) + + s := grpc.NewServer( + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)), + ) + interceptor.RegisterGreeterServiceServer(s, greeter) + go func() { + err := s.Serve(listener) + require.NoError(err) + }() + + conn, _ := grpc.DialContext(dialCtx, "", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return listener.Dial() + }), grpc.WithInsecure()) + + t.Cleanup(func() { + listener.Close() + s.Stop() + }) + + client := interceptor.NewGreeterServiceClient(conn) + return client +} diff --git a/internal/tests/cli/boundary/_helpers.bash b/internal/tests/cli/boundary/_helpers.bash index ec87630ae..3b65f5de1 100644 --- a/internal/tests/cli/boundary/_helpers.bash +++ b/internal/tests/cli/boundary/_helpers.bash @@ -22,3 +22,11 @@ function strip_all() { echo "$1" | tr -d '"' | tr -d '\'\' } +function has_status_code() { + local json=$1 + local code=$2 + if [ echo "$json"|jq -c ".status_code == $code" ]; then + return 1 + fi + +} \ No newline at end of file diff --git a/internal/tests/cli/boundary/groups.bats b/internal/tests/cli/boundary/groups.bats index 498b95da3..798dc1719 100644 --- a/internal/tests/cli/boundary/groups.bats +++ b/internal/tests/cli/boundary/groups.bats @@ -59,6 +59,7 @@ export NEW_GROUP='test' local gid=$(group_id $NEW_GROUP) run delete_group $gid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/host_catalogs.bats b/internal/tests/cli/boundary/host_catalogs.bats index 3e3630429..376b0afe4 100644 --- a/internal/tests/cli/boundary/host_catalogs.bats +++ b/internal/tests/cli/boundary/host_catalogs.bats @@ -44,6 +44,7 @@ export NEW_HOST_CATALOG='test' local hid=$(host_catalog_id $NEW_HOST_CATALOG $DEFAULT_P_ID) run delete_host_catalog $hid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/host_sets.bats b/internal/tests/cli/boundary/host_sets.bats index 4f1a17667..bd095bfce 100644 --- a/internal/tests/cli/boundary/host_sets.bats +++ b/internal/tests/cli/boundary/host_sets.bats @@ -58,6 +58,7 @@ export NEW_HOST_SET='test' local hsid=$(host_set_id $NEW_HOST_SET $DEFAULT_HOST_CATALOG) run delete_host_set $hsid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/hosts.bats b/internal/tests/cli/boundary/hosts.bats index f25e15880..91ae9ef3e 100644 --- a/internal/tests/cli/boundary/hosts.bats +++ b/internal/tests/cli/boundary/hosts.bats @@ -44,6 +44,7 @@ export NEW_HOST='test' local hid=$(host_id $NEW_HOST $DEFAULT_HOST_CATALOG) run delete_host $hid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/roles.bats b/internal/tests/cli/boundary/roles.bats index 6546f9eff..3555c0011 100644 --- a/internal/tests/cli/boundary/roles.bats +++ b/internal/tests/cli/boundary/roles.bats @@ -102,6 +102,7 @@ export NEW_GRANT='id=*;type=*;actions=create,read,update,delete,list' local rid=$(role_id $NEW_ROLE $DEFAULT_GLOBAL) run delete_role $rid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/scopes.bats b/internal/tests/cli/boundary/scopes.bats index af7ece028..6d27495a3 100644 --- a/internal/tests/cli/boundary/scopes.bats +++ b/internal/tests/cli/boundary/scopes.bats @@ -64,6 +64,7 @@ export NEW_PROJECT='test_project' local sid=$(scope_id $NEW_PROJECT $parent) run delete_scope $sid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } @@ -79,6 +80,7 @@ export NEW_PROJECT='test_project' local sid=$(scope_id $NEW_ORG $DEFAULT_GLOBAL) run delete_scope $sid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/target.bats b/internal/tests/cli/boundary/target.bats index f53f2d4be..812ce0d9b 100644 --- a/internal/tests/cli/boundary/target.bats +++ b/internal/tests/cli/boundary/target.bats @@ -92,6 +92,7 @@ load _helpers @test "boundary/target: default user can delete target" { local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run delete_target $id + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/user.bats b/internal/tests/cli/boundary/user.bats index 9451ed655..8fac3eb86 100644 --- a/internal/tests/cli/boundary/user.bats +++ b/internal/tests/cli/boundary/user.bats @@ -68,7 +68,9 @@ export NEW_USER='test' @test "boundary/user: can delete $NEW_USER user" { login $DEFAULT_LOGIN local uid=$(user_id $NEW_USER) - run delete_user $uid + run delete_user $uid + echo $output + run has_status_code "$output" "204" [ "$status" -eq 0 ] } @@ -82,4 +84,4 @@ export NEW_USER='test' local aid=$(account_id $NEW_USER) run delete_account $aid [ "$status" -eq 0 ] -} \ No newline at end of file +} From b5fbf9608ea74a8e82ab64ec99fbaed6a09f68ed Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 20 Oct 2021 08:35:33 -0400 Subject: [PATCH 10/17] refact(target): Remove old TargetType (#1618) Now that subtypes are registered via sub-packages, this TargetType is not needed and should not be used. This removes it and updates the target.Repository and targets.Service to use the updated code. This was missed in eac433f7ff929cb35babf716c0036c4dd713af1c. --- .../handlers/targets/target_service.go | 8 +- .../handlers/targets/target_service_test.go | 22 +- internal/target/options.go | 16 +- internal/target/options_test.go | 10 +- internal/target/repository.go | 6 +- internal/target/repository_test.go | 359 +----------------- internal/target/target.go | 16 - internal/target/tcp/repository_test.go | 2 +- 8 files changed, 49 insertions(+), 390 deletions(-) diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index b16da14ee..78c1261de 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -1474,7 +1474,7 @@ func toProto(ctx context.Context, in target.Target, hostSources []target.HostSou out.ScopeId = in.GetScopeId() } if outputFields.Has(globals.TypeField) { - out.Type = target.TcpTargetType.String() + out.Type = in.GetType().String() } if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = wrapperspb.String(in.GetDescription()) @@ -1626,11 +1626,9 @@ func validateCreateRequest(req *pbs.CreateTargetRequest) error { badFields["attributes.default_port"] = "This optional field cannot be set to 0." } } - switch req.GetItem().GetType() { - case target.TcpTargetType.String(): - case "": + if req.GetItem().GetType() == "" { badFields[globals.TypeField] = "This is a required field." - default: + } else if target.SubtypeFromType(req.GetItem().GetType()) == "" { badFields[globals.TypeField] = "Unknown type provided." } if filter := req.GetItem().GetWorkerFilter(); filter != nil { diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index 7dd69c1ed..8b1e20558 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -117,7 +117,7 @@ func TestGet(t *testing.T) { CreatedTime: tar.CreateTime.GetTimestamp(), UpdatedTime: tar.UpdateTime.GetTimestamp(), Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), HostSetIds: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, HostSourceIds: []string{hs[0].GetPublicId(), hs[1].GetPublicId()}, Attributes: new(structpb.Struct), @@ -212,7 +212,7 @@ func TestList(t *testing.T) { CreatedTime: tar.GetCreateTime().GetTimestamp(), UpdatedTime: tar.GetUpdateTime().GetTimestamp(), Version: tar.GetVersion(), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: new(structpb.Struct), SessionMaxSeconds: wrapperspb.UInt32(28800), SessionConnectionLimit: wrapperspb.Int32(1), @@ -228,7 +228,7 @@ func TestList(t *testing.T) { CreatedTime: tar.GetCreateTime().GetTimestamp(), UpdatedTime: tar.GetUpdateTime().GetTimestamp(), Version: tar.GetVersion(), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: new(structpb.Struct), SessionMaxSeconds: wrapperspb.UInt32(28800), SessionConnectionLimit: wrapperspb.Int32(1), @@ -421,7 +421,7 @@ func TestCreate(t *testing.T) { ScopeId: proj.GetPublicId(), Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -434,7 +434,7 @@ func TestCreate(t *testing.T) { Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -450,7 +450,7 @@ func TestCreate(t *testing.T) { req: &pbs.CreateTargetRequest{Item: &pb.Target{ Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(0), }}, @@ -623,7 +623,7 @@ func TestUpdate(t *testing.T) { Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -658,7 +658,7 @@ func TestUpdate(t *testing.T) { Name: wrapperspb.String("name"), Description: wrapperspb.String("desc"), CreatedTime: tar.GetCreateTime().GetTimestamp(), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -745,7 +745,7 @@ func TestUpdate(t *testing.T) { Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, Name: wrapperspb.String("default"), CreatedTime: tar.GetCreateTime().GetTimestamp(), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -778,7 +778,7 @@ func TestUpdate(t *testing.T) { Name: wrapperspb.String("updated"), Description: wrapperspb.String("default"), CreatedTime: tar.GetCreateTime().GetTimestamp(), - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, @@ -814,7 +814,7 @@ func TestUpdate(t *testing.T) { Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "default_port": structpb.NewNumberValue(2), }}, - Type: target.TcpTargetType.String(), + Type: tcp.Subtype.String(), HostSetIds: hsIds, HostSets: hostSets, HostSourceIds: hostSourceIds, diff --git a/internal/target/options.go b/internal/target/options.go index f87c4763f..5fa1bbe08 100644 --- a/internal/target/options.go +++ b/internal/target/options.go @@ -1,6 +1,10 @@ package target -import "time" +import ( + "time" + + "github.com/hashicorp/boundary/internal/types/subtypes" +) // GetOpts - iterate the inbound Options and return a struct func GetOpts(opt ...Option) options { @@ -24,7 +28,7 @@ type options struct { WithScopeIds []string WithScopeName string WithUserId string - WithTargetType *TargetType + WithType subtypes.Subtype WithHostSources []string WithCredentialSources []string WithSessionMaxSeconds uint32 @@ -43,7 +47,7 @@ func getDefaultOptions() options { WithScopeIds: nil, WithScopeName: "", WithUserId: "", - WithTargetType: nil, + WithType: "", WithHostSources: nil, WithCredentialSources: nil, WithSessionMaxSeconds: uint32((8 * time.Hour).Seconds()), @@ -111,10 +115,10 @@ func WithUserId(userId string) Option { } } -// WithTargetType provides an option to search by a target type -func WithTargetType(t TargetType) Option { +// WithType provides an option to search by a target type +func WithType(t subtypes.Subtype) Option { return func(o *options) { - o.WithTargetType = &t + o.WithType = t } } diff --git a/internal/target/options_test.go b/internal/target/options_test.go index 4c3f73716..a4d3431f5 100644 --- a/internal/target/options_test.go +++ b/internal/target/options_test.go @@ -3,6 +3,7 @@ package target import ( "testing" + "github.com/hashicorp/boundary/internal/types/subtypes" "github.com/stretchr/testify/assert" ) @@ -82,12 +83,13 @@ func Test_GetOpts(t *testing.T) { testOpts.WithPublicId = "testId" assert.Equal(opts, testOpts) }) - t.Run("WithTargetType", func(t *testing.T) { + t.Run("WithType", func(t *testing.T) { + subtype := subtypes.Subtype("testtype") assert := assert.New(t) - opts := GetOpts(WithTargetType(TcpTargetType)) + opts := GetOpts(WithType(subtype)) testOpts := getDefaultOptions() - target := TcpTargetType - testOpts.WithTargetType = &target + target := subtype + testOpts.WithType = target assert.Equal(opts, testOpts) }) t.Run("WithHostSources", func(t *testing.T) { diff --git a/internal/target/repository.go b/internal/target/repository.go index 96f53a0d4..04843199c 100644 --- a/internal/target/repository.go +++ b/internal/target/repository.go @@ -138,7 +138,7 @@ func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, op return subtype, hostSources, credSources, nil } -// ListTargets in targets in a scope. Supports the WithScopeId, WithLimit, WithTargetType options. +// ListTargets in targets in a scope. Supports the WithScopeId, WithLimit, WithType options. func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, error) { const op = "target.(Repository).ListTargets" opts := GetOpts(opt...) @@ -151,8 +151,8 @@ func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, if len(opts.WithScopeIds) != 0 { where, args = append(where, "scope_id in (?)"), append(args, opts.WithScopeIds) } - if opts.WithTargetType != nil { - where, args = append(where, "type = ?"), append(args, opts.WithTargetType.String()) + if opts.WithType != "" { + where, args = append(where, "type = ?"), append(args, opts.WithType.String()) } var foundTargets []*targetView diff --git a/internal/target/repository_test.go b/internal/target/repository_test.go index 223bdf72f..6187421a3 100644 --- a/internal/target/repository_test.go +++ b/internal/target/repository_test.go @@ -1,11 +1,10 @@ -package target_test +package target import ( "testing" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/kms" - "github.com/hashicorp/boundary/internal/target" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,9 +21,9 @@ func TestNewRepository(t *testing.T) { kms *kms.Kms } tests := []struct { - name string - args args - // want *target.Repository + name string + args args + want *Repository wantErr bool wantErrString string }{ @@ -35,12 +34,12 @@ func TestNewRepository(t *testing.T) { w: rw, kms: testKms, }, - // want: &target.Repository{ - // reader: rw, - // writer: rw, - // kms: testKms, - // defaultLimit: db.DefaultLimit, - // }, + want: &Repository{ + reader: rw, + writer: rw, + kms: testKms, + defaultLimit: db.DefaultLimit, + }, wantErr: false, }, { @@ -50,7 +49,7 @@ func TestNewRepository(t *testing.T) { w: rw, kms: nil, }, - // want: nil, + want: nil, wantErr: true, wantErrString: "target.NewRepository: nil kms: parameter violation: error #100", }, @@ -61,7 +60,7 @@ func TestNewRepository(t *testing.T) { w: nil, kms: testKms, }, - // want: nil, + want: nil, wantErr: true, wantErrString: "target.NewRepository: nil writer: parameter violation: error #100", }, @@ -72,7 +71,7 @@ func TestNewRepository(t *testing.T) { w: rw, kms: testKms, }, - // want: nil, + want: nil, wantErr: true, wantErrString: "target.NewRepository: nil reader: parameter violation: error #100", }, @@ -80,7 +79,7 @@ func TestNewRepository(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := target.NewRepository(tt.args.r, tt.args.w, tt.args.kms) + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms) if tt.wantErr { require.Error(err) assert.Equal(tt.wantErrString, err.Error()) @@ -88,335 +87,7 @@ func TestNewRepository(t *testing.T) { } require.NoError(err) assert.NotNil(got) - // assert.Equal(tt.want, got) + assert.Equal(tt.want, got) }) } } - -// func TestRepository_LookupTarget(t *testing.T) { -// t.Parallel() -// conn, _ := db.TestSetup(t, "postgres") -// wrapper := db.TestWrapper(t) -// testKms := kms.TestKms(t, conn, wrapper) -// iamRepo := iam.TestRepo(t, conn, wrapper) -// _, proj := iam.TestScopes(t, iamRepo) -// proj.Name = "project-name" -// _, _, err := iamRepo.UpdateScope(context.Background(), proj, 1, []string{"name"}) -// require.NoError(t, err) -// rw := db.New(conn) -// repo, err := target.NewRepository(rw, rw, testKms) -// require.NoError(t, err) -// tgt := target.TestTcpTarget(t, conn, proj.PublicId, "target-name") -// -// tests := []struct { -// testName string -// id string -// name string -// scopeId string -// scopeName string -// wantErr bool -// }{ -// { -// testName: "id", -// id: tgt.PublicId, -// wantErr: false, -// }, -// { -// testName: "name only", -// name: tgt.Name, -// wantErr: true, -// }, -// { -// testName: "scope id only", -// scopeId: proj.PublicId, -// wantErr: true, -// }, -// { -// testName: "scope name only", -// scopeName: proj.Name, -// wantErr: true, -// }, -// { -// testName: "scope name and id", -// scopeId: proj.PublicId, -// scopeName: proj.Name, -// wantErr: true, -// }, -// { -// testName: "everything", -// name: tgt.Name, -// scopeId: proj.PublicId, -// scopeName: proj.Name, -// wantErr: true, -// }, -// { -// testName: "name and scope name", -// name: tgt.Name, -// scopeName: proj.Name, -// wantErr: false, -// }, -// { -// testName: "name and scope id", -// name: tgt.Name, -// scopeId: proj.PublicId, -// wantErr: false, -// }, -// { -// testName: "id and name", -// id: tgt.PublicId, -// name: tgt.Name, -// scopeId: proj.PublicId, -// wantErr: true, -// }, -// { -// testName: "id and scope name", -// id: tgt.PublicId, -// scopeName: proj.Name, -// wantErr: true, -// }, -// { -// testName: "id and scope id", -// id: tgt.PublicId, -// scopeId: proj.PublicId, -// wantErr: true, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.testName, func(t *testing.T) { -// assert, require := assert.New(t), require.New(t) -// id := tt.id -// if tt.name != "" && tt.id == "" { -// id = tt.name -// } -// var opts []target.Option -// if tt.name != "" { -// opts = append(opts, target.WithName(tt.name)) -// } -// if tt.scopeId != "" { -// opts = append(opts, target.WithScopeId(tt.scopeId)) -// } -// if tt.scopeName != "" { -// opts = append(opts, target.WithScopeName(tt.scopeName)) -// } -// got, _, _, err := repo.LookupTarget(context.Background(), id, opts...) -// if tt.wantErr { -// require.Error(err) -// return -// } -// require.NoError(err) -// assert.Equal(tgt.PublicId, got.GetPublicId()) -// }) -// } -// } -// -// func TestRepository_ListTargets(t *testing.T) { -// t.Parallel() -// conn, _ := db.TestSetup(t, "postgres") -// const testLimit = 10 -// wrapper := db.TestWrapper(t) -// testKms := kms.TestKms(t, conn, wrapper) -// iamRepo := iam.TestRepo(t, conn, wrapper) -// _, proj := iam.TestScopes(t, iamRepo) -// rw := db.New(conn) -// repo, err := target.NewRepository(rw, rw, testKms, target.WithLimit(testLimit)) -// require.NoError(t, err) -// -// type args struct { -// opt []target.Option -// } -// tests := []struct { -// name string -// createCnt int -// createScopeId string -// createScopeId2 string -// grantUserId string -// args args -// wantCnt int -// wantErr bool -// }{ -// { -// name: "tcp-target", -// createCnt: 5, -// createScopeId: proj.PublicId, -// args: args{ -// opt: []target.Option{target.WithTargetType(target.TcpTargetType), target.WithScopeIds([]string{proj.PublicId})}, -// }, -// wantCnt: 5, -// wantErr: false, -// }, -// { -// name: "no-limit", -// createCnt: testLimit + 1, -// createScopeId: proj.PublicId, -// args: args{ -// opt: []target.Option{target.WithLimit(-1), target.WithScopeIds([]string{proj.PublicId})}, -// }, -// wantCnt: testLimit + 1, -// wantErr: false, -// }, -// { -// name: "default-limit", -// createCnt: testLimit + 1, -// createScopeId: proj.PublicId, -// args: args{ -// opt: []target.Option{target.WithScopeIds([]string{proj.PublicId})}, -// }, -// wantCnt: testLimit, -// wantErr: false, -// }, -// { -// name: "custom-limit", -// createCnt: testLimit + 1, -// createScopeId: proj.PublicId, -// args: args{ -// opt: []target.Option{target.WithLimit(3), target.WithScopeIds([]string{proj.PublicId})}, -// }, -// wantCnt: 3, -// wantErr: false, -// }, -// { -// name: "bad-org", -// createCnt: 1, -// createScopeId: proj.PublicId, -// args: args{ -// opt: []target.Option{target.WithScopeIds([]string{"bad-id"})}, -// }, -// wantCnt: 0, -// wantErr: false, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// assert, require := assert.New(t), require.New(t) -// require.NoError(conn.Where("1=1").Delete(target.NewTestTcpTarget("")).Error) -// testGroups := []*target.TcpTarget{} -// for i := 0; i < tt.createCnt; i++ { -// switch { -// case tt.createScopeId2 != "" && i%2 == 0: -// testGroups = append(testGroups, target.TestTcpTarget(t, conn, tt.createScopeId2, strconv.Itoa(i))) -// default: -// testGroups = append(testGroups, target.TestTcpTarget(t, conn, tt.createScopeId, strconv.Itoa(i))) -// } -// } -// assert.Equal(tt.createCnt, len(testGroups)) -// got, err := repo.ListTargets(context.Background(), tt.args.opt...) -// if tt.wantErr { -// require.Error(err) -// return -// } -// require.NoError(err) -// assert.Equal(tt.wantCnt, len(got)) -// }) -// } -// } -// -// func TestRepository_ListRoles_Multiple_Scopes(t *testing.T) { -// t.Parallel() -// conn, _ := db.TestSetup(t, "postgres") -// wrapper := db.TestWrapper(t) -// testKms := kms.TestKms(t, conn, wrapper) -// iamRepo := iam.TestRepo(t, conn, wrapper) -// _, proj1 := iam.TestScopes(t, iamRepo) -// _, proj2 := iam.TestScopes(t, iamRepo) -// rw := db.New(conn) -// repo, err := target.NewRepository(rw, rw, testKms) -// require.NoError(t, err) -// -// require.NoError(t, conn.Where("1=1").Delete(target.NewTestTcpTarget("")).Error) -// -// const numPerScope = 10 -// var total int -// for i := 0; i < numPerScope; i++ { -// target.TestTcpTarget(t, conn, proj1.GetPublicId(), fmt.Sprintf("proj1-%d", i)) -// total++ -// target.TestTcpTarget(t, conn, proj2.GetPublicId(), fmt.Sprintf("proj2-%d", i)) -// total++ -// } -// -// got, err := repo.ListTargets(context.Background(), target.WithScopeIds([]string{"global", proj1.GetPublicId(), proj2.GetPublicId()})) -// require.NoError(t, err) -// assert.Equal(t, total, len(got)) -// } -// -// func TestRepository_DeleteTarget(t *testing.T) { -// t.Parallel() -// conn, _ := db.TestSetup(t, "postgres") -// rw := db.New(conn) -// wrapper := db.TestWrapper(t) -// testKms := kms.TestKms(t, conn, wrapper) -// iamRepo := iam.TestRepo(t, conn, wrapper) -// _, proj := iam.TestScopes(t, iamRepo) -// repo, err := target.NewRepository(rw, rw, testKms) -// require.NoError(t, err) -// -// type args struct { -// target target.Target -// opt []target.Option -// } -// tests := []struct { -// name string -// args args -// wantRowsDeleted int -// wantErr bool -// wantErrMsg string -// }{ -// { -// name: "valid", -// args: args{ -// target: target.TestTcpTarget(t, conn, proj.PublicId, "valid"), -// }, -// wantRowsDeleted: 1, -// wantErr: false, -// }, -// { -// name: "no-public-id", -// args: args{ -// target: func() target.Target { -// tar, _ := target.NewTcpTarget(proj.PublicId) -// return tar -// }(), -// }, -// wantRowsDeleted: 0, -// wantErr: true, -// wantErrMsg: "target.(Repository).DeleteTarget: missing public id: parameter violation: error #100", -// }, -// { -// name: "not-found", -// args: args{ -// target: func() target.Target { -// id, err := target.NewTcpTargetId() -// require.NoError(t, err) -// tar, _ := target.NewTcpTarget(proj.PublicId) -// tar.PublicId = id -// return tar -// }(), -// }, -// wantRowsDeleted: 0, -// wantErr: true, -// wantErrMsg: "db.LookupById: record not found, search issue: error #1100", -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// assert := assert.New(t) -// deletedRows, err := repo.DeleteTarget(context.Background(), tt.args.target.GetPublicId(), tt.args.opt...) -// if tt.wantErr { -// assert.Error(err) -// assert.Equal(0, deletedRows) -// assert.Contains(err.Error(), tt.wantErrMsg) -// err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) -// assert.Error(err) -// assert.True(errors.IsNotFoundError(err)) -// return -// } -// assert.NoError(err) -// assert.Equal(tt.wantRowsDeleted, deletedRows) -// foundGroup, _, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) -// assert.NoError(err) -// assert.Nil(foundGroup) -// -// err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) -// assert.NoError(err) -// }) -// } -// } diff --git a/internal/target/target.go b/internal/target/target.go index fc459a389..6916c43e0 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -40,22 +40,6 @@ type Target interface { Oplog(op oplog.OpType) oplog.Metadata } -// TargetType defines the possible types for targets. -type TargetType uint32 - -const ( - UnknownTargetType TargetType = 0 - TcpTargetType TargetType = 1 -) - -// String returns a string representation of the target type. -func (t TargetType) String() string { - return [...]string{ - "unknown", - "tcp", - }[t] -} - const ( targetsViewDefaultTable = "target_all_subtypes" ) diff --git a/internal/target/tcp/repository_test.go b/internal/target/tcp/repository_test.go index ffc76fca6..32501047b 100644 --- a/internal/target/tcp/repository_test.go +++ b/internal/target/tcp/repository_test.go @@ -164,7 +164,7 @@ func TestRepository_ListTargets(t *testing.T) { createCnt: 5, createScopeId: proj.PublicId, args: args{ - opt: []target.Option{target.WithTargetType(target.TcpTargetType), target.WithScopeIds([]string{proj.PublicId})}, + opt: []target.Option{target.WithType(tcp.Subtype), target.WithScopeIds([]string{proj.PublicId})}, }, wantCnt: 5, wantErr: false, From 3399ca30b302794821399dd974b6bc597c984c69 Mon Sep 17 00:00:00 2001 From: Zachary Shilton <4624598+zchsh@users.noreply.github.com> Date: Wed, 20 Oct 2021 11:29:54 -0400 Subject: [PATCH 11/17] website: use shared open-api-page component (#1519) * website: use canary release of open-api-page * website: bump to latest open-api-page pre-release * chore: bump to latest open-api-page pre-release * chore: refresh package files from main to avoid conflicts * bump to stable open-api-page release --- website/components/openapi-page/index.jsx | 94 ----------- .../components/openapi-page/index.props.js | 32 ---- .../partials/chevron-down/black.jsx | 23 --- .../collapsible/collapsible.module.css | 18 -- .../partials/collapsible/index.js | 81 --------- .../partials/operation-object/index.js | 154 ------------------ .../partials/operation-object/index.props.js | 61 ------- .../operation-object.module.css | 145 ----------------- .../partials/property-object/index.js | 91 ----------- .../property-object.module.css | 85 ---------- .../partials/response-object/index.js | 27 --- .../partials/response-object/index.props.js | 16 -- .../components/openapi-page/style.module.css | 57 ------- .../openapi-page/utils/routing-utils.js | 121 -------------- website/package-lock.json | 129 +++++++++++---- website/package.json | 1 + website/pages/api-docs/[[...page]].jsx | 16 +- 17 files changed, 108 insertions(+), 1043 deletions(-) delete mode 100644 website/components/openapi-page/index.jsx delete mode 100644 website/components/openapi-page/index.props.js delete mode 100644 website/components/openapi-page/partials/chevron-down/black.jsx delete mode 100644 website/components/openapi-page/partials/collapsible/collapsible.module.css delete mode 100644 website/components/openapi-page/partials/collapsible/index.js delete mode 100644 website/components/openapi-page/partials/operation-object/index.js delete mode 100644 website/components/openapi-page/partials/operation-object/index.props.js delete mode 100644 website/components/openapi-page/partials/operation-object/operation-object.module.css delete mode 100644 website/components/openapi-page/partials/property-object/index.js delete mode 100644 website/components/openapi-page/partials/property-object/property-object.module.css delete mode 100644 website/components/openapi-page/partials/response-object/index.js delete mode 100644 website/components/openapi-page/partials/response-object/index.props.js delete mode 100644 website/components/openapi-page/style.module.css delete mode 100644 website/components/openapi-page/utils/routing-utils.js diff --git a/website/components/openapi-page/index.jsx b/website/components/openapi-page/index.jsx deleted file mode 100644 index a3853708b..000000000 --- a/website/components/openapi-page/index.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState, useRef } from 'react' -import Link from 'next/link' -import OperationObject from './partials/operation-object' -import HashiHead from '@hashicorp/react-head' -import DocsSidenav from '@hashicorp/react-docs-sidenav' -import Content from '@hashicorp/react-content' -import { getPathsFromSchema, getPropsForPage } from './utils/routing-utils' -import styles from './style.module.css' -import useOnClickOutside from 'lib/hooks/use-on-click-outside' - -function OpenApiPage({ - info, - operationCategory, - navData, - productName, - productSlug, - currentPath, -}) { - const operationsRef = useRef(null) - const [expandedOperations, setExpandedOperations] = useState([]) - useOnClickOutside(operationsRef, () => setExpandedOperations([])) - - function setOperationState(slug, isExpanded) { - const newStates = expandedOperations.filter((s) => s !== slug) - if (isExpanded) newStates.push(slug) - setExpandedOperations(newStates) - } - - const pageTitle = operationCategory ? operationCategory.name : info.title - - return ( -

- - - -

- {info.title} -

-

- {operationCategory.name} -

-
- {operationCategory.operations.map((op) => { - const isExpanded = - expandedOperations.indexOf(op.operationId) !== -1 - - return ( - - setOperationState(op.operationId, !isCollapsed) - } - /> - ) - })} -
- - ) : ( - <> -

- {info.title} -

-

- Select a service from the sidebar. -

- - ) - } - /> -
- ) -} - -export { getPathsFromSchema, getPropsForPage } -export default OpenApiPage diff --git a/website/components/openapi-page/index.props.js b/website/components/openapi-page/index.props.js deleted file mode 100644 index 92c651d83..000000000 --- a/website/components/openapi-page/index.props.js +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types' -import OpenAPIPage from '.' - -function OpenAPIPageProps(props) { - return -} - -OpenAPIPageProps.propTypes = { - /** Information about the API, pulled from the schema's [Info Object](https://swagger.io/specification/v2/#info-object) */ - info: PropTypes.shape({ - title: PropTypes.string.isRequired, - description: PropTypes.string, - version: PropTypes.string, - }), - /** The name of the associated HashiCopr product, used in page metadata */ - productName: PropTypes.string, - /** The slug of the associated HashiCorp product, used for theming. Only supports "boundary" or a default theme at present.*/ - productSlug: PropTypes.oneOf(['boundary']), - /** An object that determines which operations will be shown on the page */ - operationCategory: PropTypes.shape({ - /** The name of the operation "category", used as the page title */ - name: PropTypes.string, - /** The slug-ified name, used as the page's location */ - slug: PropTypes.string, - /** An array of [Operation Objects](https://swagger.io/specification/v2/#operation-object) to be displayed on the page */ - operations: PropTypes.arrayOf(PropTypes.object), - }), - /** ["order"](https://github.com/hashicorp/react-components/blob/11d7bda7d518bc675ab0033067d43057e21f3b77/packages/docs-sidenav/index.js#L9) data generated to be passed directly to our DocsSidenav component */ - sidenavOrderData: PropTypes.object, -} - -export default OpenAPIPageProps diff --git a/website/components/openapi-page/partials/chevron-down/black.jsx b/website/components/openapi-page/partials/chevron-down/black.jsx deleted file mode 100644 index 2c7c0fa0c..000000000 --- a/website/components/openapi-page/partials/chevron-down/black.jsx +++ /dev/null @@ -1,23 +0,0 @@ -function ChevronDownBlack({ title, ...props }) { - return ( - - {title ? {title} : null} - - - ) -} - -export default ChevronDownBlack diff --git a/website/components/openapi-page/partials/collapsible/collapsible.module.css b/website/components/openapi-page/partials/collapsible/collapsible.module.css deleted file mode 100644 index a2ad211a2..000000000 --- a/website/components/openapi-page/partials/collapsible/collapsible.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.outer { - overflow: hidden; - height: 0; - &[data-useanimation='true'] { - transition: height 0.4s; - } -} - -.inner { - opacity: 1; - - &[data-iscollapsed='true'] { - opacity: 0; - } - &[data-useanimation='true'] { - transition: opacity 0.4s; - } -} diff --git a/website/components/openapi-page/partials/collapsible/index.js b/website/components/openapi-page/partials/collapsible/index.js deleted file mode 100644 index 673d8111d..000000000 --- a/website/components/openapi-page/partials/collapsible/index.js +++ /dev/null @@ -1,81 +0,0 @@ -import { Component } from 'react' -import styles from './collapsible.module.css' - -/* - Collapsible content block, receives children and a 'isCollapsed' prop, - smoothly animates from collapsed (height 0) to expanded (height auto) -*/ -export default class Collapsible extends Component { - constructor(props) { - super(props) - this.animateHeight = this.animateHeight.bind(this) - } - - componentDidMount() { - this.animateHeight() - } - - componentDidUpdate(prevProps) { - // Only update if isCollapsed prop has changed - if (this.props.isCollapsed === prevProps.isCollapsed) return - this.animateHeight() - } - - animateHeight() { - function handleTransitionEnd(event) { - const elem = event.target - const elemStyle = getComputedStyle(elem) - const isCollapsed = elem.getAttribute('data-iscollapsed') - if (event.propertyName !== 'height') return - const innerElem = event.target.firstChild - const innerElemStyle = getComputedStyle(innerElem) - if (isCollapsed === 'false') { - elem.style.height = 'auto' - innerElem.style.transitionDelay = '0s' - } else if (isCollapsed === 'true') { - const heightDurn = parseFloat(elemStyle['transition-duration']) - const opactiyDurn = parseFloat(innerElemStyle['transition-duration']) - innerElem.style.transitionDelay = `${heightDurn - opactiyDurn}s` - } - elem.removeEventListener('transitionend', handleTransitionEnd, false) - } - const { isCollapsed } = this.props - // Do not attempt update if container elem is invalid - const elem = this.containerRef - if (!elem) return - const elemStyle = getComputedStyle(elem) - if (isCollapsed) { - // Transition from auto to 0 - elem.style.height = elemStyle.height - elem.offsetHeight // force repaint - elem.style.height = '0px' - elem.setAttribute('data-iscollapsed', 'true') - elem.addEventListener('transitionend', handleTransitionEnd, false) - } else { - // Transition from 0 to auto - const prevHeight = elem.style.height - elem.style.height = 'auto' - const endHeight = elemStyle.height - elem.style.height = prevHeight - elem.offsetHeight // force repaint - elem.style.height = endHeight - elem.setAttribute('data-iscollapsed', 'false') - elem.addEventListener('transitionend', handleTransitionEnd, false) - } - } - - render() { - const { children, noAnimation } = this.props - return ( -
(this.containerRef = ref)} - data-useanimation={Boolean(!noAnimation)} - > -
- {children} -
-
- ) - } -} diff --git a/website/components/openapi-page/partials/operation-object/index.js b/website/components/openapi-page/partials/operation-object/index.js deleted file mode 100644 index 2c6a87373..000000000 --- a/website/components/openapi-page/partials/operation-object/index.js +++ /dev/null @@ -1,154 +0,0 @@ -import Collapsible from '../collapsible' -import ResponseObject from '../response-object' -import PropertyObject from '../property-object' -import { capitalCase } from 'change-case' -import useHover from '../../../../lib/hooks/use-hover' -import SvgrChevronDown from '../chevron-down/black' - -import styles from './operation-object.module.css' - -function OperationObject({ - data, - path, - type, - isHighlighted, - isCollapsed, - setIsCollapsed, -}) { - const [headerRef, isHeaderHovered] = useHover() - - // const [isCollapsed, setIsCollapsed] = useState(true) - const { operationId, parameters, responses, summary } = data - const successResponse = responses['200'] - const title = capitalCase(operationId.split('_').slice(1).join()) - - // Group parameter properties by type - const pathParams = parameters.filter((p) => p.in === 'path') - const queryParams = parameters.filter((p) => p.in === 'query') - const bodyParam = parameters.filter((p) => p.in === 'body')[0] // Note: we only accept a single "in=body" param - const bodyProps = bodyParam ? getBodyParamProps(bodyParam) : [] - - return ( -
-
setIsCollapsed(!isCollapsed)} - > -
-
- {title} -
-
- - {type.toUpperCase()}{' '} - - {path} -
-
-
- - {isCollapsed ? 'Expand' : 'Collapse'} - - - - -
-
- -
-

{summary}

- -

Request

- {pathParams.length > 0 ? ( - - ) : null} - {queryParams.length > 0 ? ( - - ) : null} - {bodyProps.length > 0 ? ( - - ) : null} -
- } - columnTwo={ -
-

Response

- {!!successResponse ? ( -
-

- Successful Response -

- -
- ) : ( -

No response has been defined.

- )} -
- } - /> -
- - - ) -} - -function getBodyParamProps(bodyParam) { - // We always expect the bodyParam to be an object, - // with a schema which defines the body properties. - if (!bodyParam.schema || !bodyParam.schema.properties) return [] - // We flatten these properties to avoid showing a - // "collapsed object" UI under the "Body Parameters" section, - // which would be a bit redundant and annoying to have to expand - const bodyPropsObj = bodyParam.schema.properties - const bodyProps = Object.keys(bodyPropsObj).reduce((acc, key) => { - const data = Object.assign({}, bodyPropsObj[key]) - // We need the property name. This is usually be handled by "key" in an object, - // but we're flattening the object so we need to make sure it's there - data.name = key - if (!data.readOnly) acc.push(data) - return acc - }, []) - return bodyProps -} - -function TwoColumnLayout({ columnOne, columnTwo }) { - return ( -
-
{columnOne}
-
-
{columnTwo}
-
- ) -} - -function Parameters({ title, params }) { - return ( -
-

- {title} -

- {params.map((parameter, idx) => { - return ( - - ) - })} -
- ) -} - -export default OperationObject diff --git a/website/components/openapi-page/partials/operation-object/index.props.js b/website/components/openapi-page/partials/operation-object/index.props.js deleted file mode 100644 index a414821d5..000000000 --- a/website/components/openapi-page/partials/operation-object/index.props.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import OperationObject from '.' - -function OperationObjectProps(props) { - return -} - -/** Displays [Operation Object](https://swagger.io/specification/v2/#operation-object) data to the user. */ -OperationObjectProps.propTypes = { - /** The path to the endpoint where this operation is executed */ - path: PropTypes.string.isRequired, - /** The type of operation */ - type: PropTypes.oneOf([ - 'get', - 'put', - 'post', - 'delete', - 'options', - 'head', - 'patch', - ]).isRequired, - /** A subset of [Operation Object](https://swagger.io/specification/v2/#operation-object) relevant to the UI */ - data: PropTypes.shape({ - /** Flag whether this operation is deprecated */ - deprecated: PropTypes.bool, - /** A unique string used to identify the operation.*/ - operationId: PropTypes.string, - /** Array of parameter objects. We group them by their "in" property, and pass them to the component for rendering. */ - parameters: PropTypes.arrayOf( - PropTypes.shape({ - /** The name of the parameter. Parameter names are case sensitive. */ - name: PropTypes.string.isRequired, - /** A brief, plain text description of the parameter. Note that [GitHub-flavor markdown](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) is not currently supported. */ - description: PropTypes.string, - /** Indicates whether this parameter is mandatory. */ - required: PropTypes.boolean, - /** The location of the parameter. Note: `formData` and `header` are not currently supported in the UI. */ - in: PropTypes.oneOf(['query', 'path', 'body']).isRequired, - /** Schema is used for "body" parameters. */ - schema: PropTypes.object, - /** Type defines the parameter type, for non-"body" parameters. */ - type: PropTypes.oneOf([ - 'array', - 'boolean', - 'integer', - 'number', - 'object', - 'string', - ]), - }) - ), - /** Responses object, keys are HTTP response codes */ - responses: PropTypes.shape({ - /** We only support "200" response codes, for now. This object is passed to the component. */ - 200: PropTypes.object, - }), - }), -} - -export default OperationObjectProps diff --git a/website/components/openapi-page/partials/operation-object/operation-object.module.css b/website/components/openapi-page/partials/operation-object/operation-object.module.css deleted file mode 100644 index 93bea1984..000000000 --- a/website/components/openapi-page/partials/operation-object/operation-object.module.css +++ /dev/null @@ -1,145 +0,0 @@ -.root { - border-bottom: 1px solid var(--gray-5); - margin: 0; - margin-top: -1px; - position: relative; - transition: border-color 0.2s; - - &[data-is-hovered='true'] { - border-bottom-color: var(--brand); - z-index: 1; - } -} - -.header { - display: flex; - position: relative; - padding: 16px 0; - - &:hover { - cursor: pointer; - } -} - -.meta { - width: 100%; -} - -.title { - position: relative; - display: inline-block; - composes: g-type-body-x-strong from global; - color: var(--gray-2); - margin: 0 0 4px 0; - padding: 0; - - &[data-is-highlighted='true'] { - &::before { - content: '\200B'; - position: absolute; - top: -4px; - left: -8px; - right: -8px; - bottom: -4px; - background: var(--warning-l1); - z-index: -1; - border-radius: 8px; - } - } - - &[data-is-hovered='true'] { - color: var(--brand); - } -} - -.endpoint { - composes: g-type-code from global; - - & .method { - color: var(--gray-3); - - &[data-is-hovered='true'] { - color: var(--brand); - } - } - - & .path { - color: var(--gray-2); - } -} - -.toggleButton { - border: none; - color: var(--brand); - background: none; - display: flex; - align-items: center; - font-size: inherit; - margin: 0; - padding: 0 0 0 24px; - transition: color 0.2s; - - & .toggleIcon { - width: 16px; - height: 16px; - margin-left: 8px; - position: relative; - top: 1px; - transition: transform 0.3s; - display: flex; - align-items: center; - justify-content: center; - - & svg { - & [fill] { - fill: var(--brand); - } - & [stroke] { - stroke: var(--brand); - } - } - - &[data-is-collapsed='false'] { - transform: rotate(-180deg); - } - } - - &[data-is-hovered='true'] { - color: var(--brand); - } -} - -.details { - padding-bottom: 80px; - - & .summary { - color: var(--gray-2); - margin: 8px 0 24px 0; - } - - & .columnHeading { - margin: 0 0 1em 0; - font-weight: bold; - } - - & .columnSectionHeading { - margin: 0 !important; /* Needed to override ".g-content p" margins */ - padding-bottom: 8px; - border-bottom: 1px solid var(--gray-5); - color: var(--gray-3); - } -} - -.twoColumnLayout { - display: flex; - - & > div:nth-child(1), - & > div:nth-child(3) { - position: relative; - width: calc(50% - 16px); - } - - & > div:nth-child(2) { - width: 32px; - } -} diff --git a/website/components/openapi-page/partials/property-object/index.js b/website/components/openapi-page/partials/property-object/index.js deleted file mode 100644 index 63e672a20..000000000 --- a/website/components/openapi-page/partials/property-object/index.js +++ /dev/null @@ -1,91 +0,0 @@ -import { useState } from 'react' -import Collapsible from '../collapsible' -import SvgrChevronDown from '../chevron-down/black' -import styles from './property-object.module.css' - -function PropertyObject({ - name, - data, - isFirstItem, - isLastItem, - arrayDepth = 0, -}) { - const [isCollapsed, setIsCollapsed] = useState(true) - const isArray = data.type === 'array' - if (isArray) - return ( - - ) - const hasProperties = data.type === 'object' && Boolean(data.properties) - - const typeArraySuffix = - arrayDepth > 0 ? arrayFrom(arrayDepth, '[]').join('') : '' - const typeString = `${data.type}${typeArraySuffix}` - return ( -
- {name}{' '} - {typeString}{' '} - {data.required ? ( - - Required - - ) : null} - {data.title && ( -

{data.title}

- )} - {data.description && ( -

- {data.description} -

- )} - {hasProperties && ( -
- - -
- {Object.keys(data.properties).map((propertyKey, idx) => { - return ( - - ) - })} -
-
-
- )} -
- ) -} - -function arrayFrom(length, value = null) { - let array = [] - for (var i = 0; i < length; i++) { - array.push(value) - } - return array -} - -export default PropertyObject diff --git a/website/components/openapi-page/partials/property-object/property-object.module.css b/website/components/openapi-page/partials/property-object/property-object.module.css deleted file mode 100644 index c7f9a73f6..000000000 --- a/website/components/openapi-page/partials/property-object/property-object.module.css +++ /dev/null @@ -1,85 +0,0 @@ -.root { - border-top: 1px solid var(--gray-5); - border-bottom: 1px solid var(--gray-5); - padding: 16px 0; - margin: -1px 0 0 0; - - &[data-is-first-item='true'] { - border-top: none; - } - - &[data-is-last-item='true'] { - border-bottom: none; - } - - & .name { - color: var(--brand); - background: rgba(29, 30, 35, 0.07); - border-radius: 3px; - padding: 0.3em 0.625em; - } - - & .typeString { - color: var(--gray-2); - background: none; - padding: 0; - margin-left: 2px; - } - - & .requiredFlag { - color: var(--danger-d1); - margin-left: 4px; - } - - & .title, - & .description { - color: var(--gray-2); - margin: 8px 0 0 0; - } - - & .toggleButton { - border: 1px solid red; - border: none; - color: var(--brand); - background: none; - display: flex; - align-items: center; - margin: 8px 0 0 0; - padding: 0; - - & .toggleIcon { - position: relative; - top: 1px; - width: 16px; - height: 16px; - margin-right: 8px; - transition: transform 0.3s; - display: flex; - align-items: center; - justify-content: center; - - & svg { - & [fill] { - fill: var(--brand); - } - & [stroke] { - stroke: var(--brand); - } - } - - &[data-is-collapsed='false'] { - transform: rotate(-180deg); - } - } - - &:hover { - cursor: pointer; - } - } - - & .propertiesContainer { - margin: 8px 0 0 8px; - padding-left: 16px; - border-left: 1px solid var(--gray-5); - } -} diff --git a/website/components/openapi-page/partials/response-object/index.js b/website/components/openapi-page/partials/response-object/index.js deleted file mode 100644 index 4d39fe833..000000000 --- a/website/components/openapi-page/partials/response-object/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropertyObject from '../property-object' - -function ResponseObject({ data }) { - // `schema` can be empty, which means the response does not return content - // We currently only support object responses (ie those that have schema.properties) in the UI - // Ref: https://swagger.io/specification/v2/#response-object - if (!data || !data.schema || !data.schema.properties) { - return
No content.
- } - return ( -
- {Object.keys(data.schema.properties).map((propertyKey, idx) => { - return ( - - ) - })} -
- ) -} - -export default ResponseObject diff --git a/website/components/openapi-page/partials/response-object/index.props.js b/website/components/openapi-page/partials/response-object/index.props.js deleted file mode 100644 index 8ed469813..000000000 --- a/website/components/openapi-page/partials/response-object/index.props.js +++ /dev/null @@ -1,16 +0,0 @@ -import PropTypes from 'prop-types' -import ResponseObject from '.' - -function ResponseObjectProps(props) { - return -} - -ResponseObjectProps.propTypes = { - /** [Response Object](https://swagger.io/specification/v2/#response-object) data. */ - data: PropTypes.shape({ - /** A [Schema Object](https://swagger.io/specification/v2/#schema-object) that describes the response. Note that we currently only support objects (with schema.properties) responses in the UI. */ - schema: PropTypes.object, - }), -} - -export default ResponseObjectProps diff --git a/website/components/openapi-page/style.module.css b/website/components/openapi-page/style.module.css deleted file mode 100644 index 52da68316..000000000 --- a/website/components/openapi-page/style.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.root { - composes: g-grid-container from global; - display: flex; - flex-direction: column; - flex: 1 0 auto; - position: relative; - width: 100%; - - @media (min-width: 940px) { - flex-direction: row; - margin-bottom: 24px; - margin-top: 72px; - } - - /* currently only supports "boundary", may want to extend to support other product themes */ - &[data-theme='boundary'] { - --brand: var(--boundary); - } - - & .pageHeading { - margin: 0; - } - - & .categoryHeading { - margin: 32px 0 16px 0; - } - - & :global(.g-content) { - padding-left: 0; - padding-right: 0; - margin: 0; - - @media (max-width: 939px) { - padding-left: 0; - padding-right: 0; - margin-top: 64px; - } - - & > h1:first-child { - margin-top: 0; - } - } - - & :global(.g-section-header) { - margin-bottom: 100px; - } - - /* TODO: this should be applied in global styles, temporary override here */ - & pre, - & code { - font-size: 0.875em; - } - - & pre code { - font-size: 1em; - } -} diff --git a/website/components/openapi-page/utils/routing-utils.js b/website/components/openapi-page/utils/routing-utils.js deleted file mode 100644 index 0bffdaa88..000000000 --- a/website/components/openapi-page/utils/routing-utils.js +++ /dev/null @@ -1,121 +0,0 @@ -import { capitalCase } from 'change-case' -import slugify from '@hashicorp/remark-plugins/generate_slug' - -/* Gieven a schema, current service slug, and parentPath, return all props needed for an openapi component page */ -export function getPropsForPage(schema, params) { - // parse the data we'll show to the user from the schema - const operationObjects = getOperationObjects(schema) - const serviceIds = getServiceIds(operationObjects) - // info and sidenav data are needed on all pages - const info = schema.info - const navData = getSidenavData(serviceIds) - // If there's no "page" param, then this is the landing page - const isLanding = !params || !params.page || params.page.length == 0 - - const currentPath = params && params.page ? params.page.join('/') : '' - - // Otherwise, we should have an operationCategory that matches the slug-ified ID from the URL path - const operationCategory = isLanding - ? false - : serviceIds - .filter((id) => getServiceSlug(id) === params.page[0]) - .map((serviceId) => { - const name = capitalCase(serviceId) - const slug = getServiceSlug(serviceId) - const operations = operationObjects.filter( - (o) => getServiceId(o.operationId) === serviceId - ) - return { name, slug, operations } - })[0] - - return { info, navData, operationCategory, currentPath } -} - -/* Given a schema, return all the paths we'll render for our openapi generated docs */ -export function getPathsFromSchema(schema) { - // Assign each operation category to a URL using its slug-ified ID - const operationObjects = getOperationObjects(schema) - const slugs = getServiceIds(operationObjects).map(getServiceSlug) - // We need a path for each "service" - const paths = slugs.map((slug) => ({ params: { page: [slug] } })) - // We also push a path for an "/" index page - paths.push({ params: { page: [] } }) - return paths -} - -/* Given a list of service ids, return an object suitable to pass to -to render a flat list of services */ -function getSidenavData(serviceIds) { - const order = serviceIds.map((serviceId) => { - return { - title: capitalCase(serviceId), - indexData: true, - path: getServiceSlug(serviceId), - } - }) - return order -} - -/* Given a serviceId, which is typically PascalCase string, return a slugified kebab-case string (ie: pascal-case rather than pascalcase) */ -function getServiceSlug(serviceId) { - return slugify(capitalCase(serviceId)) -} - -/* Given an operationId, return the "serviceId" */ -function getServiceId(operationId) { - // We expect operationIds to have two parts, separated by an underscore - // The "serviceId" is the first part of the value - return operationId.split('_')[0] -} - -/* Given a schema, return an array of unique operation "category" strings */ -export function getServiceIds(operationObjects) { - const operationIdCategories = operationObjects - .map((o) => getServiceId(o.operationId)) - .sort() - // Several related operationIds may have the same "category" part, - // so we filter for unique values before returning - return filterUnique(operationIdCategories) -} - -/* Given an array of values, return an array without duplicate items */ -function filterUnique(array) { - return array.filter((value, idx) => array.indexOf(value) === idx) -} - -/* Given a schema, return a flattened list of operation objects */ -function getOperationObjects(schema) { - const pathItemObjects = Object.keys(schema.paths).reduce((acc, path) => { - acc.push({ __path: path, ...schema.paths[path] }) - return acc - }, []) - - const operationObjects = pathItemObjects.reduce((acc, pathItemObject) => { - // Each path can support many operations through different request types - const requestTypes = [ - 'get', - 'put', - 'post', - 'delete', - 'options', - 'head', - 'patch', - ] - const pathOperations = requestTypes.reduce((acc, type) => { - // Not all paths will support every request type - if (!pathItemObject[type]) return acc - // If the request type is supported, push the associated operation - acc.push({ - __type: type, - __path: pathItemObject.__path, - ...pathItemObject[type], - }) - return acc - }, []) - return acc.concat(pathOperations) - }, []) - - return operationObjects -} - -export default getPathsFromSchema diff --git a/website/package-lock.json b/website/package-lock.json index 561a5be85..0b24b5899 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -26,6 +26,7 @@ "@hashicorp/react-hero": "^8.0.2", "@hashicorp/react-image": "^4.0.3", "@hashicorp/react-markdown-page": "^1.4.3", + "@hashicorp/react-open-api-page": "^2.0.1", "@hashicorp/react-product-downloads-page": "^2.5.2", "@hashicorp/react-product-features-list": "^5.0.0", "@hashicorp/react-search": "^6.1.1", @@ -877,6 +878,11 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==" }, + "node_modules/@hashicorp/flight-icons": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@hashicorp/flight-icons/-/flight-icons-1.2.0.tgz", + "integrity": "sha512-gMsPGbDXSBWe4v7sFjG+ZlQdc8ipNLi5w98muaKU5tUNG027b5XNy5chkiT1/gsygi5ZlKXU0klgC0JHZ44B8A==" + }, "node_modules/@hashicorp/js-utils": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@hashicorp/js-utils/-/js-utils-1.0.10.tgz", @@ -1199,9 +1205,9 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/@hashicorp/react-content": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-8.0.2.tgz", - "integrity": "sha512-FNE1IiXizUDPL/rsvo37uYSBsAB8LhtCianMU/h3MIj8Khcwc/qC3pTabGs9Kv3ujA+fiIp/ImzWEGAhB22T7g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-8.0.3.tgz", + "integrity": "sha512-WSmTlU3UU0YMbp1Eb9RCZqWh4skwIeXPQY/OFaNLgZqQh6/vP+j7c6pItPhnSz8w0u81XatuxVfo0cJjtxidYg==", "dependencies": { "@hashicorp/platform-product-meta": "^0.1.0", "classnames": "^2.3.1" @@ -1446,6 +1452,26 @@ "react": ">=16.x" } }, + "node_modules/@hashicorp/react-open-api-page": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hashicorp/react-open-api-page/-/react-open-api-page-2.0.1.tgz", + "integrity": "sha512-9D1rAKksUyvrdsKFA/uR54jFSO4egr8np6X8ayIPSRUSCbIqLNKl46MitXf9oddqL3xunaN2dL/cjqQ7FYycPA==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@hashicorp/flight-icons": "^1.1.0", + "@hashicorp/platform-markdown-utils": "^0.1.3", + "@hashicorp/react-content": "^8.0.3", + "@hashicorp/react-docs-sidenav": "^8.4.0", + "@hashicorp/react-head": "^3.1.2", + "change-case": "^4.1.2", + "classnames": "^2.3.1" + } + }, + "node_modules/@hashicorp/react-open-api-page/node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "node_modules/@hashicorp/react-placeholder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@hashicorp/react-placeholder/-/react-placeholder-0.1.0.tgz", @@ -4443,7 +4469,7 @@ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/bin-wrapper/node_modules/file-type": { @@ -4515,6 +4541,9 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true, + "dependencies": { + "p-timeout": "^2.0.1" + }, "engines": { "node": ">=4" } @@ -7296,7 +7325,7 @@ "esutils": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/eslint-plugin-import/node_modules/hosted-git-info": { @@ -8272,7 +8301,7 @@ "version": "3.19.0", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==", - "dev": true, + "devOptional": true, "bin": { "xml2js": "cli.js" }, @@ -8506,7 +8535,10 @@ "locate-path": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/find-versions": { @@ -10372,7 +10404,10 @@ "kind-of": "^6.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-date-object": { @@ -10584,10 +10619,13 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "devOptional": true, "dependencies": { - "isobject": "^3.0.1" + "fast-xml-parser": "^3.19.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-png": { @@ -10657,14 +10695,8 @@ "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.3.1.tgz", "integrity": "sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==", "dev": true, - "dependencies": { - "fast-xml-parser": "^3.19.0" - }, - "engines": { - "node": ">=6" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-symbol": { @@ -10888,7 +10920,10 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -11205,8 +11240,8 @@ "engines": { "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/lint-staged/node_modules/has-flag": { @@ -12379,7 +12414,7 @@ "is-extendable": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0.0" } }, "node_modules/mixin-deep/node_modules/is-extendable": { @@ -20957,6 +20992,11 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==" }, + "@hashicorp/flight-icons": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@hashicorp/flight-icons/-/flight-icons-1.2.0.tgz", + "integrity": "sha512-gMsPGbDXSBWe4v7sFjG+ZlQdc8ipNLi5w98muaKU5tUNG027b5XNy5chkiT1/gsygi5ZlKXU0klgC0JHZ44B8A==" + }, "@hashicorp/js-utils": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@hashicorp/js-utils/-/js-utils-1.0.10.tgz", @@ -21232,9 +21272,9 @@ } }, "@hashicorp/react-content": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-8.0.2.tgz", - "integrity": "sha512-FNE1IiXizUDPL/rsvo37uYSBsAB8LhtCianMU/h3MIj8Khcwc/qC3pTabGs9Kv3ujA+fiIp/ImzWEGAhB22T7g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-8.0.3.tgz", + "integrity": "sha512-WSmTlU3UU0YMbp1Eb9RCZqWh4skwIeXPQY/OFaNLgZqQh6/vP+j7c6pItPhnSz8w0u81XatuxVfo0cJjtxidYg==", "requires": { "@hashicorp/platform-product-meta": "^0.1.0", "classnames": "^2.3.1" @@ -21432,6 +21472,28 @@ "gray-matter": "4.0.2" } }, + "@hashicorp/react-open-api-page": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hashicorp/react-open-api-page/-/react-open-api-page-2.0.1.tgz", + "integrity": "sha512-9D1rAKksUyvrdsKFA/uR54jFSO4egr8np6X8ayIPSRUSCbIqLNKl46MitXf9oddqL3xunaN2dL/cjqQ7FYycPA==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@hashicorp/flight-icons": "^1.1.0", + "@hashicorp/platform-markdown-utils": "^0.1.3", + "@hashicorp/react-content": "^8.0.3", + "@hashicorp/react-docs-sidenav": "^8.4.0", + "@hashicorp/react-head": "^3.1.2", + "change-case": "^4.1.2", + "classnames": "^2.3.1" + }, + "dependencies": { + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + } + } + }, "@hashicorp/react-placeholder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@hashicorp/react-placeholder/-/react-placeholder-0.1.0.tgz", @@ -23891,7 +23953,10 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", - "dev": true + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } }, "p-event": { "version": "2.3.1", @@ -26937,7 +27002,7 @@ "version": "3.19.0", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==", - "dev": true + "devOptional": true }, "fastest-levenshtein": { "version": "1.0.12", @@ -28704,7 +28769,7 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "devOptional": true, "requires": { - "isobject": "^3.0.1" + "fast-xml-parser": "^3.19.0" } }, "is-png": { @@ -28749,10 +28814,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.3.1.tgz", "integrity": "sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==", - "dev": true, - "requires": { - "fast-xml-parser": "^3.19.0" - } + "dev": true }, "is-symbol": { "version": "1.0.4", @@ -29169,7 +29231,10 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } }, "has-flag": { "version": "4.0.0", diff --git a/website/package.json b/website/package.json index c2b6cd3ff..4e787564e 100644 --- a/website/package.json +++ b/website/package.json @@ -22,6 +22,7 @@ "@hashicorp/react-hero": "^8.0.2", "@hashicorp/react-image": "^4.0.3", "@hashicorp/react-markdown-page": "^1.4.3", + "@hashicorp/react-open-api-page": "^2.0.1", "@hashicorp/react-product-downloads-page": "^2.5.2", "@hashicorp/react-product-features-list": "^5.0.0", "@hashicorp/react-search": "^6.1.1", diff --git a/website/pages/api-docs/[[...page]].jsx b/website/pages/api-docs/[[...page]].jsx index eb752b15f..4fa73554f 100644 --- a/website/pages/api-docs/[[...page]].jsx +++ b/website/pages/api-docs/[[...page]].jsx @@ -1,10 +1,12 @@ import { productName, productSlug } from 'data/metadata' +import OpenApiPage from '@hashicorp/react-open-api-page' +/* Used server-side only */ import path from 'path' -import parseSwagger from '../../lib/swagger-parser' -import OpenApiPage, { +import { getPathsFromSchema, getPropsForPage, -} from '../../components/openapi-page' +} from '@hashicorp/react-open-api-page/server' +import { processSchemaFile } from '@hashicorp/react-open-api-page/process-schema' const targetFile = '../internal/gen/controller.swagger.json' const pathFromRoot = 'api-docs' @@ -15,19 +17,21 @@ export default function OpenApiDocsPage(props) { {...props} productName={productName} productSlug={productSlug} - pathFromRoot={pathFromRoot} + baseRoute={pathFromRoot} /> ) } export async function getStaticPaths() { - const schema = await parseSwagger(path.join(process.cwd(), targetFile)) + const swaggerFile = path.join(process.cwd(), targetFile) + const schema = await processSchemaFile(swaggerFile) const paths = getPathsFromSchema(schema) return { paths, fallback: false } } export async function getStaticProps({ params }) { - const schema = await parseSwagger(path.join(process.cwd(), targetFile)) + const swaggerFile = path.join(process.cwd(), targetFile) + const schema = await processSchemaFile(swaggerFile) const props = getPropsForPage(schema, params) return { props } } From d58e7670f12104eca1b074b9368101fe17667aa9 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 20 Oct 2021 15:12:09 -0400 Subject: [PATCH 12/17] Make PG resource usage nicer for dev (#1615) This overrides settings specifically in CI scenarios to the current values but reduces the resource requirements for local dev work. --- .circleci/config.yml | 2 +- .circleci/config/jobs/build.yml | 2 +- testing/dbtest/docker/postgresql.conf | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 781db88a7..824a6f7c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -980,7 +980,7 @@ jobs: - run: command: | which pg_isready || sudo apt-get update && sudo apt-get install -y postgresql-client - make DOCKER_ARGS='-d' -C testing/dbtest/docker database-up + make DOCKER_ARGS='-d' PG_OPTS='-c shared_buffers=256MB -c max_connections=200000' -C testing/dbtest/docker database-up until pg_isready -h 127.0.0.1; do docker container inspect boundary-sql-tests &> /dev/null || exit -1; sleep 1; done name: Initialize Test Database - run: diff --git a/.circleci/config/jobs/build.yml b/.circleci/config/jobs/build.yml index 9efbe0e0c..08880157b 100644 --- a/.circleci/config/jobs/build.yml +++ b/.circleci/config/jobs/build.yml @@ -5,7 +5,7 @@ steps: name: "Initialize Test Database" command: | which pg_isready || sudo apt-get update && sudo apt-get install -y postgresql-client - make DOCKER_ARGS='-d' -C testing/dbtest/docker database-up + make DOCKER_ARGS='-d' PG_OPTS='-c shared_buffers=256MB -c max_connections=200000' -C testing/dbtest/docker database-up until pg_isready -h 127.0.0.1; do docker container inspect boundary-sql-tests &> /dev/null || exit -1; sleep 1; done - run: name: "Run Acceptance Tests" diff --git a/testing/dbtest/docker/postgresql.conf b/testing/dbtest/docker/postgresql.conf index 2cfab2543..675082eee 100644 --- a/testing/dbtest/docker/postgresql.conf +++ b/testing/dbtest/docker/postgresql.conf @@ -1,9 +1,10 @@ listen_addresses = '*' # listen on all addresses so other containers can connect if need be. -max_connections = 200000 # set max connections to the limit to ensure tests can always get a connection. - # Note that this may need to be decreased if <4G are available for the container. -shared_buffers = 256MB # A more reasonable default compared to the 32MB default config. +max_connections = 1000 # set max connections a higher limit than default; this will be set to the max limit for CI runs + # the limit to ensure tests can always get a connection. +shared_buffers = 64MB # A more reasonable default compared to the 32MB default config. # This gives postgres more memory for storing results. But given that short lived nature of the # test databases and the relatively small data sets, there should not be a need to increase it much more then this. + # For CI runs this is increased to 256MB. dynamic_shared_memory_type = posix # Use POSIX shared memory allocated using shm_open fsync = off # Do not flush, can cause data corruption, but since it is for tests and the data is dropped anyway... synchronous_commit = off # Do not wait for WAL records to be written to disk before reporting success From 28e6fd591c47b94b1f2737d2b9ddc77ad13bae36 Mon Sep 17 00:00:00 2001 From: Brandon Romano Date: Wed, 20 Oct 2021 16:26:58 -0700 Subject: [PATCH 13/17] Update HashiConf alert-banner expiration (#1623) Updates the HashiConf Alert Banner expiration to 10/20 @ 11pm (PT) --- website/data/alert-banner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/data/alert-banner.js b/website/data/alert-banner.js index fe3e5abb5..48fc08984 100644 --- a/website/data/alert-banner.js +++ b/website/data/alert-banner.js @@ -8,5 +8,5 @@ export default { linkText: 'Join Now', // Set the expirationDate prop with a datetime string (e.g. '2020-01-31T12:00:00-07:00') // if you'd like the component to stop showing at or after a certain date - expirationDate: '2021-10-21T12:00:00-07:00', + expirationDate: '2021-10-20T23:00:00-07:00', } From 95327b5e438684a4c82e709975c0584e70ee4146 Mon Sep 17 00:00:00 2001 From: Louis Ruch Date: Wed, 20 Oct 2021 19:16:14 -0700 Subject: [PATCH 14/17] style: Run make fmt (#1626) --- .../servers/controller/handlers/outgoing_response_filter.go | 1 - .../controller/handlers/outgoing_response_filter_test.go | 1 - internal/servers/controller/interceptor_test.go | 2 -- 3 files changed, 4 deletions(-) diff --git a/internal/servers/controller/handlers/outgoing_response_filter.go b/internal/servers/controller/handlers/outgoing_response_filter.go index cf6242002..304409775 100644 --- a/internal/servers/controller/handlers/outgoing_response_filter.go +++ b/internal/servers/controller/handlers/outgoing_response_filter.go @@ -49,7 +49,6 @@ func OutgoingResponseFilter(ctx context.Context, w http.ResponseWriter, m proto. const op = "handlers.OutgoingResponseFilter" if md, ok := runtime.ServerMetadataFromContext(ctx); ok { - // set http status codes based on metadata set by the grpc service if statusCodes := md.HeaderMD.Get(StatusCodeHeader); len(statusCodes) > 0 { defer func() { diff --git a/internal/servers/controller/handlers/outgoing_response_filter_test.go b/internal/servers/controller/handlers/outgoing_response_filter_test.go index 2d0932f56..c445329d3 100644 --- a/internal/servers/controller/handlers/outgoing_response_filter_test.go +++ b/internal/servers/controller/handlers/outgoing_response_filter_test.go @@ -68,5 +68,4 @@ func TestOutgoingResponseFilter_StatusCode(t *testing.T) { } }) } - } diff --git a/internal/servers/controller/interceptor_test.go b/internal/servers/controller/interceptor_test.go index 8553f2498..f970022c0 100644 --- a/internal/servers/controller/interceptor_test.go +++ b/internal/servers/controller/interceptor_test.go @@ -376,7 +376,6 @@ func Test_errorInterceptor(t *testing.T) { require.NoError(err) }) } - } func Test_statusCodeInterceptor(t *testing.T) { @@ -424,7 +423,6 @@ func Test_statusCodeInterceptor(t *testing.T) { } }) } - } type testGreeter struct { From 324a00c07e82a1dc7a6ddd1dabee68d0e700e54f Mon Sep 17 00:00:00 2001 From: Pete Pacent Date: Wed, 20 Oct 2021 21:35:29 -0700 Subject: [PATCH 15/17] Adding reference architecture docs (#1625) --- website/content/docs/installing/index.mdx | 4 +++- .../content/docs/installing/reference-architectures.mdx | 7 +++++++ website/data/docs-nav-data.json | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 website/content/docs/installing/reference-architectures.mdx diff --git a/website/content/docs/installing/index.mdx b/website/content/docs/installing/index.mdx index 8073baef3..9474e701f 100644 --- a/website/content/docs/installing/index.mdx +++ b/website/content/docs/installing/index.mdx @@ -7,4 +7,6 @@ description: |- # Overview -This section covers installing Boundary for different modes of operation and across different environments. +This section covers installing Boundary for different modes of operation and across different environments + +For community-supported reference architectures, please see [Reference Architectures](/docs/installing/reference-architectures). diff --git a/website/content/docs/installing/reference-architectures.mdx b/website/content/docs/installing/reference-architectures.mdx new file mode 100644 index 000000000..2742af426 --- /dev/null +++ b/website/content/docs/installing/reference-architectures.mdx @@ -0,0 +1,7 @@ +# Reference Architectures +A number of community-supported reference architectures for non-dev deployments can be found in Boundary's dedicated [reference architecture repo](https://github.com/hashicorp/boundary-reference-architecture). + + +While some of the initial reference deployments have been seeded by the Boundary team, the seeded examples are for demonstration purposes only and intended convey how to get Boundary up and running on popular cloud and container platforms. + +For those exploring Boundary in non-dev mode, we highly encourage contributing configurations to the repo such that others may benefit from them. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 8da8f03a4..4a0d6fc64 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -56,6 +56,10 @@ { "title": "High Availability Install", "path": "installing/high-availability" + }, + { + "title": "Reference Architectures", + "path": "installing/reference-architectures" } ] }, From 5a2fa689a78960e63984c7987312c436a329bd52 Mon Sep 17 00:00:00 2001 From: Oleg Butuzov Date: Thu, 21 Oct 2021 15:02:15 +0300 Subject: [PATCH 16/17] refactor: replace (*regexp.Regexp).Match on (*regexp.Regexp).MatchString (#1617) This PR change one method of regexp.Regexp struct with a similar one, as result code produce less allocations on heap. --- internal/auth/password/repository_account.go | 2 +- internal/cmd/common/flags.go | 2 +- internal/servers/controller/handlers/verifiers.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/auth/password/repository_account.go b/internal/auth/password/repository_account.go index 3bb78540f..4bd0beb07 100644 --- a/internal/auth/password/repository_account.go +++ b/internal/auth/password/repository_account.go @@ -212,7 +212,7 @@ func validLoginName(u string) bool { if u == "" { return false } - return !reInvalidLoginName.Match([]byte(u)) + return !reInvalidLoginName.MatchString(u) } // UpdateAccount updates the repository entry for a.PublicId with the diff --git a/internal/cmd/common/flags.go b/internal/cmd/common/flags.go index f7b197896..9c022b383 100644 --- a/internal/cmd/common/flags.go +++ b/internal/cmd/common/flags.go @@ -301,7 +301,7 @@ func HandleAttributeFlags(c *base.Command, suffix, fullField string, sepFields [ case strings.HasPrefix(field.Value, `"`): // explicitly quoted string val = strings.Trim(field.Value, `"`) - case jsonNumberRegex.Match([]byte(strings.Trim(field.Value, `"`))): // number + case jsonNumberRegex.MatchString(strings.Trim(field.Value, `"`)): // number // Same logic as above if strings.Contains(field.Value, ".") { val, err = strconv.ParseFloat(field.Value, 64) diff --git a/internal/servers/controller/handlers/verifiers.go b/internal/servers/controller/handlers/verifiers.go index 5481c62ab..4d394ee07 100644 --- a/internal/servers/controller/handlers/verifiers.go +++ b/internal/servers/controller/handlers/verifiers.go @@ -183,7 +183,7 @@ func ValidId(i Id, prefixes ...string) bool { continue } id = strings.TrimPrefix(id, prefix) - if !reInvalidID.Match([]byte(id)) { + if !reInvalidID.MatchString(id) { return true } } From 1ce9a71b538c1eb3170b5165f2151c47c03c9353 Mon Sep 17 00:00:00 2001 From: Jim Date: Thu, 21 Oct 2021 11:11:56 -0400 Subject: [PATCH 17/17] feature (db): Add on conflict support to db.Create(...) (#1612) --- internal/auth/oidc/repository_auth_method.go | 142 +++++----------- internal/db/clause.go | 123 ++++++++++++++ internal/db/option.go | 19 +++ internal/db/option_test.go | 39 +++-- internal/db/read_writer.go | 83 ++++++++-- internal/db/read_writer_ext_test.go | 152 ++++++++++++++++++ internal/db/read_writer_test.go | 2 +- .../oss/postgres/17/01_servers.up.sql | 16 ++ .../local/controller/servers/v1/servers.proto | 1 + internal/servers/query.go | 12 -- internal/servers/repository.go | 18 +-- internal/servers/servers.pb.go | 3 +- 12 files changed, 461 insertions(+), 149 deletions(-) create mode 100644 internal/db/clause.go create mode 100644 internal/db/read_writer_ext_test.go create mode 100644 internal/db/schema/migrations/oss/postgres/17/01_servers.up.sql diff --git a/internal/auth/oidc/repository_auth_method.go b/internal/auth/oidc/repository_auth_method.go index da8dbb569..cadba1dd7 100644 --- a/internal/auth/oidc/repository_auth_method.go +++ b/internal/auth/oidc/repository_auth_method.go @@ -2,11 +2,9 @@ package oidc import ( "context" - "database/sql" "encoding/json" "fmt" "net/url" - "strings" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" @@ -72,148 +70,84 @@ func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenC if err != nil { return nil, errors.Wrap(ctx, err, op) } - - columns := []string{"public_id", "auth_method_id", "issuer", "subject"} - values := []interface{}{ - sql.Named("1", pubId), - sql.Named("2", am.PublicId), - sql.Named("3", iss), - sql.Named("4", sub), - } - var conflictClauses, fieldMasks, nullMasks []string - - { - marshaledTokenClaims, err := json.Marshal(IdTokenClaims) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - columns, values = append(columns, "token_claims"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), string(marshaledTokenClaims))) - conflictClauses = append(conflictClauses, fmt.Sprintf("token_claims = @%d", len(values))) - fieldMasks = append(fieldMasks, TokenClaimsField) - } - { - marshaledAccessTokenClaims, err := json.Marshal(AccessTokenClaims) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - columns, values = append(columns, "userinfo_claims"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), string(marshaledAccessTokenClaims))) - conflictClauses = append(conflictClauses, fmt.Sprintf("userinfo_claims = @%d", len(values))) - fieldMasks = append(fieldMasks, UserinfoClaimsField) - } - issAsUrl, err := url.Parse(iss) if err != nil { return nil, errors.New(ctx, errors.Unknown, op, "unable to parse issuer", errors.WithWrap(err)) } - acctForOplog, err := NewAccount(ctx, am.PublicId, sub, WithIssuer(issAsUrl)) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create new acct for oplog")) + + acct := AllocAccount() + acct.PublicId = pubId + acct.AuthMethodId = am.PublicId + acct.Issuer = issAsUrl.String() + acct.Subject = sub + + oc := db.OnConflict{ + Target: db.Constraint("auth_oidc_account_auth_method_id_issuer_subject_uq"), + Action: db.SetColumns([]string{"public_id", "auth_method_id", "issuer", "subject", "userinfo_claims", "token_claims"}), } - var foundName interface{} + marshaledTokenClaims, err := json.Marshal(IdTokenClaims) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + acct.TokenClaims = string(marshaledTokenClaims) + + marshaledAccessTokenClaims, err := json.Marshal(AccessTokenClaims) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + acct.UserinfoClaims = string(marshaledAccessTokenClaims) + + var foundName, foundEmail bool switch { case AccessTokenClaims[fromName] != nil: - foundName = AccessTokenClaims[fromName] - columns, values = append(columns, "full_name"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), foundName)) + foundName = true + acct.FullName = AccessTokenClaims[fromName].(string) case IdTokenClaims[fromName] != nil: - foundName = IdTokenClaims[fromName] - columns, values = append(columns, "full_name"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), foundName)) - } - if foundName != nil { - acctForOplog.FullName = foundName.(string) - conflictClauses = append(conflictClauses, fmt.Sprintf("full_name = @%d", len(values))) - fieldMasks = append(fieldMasks, NameField) - } else { - conflictClauses = append(conflictClauses, "full_name = NULL") - nullMasks = append(nullMasks, NameField) + foundName = true + acct.FullName = IdTokenClaims[fromName].(string) } - var foundEmail interface{} switch { case AccessTokenClaims[fromEmail] != nil: - foundEmail = AccessTokenClaims[fromEmail] - columns, values = append(columns, "email"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), foundEmail)) + foundEmail = true + acct.Email = AccessTokenClaims[fromEmail].(string) case IdTokenClaims[fromEmail] != nil: - foundEmail = IdTokenClaims[fromEmail] - columns, values = append(columns, "email"), append(values, sql.Named(fmt.Sprintf("%d", len(values)+1), foundEmail)) + foundEmail = true + acct.Email = IdTokenClaims[fromEmail].(string) } - if foundEmail != nil { - acctForOplog.Email = foundEmail.(string) - conflictClauses = append(conflictClauses, fmt.Sprintf("email = @%d", len(values))) - fieldMasks = append(fieldMasks, "Email") - } else { - conflictClauses = append(conflictClauses, "email = NULL") - nullMasks = append(nullMasks, "Email") + if foundName { + oc.Action = append(oc.Action.([]db.ColumnValue), db.SetColumns([]string{"full_name"})...) } - - placeHolders := make([]string, 0, len(columns)) - for colNum := range columns { - placeHolders = append(placeHolders, fmt.Sprintf("@%d", colNum+1)) + if foundEmail { + oc.Action = append(oc.Action.([]db.ColumnValue), db.SetColumns([]string{"email"})...) } - query := fmt.Sprintf(acctUpsertQuery, strings.Join(columns, ", "), strings.Join(placeHolders, ", "), strings.Join(conflictClauses, ", ")) oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } - updatedAcct := AllocAccount() _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { - var err error - rows, err := w.Query(ctx, query, values) + var rowCnt int64 + err := w.Create(ctx, acct, db.WithOnConflict(&oc), db.WithReturnRowsAffected(&rowCnt), db.WithOplog(oplogWrapper, acct.oplog(oplog.OpType_OP_TYPE_CREATE, am.ScopeId))) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to insert/update auth oidc account")) } - defer rows.Close() - result := struct { - PublicId string - Version int - }{} - var rowCnt int - for rows.Next() { - rowCnt += 1 - err = r.reader.ScanRows(rows, &result) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to scan rows for account")) - } - } if rowCnt > 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", rowCnt)) } - if err := reader.LookupWhere(ctx, &updatedAcct, "auth_method_id = ? and issuer = ? and subject = ?", am.PublicId, iss, sub); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to look up auth oidc account for: %s / %s / %s", am.PublicId, iss, sub))) - } - // include the version incase of predictable account public ids based on a calculation using authmethod id and subject - if result.Version == 1 && updatedAcct.PublicId == pubId { - if err := upsertOplog(ctx, w, oplogWrapper, oplog.OpType_OP_TYPE_CREATE, am.ScopeId, updatedAcct, nil, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write create oplog for account")) - } - } else { - if len(fieldMasks) > 0 || len(nullMasks) > 0 { - acctForOplog := AllocAccount() - acctForOplog.PublicId = updatedAcct.PublicId - if foundEmail != nil { - acctForOplog.Email = foundEmail.(string) - } - if foundName != nil { - acctForOplog.FullName = foundName.(string) - } - if err := upsertOplog(ctx, w, oplogWrapper, oplog.OpType_OP_TYPE_UPDATE, am.ScopeId, acctForOplog, fieldMasks, nullMasks); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write update oplog for account")) - } - } - } return nil }, ) if err != nil { return nil, errors.Wrap(ctx, err, op) } - return updatedAcct, nil + return acct, nil } // upsertOplog will write oplog msgs for account upserts. The db.Writer needs to be the writer for the current diff --git a/internal/db/clause.go b/internal/db/clause.go new file mode 100644 index 000000000..0beb4d758 --- /dev/null +++ b/internal/db/clause.go @@ -0,0 +1,123 @@ +package db + +import ( + "sort" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// ColumnValue defines a column and it's assigned value for a database operation +type ColumnValue struct { + column string + value interface{} +} + +type assigner interface { + toAssignment(column string) clause.Assignment +} + +// column represents a table column +type column struct { + name string + table string +} + +func (c *column) toAssignment(column string) clause.Assignment { + return clause.Assignment{ + Column: clause.Column{Name: column}, + Value: clause.Column{Table: c.table, Name: c.name}, + } +} + +func rawAssignment(column string, value interface{}) clause.Assignment { + return clause.Assignment{ + Column: clause.Column{Name: column}, + Value: value, + } +} + +// ExprValue encapsulates an expression value for a column assignment. See +// Expr(...) to create these values. +type ExprValue struct { + sql string + vars []interface{} +} + +func (ev *ExprValue) toAssignment(column string) clause.Assignment { + return clause.Assignment{ + Column: clause.Column{Name: column}, + Value: gorm.Expr(ev.sql, ev.vars...), + } +} + +// Expr creates an expression value (ExprValue) which can be used when setting +// column values for database operations. See: Expr(...) +// +// Set name column to null example: +// SetColumnValues(map[string]interface{}{"name": Expr("NULL")}) +// +// Set exp_time column to N seconds from now: +// SetColumnValues(map[string]interface{}{"exp_time": Expr("wt_add_seconds_to_now(?)", 10)}) +func Expr(expr string, args ...interface{}) ExprValue { + return ExprValue{sql: expr, vars: args} +} + +// SetColumnValues defines a map from column names to values +func SetColumnValues(columnValues map[string]interface{}) []ColumnValue { + keys := make([]string, 0, len(columnValues)) + for key := range columnValues { + keys = append(keys, key) + } + sort.Strings(keys) + + assignments := make([]ColumnValue, len(keys)) + for idx, key := range keys { + assignments[idx] = ColumnValue{column: key, value: columnValues[key]} + } + return assignments +} + +// SetColumns defines a list of column (names) to update using the set of +// proposed insert columns during an on conflict update. +func SetColumns(names []string) []ColumnValue { + assignments := make([]ColumnValue, len(names)) + for idx, name := range names { + assignments[idx] = ColumnValue{ + column: name, + value: column{name: name, table: "excluded"}, + } + } + return assignments +} + +// OnConflict specifies how to handle alternative actions to take when an insert +// results in a unique constraint or exclusion constraint error. +type OnConflict struct { + + // Target specifies what conflict you want to define a policy for. This can + // be any one of these: + // Columns: the name of a specific column or columns + // Constraint: the name of a unique constraint + Target interface{} + + // Action specifies the action to take on conflict. This can be any one of + // these: + // DoNothing: leaves the conflicting record as-is + // UpdateAll: updates all the columns of the conflicting record using the resource's data + // []ColumnValue: update a set of columns of the conflicting record using the set of assignments + Action interface{} +} + +// Constraint defines database constraint name +type Constraint string + +// Columns defines a set of column names +type Columns []string + +// DoNothing defines an "on conflict" action of doing nothing +type DoNothing bool + +// UpdateAll defines an "on conflict" action of updating all columns using the +// proposed insert column values +type UpdateAll bool diff --git a/internal/db/option.go b/internal/db/option.go index 32b50dcc4..ba963875d 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -47,6 +47,9 @@ type Options struct { withGormFormatter hclog.Logger withMaxOpenConnections int + + withOnConflict *OnConflict + withRowsAffected *int64 } type oplogOpts struct { @@ -185,3 +188,19 @@ func WithMaxOpenConnections(max int) Option { o.withMaxOpenConnections = max } } + +// WithOnConflict specifies an optional on conflict criteria which specify +// alternative actions to take when an insert results in a unique constraint or +// exclusion constraint error +func WithOnConflict(onConflict *OnConflict) Option { + return func(o *Options) { + o.withOnConflict = onConflict + } +} + +// WithReturnRowsAffected specifies an option for returning the rows affected +func WithReturnRowsAffected(rowsAffected *int64) Option { + return func(o *Options) { + o.withRowsAffected = rowsAffected + } +} diff --git a/internal/db/option_test.go b/internal/db/option_test.go index 963b9d008..f2cf9588b 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -141,8 +141,6 @@ func Test_getOpts(t *testing.T) { testOpts := getDefaultOptions() testOpts.withSkipVetForWrite = false assert.Equal(opts, testOpts) - - // try setting to false opts = GetOpts(WithSkipVetForWrite(true)) testOpts = getDefaultOptions() testOpts.withSkipVetForWrite = true @@ -156,8 +154,6 @@ func Test_getOpts(t *testing.T) { testOpts.withWhereClause = "" testOpts.withWhereClauseArgs = nil assert.Equal(opts, testOpts) - - // try setting to false opts = GetOpts(WithWhere("id = ? and foo = ?", 1234, "bar")) testOpts.withWhereClause = "id = ? and foo = ?" testOpts.withWhereClauseArgs = []interface{}{1234, "bar"} @@ -170,8 +166,6 @@ func Test_getOpts(t *testing.T) { testOpts := getDefaultOptions() testOpts.withOrder = "" assert.Equal(opts, testOpts) - - // try setting to false opts = GetOpts(WithOrder("version desc")) testOpts.withOrder = "version desc" assert.Equal(opts, testOpts) @@ -184,8 +178,6 @@ func Test_getOpts(t *testing.T) { assert.Equal(opts, testOpts) testLogger := hclog.New(&hclog.LoggerOptions{}) - - // try setting to false opts = GetOpts(WithGormFormatter(testLogger)) testOpts.withGormFormatter = testLogger assert.Equal(opts, testOpts) @@ -196,10 +188,37 @@ func Test_getOpts(t *testing.T) { opts := GetOpts() testOpts := getDefaultOptions() assert.Equal(opts, testOpts) - - // try setting to false opts = GetOpts(WithMaxOpenConnections(22)) testOpts.withMaxOpenConnections = 22 assert.Equal(opts, testOpts) }) + t.Run("WithOnConflict", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := GetOpts() + testOpts := getDefaultOptions() + assert.Equal(opts, testOpts) + + columns := SetColumns([]string{"name", "description"}) + columnValues := SetColumnValues(map[string]interface{}{"expiration": "NULL"}) + testOnConflict := OnConflict{ + Target: Constraint("uniq-name"), + Action: append(columns, columnValues...), + } + opts = GetOpts(WithOnConflict(&testOnConflict)) + testOpts.withOnConflict = &testOnConflict + assert.Equal(opts, testOpts) + }) + t.Run("WithReturnRowsAffected", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := GetOpts() + testOpts := getDefaultOptions() + assert.Equal(opts, testOpts) + + var rowsAffected int64 + opts = GetOpts(WithReturnRowsAffected(&rowsAffected)) + testOpts.withRowsAffected = &rowsAffected + assert.Equal(opts, testOpts) + }) } diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index 119bb8f42..e942147ae 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -15,6 +15,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "google.golang.org/protobuf/proto" "gorm.io/gorm" + "gorm.io/gorm/clause" ) const ( @@ -260,10 +261,12 @@ func (rw *Db) lookupAfterWrite(ctx context.Context, i interface{}, opt ...Option return nil } -// Create an object in the db with options: WithOplog, NewOplogMsg and -// WithLookup. WithOplog will write an oplog entry for the create. +// Create an object in the db with options: WithOplog, NewOplogMsg, WithLookup +// and OnConflict. WithOplog will write an oplog entry for the create. // NewOplogMsg will return in-memory oplog message. WithOplog and NewOplogMsg // cannot be used together. WithLookup with to force a lookup after create. +// OnConflict specifies alternative actions to take when an insert results in a +// unique constraint or exclusion constraint error. func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { const op = "db.Create" if rw.underlying == nil { @@ -295,6 +298,54 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { } } } + + db := rw.underlying.WithContext(ctx) + var onConflictDoNothing bool + if opts.withOnConflict != nil { + c := clause.OnConflict{} + switch opts.withOnConflict.Target.(type) { + case Constraint: + c.OnConstraint = string(opts.withOnConflict.Target.(Constraint)) + case Columns: + columns := make([]clause.Column, 0, len(opts.withOnConflict.Target.(Columns))) + for _, name := range opts.withOnConflict.Target.(Columns) { + columns = append(columns, clause.Column{Name: name}) + } + c.Columns = columns + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid conflict target: %v", reflect.TypeOf(opts.withOnConflict.Target))) + } + + switch opts.withOnConflict.Action.(type) { + case DoNothing: + c.DoNothing = true + onConflictDoNothing = true + case UpdateAll: + c.UpdateAll = true + case []ColumnValue: + updates := opts.withOnConflict.Action.([]ColumnValue) + set := make(clause.Set, 0, len(updates)) + for _, s := range updates { + // make sure it's not one of the std immutable columns + if contains([]string{"createtime", "publicid"}, strings.ToLower(s.column)) { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("Cannot do update on conflict for column %s", s.column)) + } + switch sv := s.value.(type) { + case column: + set = append(set, sv.toAssignment(s.column)) + case ExprValue: + set = append(set, sv.toAssignment(s.column)) + default: + set = append(set, rawAssignment(s.column, s.value)) + } + } + c.DoUpdates = set + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid conflict action: %v", reflect.TypeOf(opts.withOnConflict.Action))) + } + db = db.Clauses(c) + } + var ticket *store.Ticket if withOplog { var err error @@ -303,20 +354,32 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } } - if err := rw.underlying.Create(i).Error; err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("create failed"), errors.WithoutEvent()) + tx := db.Create(i) + if tx.Error != nil { + return errors.Wrap(ctx, tx.Error, op, errors.WithMsg("create failed"), errors.WithoutEvent()) + } + if opts.withRowsAffected != nil { + *opts.withRowsAffected = tx.RowsAffected } if withOplog { - if err := rw.addOplog(ctx, CreateOp, opts, ticket, i); err != nil { - return errors.Wrap(ctx, err, op) + switch { + case onConflictDoNothing && tx.RowsAffected == 0: + default: + if err := rw.addOplog(ctx, CreateOp, opts, ticket, i); err != nil { + return errors.Wrap(ctx, err, op) + } } } if opts.newOplogMsg != nil { - msg, err := rw.newOplogMessage(ctx, CreateOp, i) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("returning oplog failed")) + switch { + case onConflictDoNothing && tx.RowsAffected == 0: + default: + msg, err := rw.newOplogMessage(ctx, CreateOp, i) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("returning oplog failed")) + } + *opts.newOplogMsg = *msg } - *opts.newOplogMsg = *msg } if err := rw.lookupAfterWrite(ctx, i, opt...); err != nil { return errors.Wrap(ctx, err, op, errors.WithoutEvent()) diff --git a/internal/db/read_writer_ext_test.go b/internal/db/read_writer_ext_test.go new file mode 100644 index 000000000..889ecdaaa --- /dev/null +++ b/internal/db/read_writer_ext_test.go @@ -0,0 +1,152 @@ +package db_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/db_test" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestDb_Create_OnConflict(t *testing.T) { + ctx := context.Background() + wrapper := db.TestWrapper(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + // create initial user for on conflict tests + id, err := db.NewPublicId("test-user") + require.NoError(t, err) + initialUser, err := db_test.NewTestUser() + require.NoError(t, err) + ts := ×tamp.Timestamp{Timestamp: timestamppb.Now()} + initialUser.CreateTime = ts + initialUser.UpdateTime = ts + initialUser.Name = "foo-" + id + err = rw.Create(ctx, initialUser) + require.NoError(t, err) + assert.NotEmpty(t, initialUser.Id) + + tests := []struct { + name string + onConflict db.OnConflict + wantUpdate bool + wantEmail string + wantOplog bool + }{ + { + name: "set-columns", + onConflict: db.OnConflict{ + Target: db.Columns{"public_id"}, + Action: db.SetColumns([]string{"name"}), + }, + wantUpdate: true, + wantOplog: true, + }, + { + name: "set-column-values", + onConflict: db.OnConflict{ + Target: db.Columns{"public_id"}, + Action: db.SetColumnValues(map[string]interface{}{ + "name": db.Expr("md5(?)", "alice eve smith"), + "email": "alice@gmail.com", + "phone_number": db.Expr("NULL"), + }), + }, + wantUpdate: true, + wantEmail: "alice@gmail.com", + wantOplog: true, + }, + { + name: "both-set-columns-and-set-column-values", + onConflict: func() db.OnConflict { + onConflict := db.OnConflict{ + Target: db.Columns{"public_id"}, + } + cv := db.SetColumns([]string{"name"}) + cv = append(cv, + db.SetColumnValues(map[string]interface{}{ + "email": "alice@gmail.com", + "phone_number": db.Expr("NULL"), + })...) + onConflict.Action = cv + return onConflict + }(), + wantUpdate: true, + wantEmail: "alice@gmail.com", + wantOplog: true, + }, + { + name: "update-all", + onConflict: db.OnConflict{ + Target: db.Columns{"public_id"}, + Action: db.UpdateAll(true), + }, + wantUpdate: true, + wantOplog: true, + }, + { + name: "do-nothing", + onConflict: db.OnConflict{ + Target: db.Columns{"public_id"}, + Action: db.DoNothing(true), + }, + wantOplog: false, + }, + { + name: "on-constraint", + onConflict: db.OnConflict{ + Target: db.Constraint("db_test_user_public_id_key"), + Action: db.SetColumns([]string{"name"}), + }, + wantUpdate: true, + wantOplog: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conflictUser, err := db_test.NewTestUser() + require.NoError(err) + userNameId, err := db.NewPublicId("test-user-name") + require.NoError(err) + conflictUser.PublicId = initialUser.PublicId + conflictUser.Name = userNameId + md := oplog.Metadata{ + "resource-public-id": []string{conflictUser.PublicId}, + "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, + } + var rowsAffected int64 + err = rw.Create(ctx, conflictUser, db.WithOnConflict(&tt.onConflict), db.WithOplog(wrapper, md), db.WithReturnRowsAffected(&rowsAffected)) + require.NoError(err) + if tt.wantOplog { + err = db.TestVerifyOplog(t, rw, conflictUser.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + } + foundUser, err := db_test.NewTestUser() + require.NoError(err) + foundUser.PublicId = conflictUser.PublicId + err = rw.LookupByPublicId(context.Background(), foundUser) + require.NoError(err) + if tt.wantUpdate { + assert.Equal(int64(1), rowsAffected) + assert.Equal(conflictUser.Id, foundUser.Id) + assert.Equal(conflictUser.Name, foundUser.Name) + if tt.wantEmail != "" { + assert.Equal(tt.wantEmail, foundUser.Email) + } + } else { + assert.Equal(int64(0), rowsAffected) + assert.NotEqual(conflictUser.Id, foundUser.Id) + assert.NotEqual(conflictUser.Name, foundUser.Name) + } + }) + } +} diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 14404793c..793663dd4 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -723,8 +723,8 @@ func TestDb_Create(t *testing.T) { metadata := oplog.Metadata{ "resource-public-id": []string{user.PublicId}, - // "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, } + err = w.WriteOplogEntryWith(context.Background(), TestWrapper(t), ticket, metadata, []*oplog.Message{&createMsg, &updateMsg}) require.NoError(err) diff --git a/internal/db/schema/migrations/oss/postgres/17/01_servers.up.sql b/internal/db/schema/migrations/oss/postgres/17/01_servers.up.sql new file mode 100644 index 000000000..4b84e5c9c --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/17/01_servers.up.sql @@ -0,0 +1,16 @@ +begin; + +-- server table inserts/updates historically included setting the update_time +-- column to now(). Since we're now using the "standard" db package to do this, +-- we need to add triggers to set the update_time for every insert/update. +create trigger + update_time_column +before update on server + for each row execute procedure update_time_column(); + +create trigger + insert_time_column +before insert on server + for each row execute procedure update_time_column(); + +commit; \ No newline at end of file diff --git a/internal/proto/local/controller/servers/v1/servers.proto b/internal/proto/local/controller/servers/v1/servers.proto index 7adf4bbb3..ecb426497 100644 --- a/internal/proto/local/controller/servers/v1/servers.proto +++ b/internal/proto/local/controller/servers/v1/servers.proto @@ -17,6 +17,7 @@ message Server { // 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]; // Description of the resource diff --git a/internal/servers/query.go b/internal/servers/query.go index 1e814a853..c0f865a17 100644 --- a/internal/servers/query.go +++ b/internal/servers/query.go @@ -1,18 +1,6 @@ package servers const ( - serverUpsertQuery = ` - insert into server - (private_id, type, description, address, update_time) - values - (@private_id, @type, @description, @address, now()) - on conflict on constraint server_pkey - do update set - type = @type, - description = @description, - address = @address, - update_time = now(); - ` deleteWhereCreateTimeSql = `create_time < ?` deleteTagsSql = `server_id = ?` ) diff --git a/internal/servers/repository.go b/internal/servers/repository.go index 2634e87a7..1a5a49639 100644 --- a/internal/servers/repository.go +++ b/internal/servers/repository.go @@ -2,7 +2,6 @@ package servers import ( "context" - "database/sql" "fmt" "time" @@ -138,7 +137,7 @@ func (r *Repository) UpsertServer(ctx context.Context, server *Server, opt ...Op opts := getOpts(opt...) - var rowsUpdated int + var rowsUpdated int64 var controllers []*Server _, err := r.writer.DoTx( ctx, @@ -146,14 +145,11 @@ func (r *Repository) UpsertServer(ctx context.Context, server *Server, opt ...Op db.ExpBackoff{}, func(read db.Reader, w db.Writer) error { var err error - rowsUpdated, err = w.Exec(ctx, - serverUpsertQuery, - []interface{}{ - sql.Named("private_id", server.PrivateId), - sql.Named("type", server.Type), - sql.Named("description", server.Description), - sql.Named("address", server.Address), - }) + 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") } @@ -208,7 +204,7 @@ func (r *Repository) UpsertServer(ctx context.Context, server *Server, opt ...Op return nil, db.NoRowsAffected, err } - return controllers, rowsUpdated, nil + return controllers, int(rowsUpdated), nil } type RecoveryNonce struct { diff --git a/internal/servers/servers.pb.go b/internal/servers/servers.pb.go index b5fad0c29..cd07d8563 100644 --- a/internal/servers/servers.pb.go +++ b/internal/servers/servers.pb.go @@ -35,9 +35,10 @@ type Server struct { // 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"` + 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