diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31db2febe..2fc056e76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: echo "::set-output name=product-minor-version::$MINOR_VERSION" verify-product-metadata: - needs: + needs: - set-product-version - product-metadata runs-on: ubuntu-latest @@ -422,7 +422,7 @@ jobs: uses: actions/cache@v3 with: path: /tmp/bats-cli-ui-deps - key: enos-bats-cli-ui-deps-jq-1.6-password-store-1.7.4 + key: enos-bats-cli-ui-deps-jq-1.6-password-store-1.7.4-vault-1.12.2 - name: Set up Node for Bats install uses: actions/setup-node@v3 with: @@ -453,6 +453,13 @@ jobs: cd /tmp/bats-cli-ui-deps/pass/password-store-1.7.4 sudo make install pass init ${{ secrets.ENOS_GPG_UID }} + - name: Download Vault AMD64 binary for integration testing + if: steps.dep-cache.outputs.cache-hit != 'true' + run: | + wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/bats-cli-ui-deps/vault.zip + - name: Install Vault + run: | + unzip /tmp/bats-cli-ui-deps/vault.zip -d /usr/local/bin - name: Download Linux AMD64 Boundary bundle id: download uses: actions/download-artifact@v3 @@ -479,6 +486,16 @@ jobs: bash --version echo "boundary version:" boundary version + echo "vault version:" + vault version - name: Run cli bats tests run: | + export VAULT_ADDR=http://127.0.0.1:8200 + export VAULT_TOKEN=boundarytok + make -C internal/tests/cli test-vault-up + until vault status; do docker container inspect boundary-cli-tests-vault &> /dev/null || exit -1; sleep 1; done make test-cli + - name: Cleanup + if: success() || failure() + run: | + make -C internal/tests/cli test-vault-down diff --git a/CHANGELOG.md b/CHANGELOG.md index 925e0d18d..fcbbbda23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,22 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. 0.14.0, this will become an error instead. As a consequence, it means that the fallback logic for targets that did not have a default port defined is no longer in service; all targets must now have a default port defined. +* With the introduction of `vault-ssh-certificate` credential libraries, the + `vault` credential library subtype is being renamed to `vault-generic` to + denote it as a credential library that can be used in a generalized way to + issue credentials from vault. Existing credential libraries with the + subtype of `vault` will be updated to `vault-generic`. The subtype of + `vault` will still be accepted as a valid subtype in API requests to the + credential libraries endpoints, but is deprecated. Instead `vault-generic` + should be used. In addition the `boundary credential-libraries create + vault` and `boundary credential-libraries update vault` subcommands will + still function, but are deprecated. Instead `boundary credential-libraries + create vault-generic` and `boundary credential-libraries update + vault-generic` should be used. Also note that any credential library created + using the subtype of `vault`, either via the API or via the deprecated + subcommand, will have the subtype set to `vault-generic`. The deprecated + subtype and subcommands will be removed in boundary 0.14.0, at which point + `vault-generic` must be used. ### New and Improved @@ -39,6 +55,10 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. multiple workers together to access services hidden under layers of network security. Multi-hop workers can also establish a TCP session through multiple workers, with the ability to reverse proxy and establish a connection. +* Vault SSH certificate credential library: A new credential library that uses + the vault ssh secret engine to generate ssh private key and certificates. The + library can be used as an injected application credential source for targets + that support credential injection. ([PR](https://github.com/hashicorp/boundary/pull/2860)) ### Bug Fixes diff --git a/api/credentiallibraries/credential_library.gen.go b/api/credentiallibraries/credential_library.gen.go index 4eb7d8cee..ba315aa1c 100644 --- a/api/credentiallibraries/credential_library.gen.go +++ b/api/credentiallibraries/credential_library.gen.go @@ -90,7 +90,7 @@ func (c *Client) ApiClient() *api.Client { return c.client } -func (c *Client) Create(ctx context.Context, credentialStoreId string, opt ...Option) (*CredentialLibraryCreateResult, error) { +func (c *Client) Create(ctx context.Context, resourceType string, credentialStoreId string, opt ...Option) (*CredentialLibraryCreateResult, error) { if credentialStoreId == "" { return nil, fmt.Errorf("empty credentialStoreId value passed into Create request") } @@ -100,6 +100,11 @@ func (c *Client) Create(ctx context.Context, credentialStoreId string, opt ...Op if c.client == nil { return nil, fmt.Errorf("nil client") } + if resourceType == "" { + return nil, fmt.Errorf("empty resourceType value passed into Create request") + } else { + opts.postMap["type"] = resourceType + } opts.postMap["credential_store_id"] = credentialStoreId diff --git a/api/credentiallibraries/option.gen.go b/api/credentiallibraries/option.gen.go index c63590351..12ba34836 100644 --- a/api/credentiallibraries/option.gen.go +++ b/api/credentiallibraries/option.gen.go @@ -110,6 +110,30 @@ func DefaultCredentialType() Option { } } +func WithVaultSSHCertificateCredentialLibraryCriticalOptions(inCriticalOptions map[string]string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["critical_options"] = inCriticalOptions + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryCriticalOptions() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["critical_options"] = nil + o.postMap["attributes"] = val + } +} + func WithDescription(inDescription string) Option { return func(o *options) { o.postMap["description"] = inDescription @@ -122,6 +146,30 @@ func DefaultDescription() Option { } } +func WithVaultSSHCertificateCredentialLibraryExtensions(inExtensions map[string]string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["extensions"] = inExtensions + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryExtensions() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["extensions"] = nil + o.postMap["attributes"] = val + } +} + func WithVaultCredentialLibraryHttpMethod(inHttpMethod string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -170,6 +218,78 @@ func DefaultVaultCredentialLibraryHttpRequestBody() Option { } } +func WithVaultSSHCertificateCredentialLibraryKeyBits(inKeyBits uint32) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_bits"] = inKeyBits + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyBits() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_bits"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryKeyId(inKeyId string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_id"] = inKeyId + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyId() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_id"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryKeyType(inKeyType string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_type"] = inKeyType + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryKeyType() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["key_type"] = nil + o.postMap["attributes"] = val + } +} + func WithName(inName string) Option { return func(o *options) { o.postMap["name"] = inName @@ -193,3 +313,51 @@ func WithVaultCredentialLibraryPath(inPath string) Option { o.postMap["attributes"] = val } } + +func WithVaultSSHCertificateCredentialLibraryPath(inPath string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["path"] = inPath + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryTtl(inTtl string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["ttl"] = inTtl + o.postMap["attributes"] = val + } +} + +func DefaultVaultSSHCertificateCredentialLibraryTtl() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["ttl"] = nil + o.postMap["attributes"] = val + } +} + +func WithVaultSSHCertificateCredentialLibraryUsername(inUsername string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["username"] = inUsername + o.postMap["attributes"] = val + } +} diff --git a/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go b/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go new file mode 100644 index 000000000..534e6f2f2 --- /dev/null +++ b/api/credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go @@ -0,0 +1,44 @@ +// Code generated by "make api"; DO NOT EDIT. +package credentiallibraries + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type VaultSSHCertificateCredentialLibraryAttributes struct { + Path string `json:"path,omitempty"` + Username string `json:"username,omitempty"` + KeyType string `json:"key_type,omitempty"` + KeyBits uint32 `json:"key_bits,omitempty"` + Ttl string `json:"ttl,omitempty"` + KeyId string `json:"key_id,omitempty"` + CriticalOptions map[string]string `json:"critical_options,omitempty"` + Extensions map[string]string `json:"extensions,omitempty"` +} + +func AttributesMapToVaultSSHCertificateCredentialLibraryAttributes(in map[string]interface{}) (*VaultSSHCertificateCredentialLibraryAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out VaultSSHCertificateCredentialLibraryAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *CredentialLibrary) GetVaultSSHCertificateCredentialLibraryAttributes() (*VaultSSHCertificateCredentialLibraryAttributes, error) { + if pt.Type != "vaultsshcertificate" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential-library is of type %s", "vaultsshcertificate", pt.Type) + } + return AttributesMapToVaultSSHCertificateCredentialLibraryAttributes(pt.Attributes) +} diff --git a/go.mod b/go.mod index 6dd186b56..68ab294b9 100644 --- a/go.mod +++ b/go.mod @@ -161,6 +161,7 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 8966004b2..202734ace 100644 --- a/go.sum +++ b/go.sum @@ -980,6 +980,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index ad660b226..807cebb10 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -518,12 +518,58 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &credentiallibraries.VaultSSHCertificateCredentialLibraryAttributes{}, + outFile: "credentiallibraries/vault_ssh_certificate_credential_library_attributes.gen.go", + subtypeName: "VaultSSHCertificateCredentialLibrary", + fieldOverrides: []fieldInfo{ + { + Name: "Path", + SkipDefault: true, + }, + { + Name: "Username", + SkipDefault: true, + }, + { + Name: "CriticalOptions", + FieldType: "map[string]string", + }, + { + Name: "Extensions", + FieldType: "map[string]string", + }, + }, + parentTypeName: "CredentialLibrary", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentiallibraries.CredentialLibrary{}, outFile: "credentiallibraries/credential_library.gen.go", templates: []*template.Template{ clientTemplate, - commonCreateTemplate, + template.Must(template.New("").Funcs( + template.FuncMap{ + "snakeCase": snakeCase, + "funcName": func() string { + return "Create" + }, + "apiAction": func() string { + return "" + }, + "extraRequiredParams": func() []requiredParam { + return []requiredParam{ + { + Name: "resourceType", + Typ: "string", + PostType: "type", + }, + } + }, + }, + ).Parse(createTemplateStr)), readTemplate, updateTemplate, deleteTemplate, diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 27a0d473c..3b3066e4d 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -364,6 +364,18 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "credential-libraries create vault-generic": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, + "credential-libraries create vault-ssh-certificate": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultSshCertificateCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "credential-libraries update": func() (cli.Command, error) { return &credentiallibrariescmd.Command{ Command: base.NewCommand(ui), @@ -371,7 +383,19 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, nil }, "credential-libraries update vault": func() (cli.Command, error) { - return &credentiallibrariescmd.VaultCommand{ + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "credential-libraries update vault-generic": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultGenericCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "credential-libraries update vault-ssh-certificate": func() (cli.Command, error) { + return &credentiallibrariescmd.VaultSshCertificateCommand{ Command: base.NewCommand(ui), Func: "update", }, nil diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index e6a3efa1b..b131536b9 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -140,6 +140,16 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo nonAttributeMap["Type"] = item.Type } + var keySubstMap map[string]string + switch item.Type { + case "vault": + fallthrough + case "vault-generic": + keySubstMap = genericKeySubstMap + case "vault-ssh-certificate": + keySubstMap = sshCertKeySubstMap + } + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ @@ -189,8 +199,19 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo return base.WrapForHelpText(ret) } -var keySubstMap = map[string]string{ +var genericKeySubstMap = map[string]string{ "path": "Path", "http_method": "HTTP Method", "http_request_body": "HTTP Request Body", } + +var sshCertKeySubstMap = map[string]string{ + "path": "Path", + "username": "Username", + "key_type": "Key Type", + "key_bits": "Key Bits", + "ttl": "TTL", + "key_id": "Key ID", + "critical_options": "Critical Options", + "extensions": "Extensions", +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go new file mode 100644 index 000000000..0aaa8b27d --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-generic_credentiallibraries.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package credentiallibrariescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initVaultGenericFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultGenericActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultGenericMap[k] = append(flagsVaultGenericMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultGenericCommand)(nil) + _ cli.CommandAutocomplete = (*VaultGenericCommand)(nil) +) + +type VaultGenericCommand struct { + *base.Command + + Func string + + plural string + + extraVaultGenericCmdVars +} + +func (c *VaultGenericCommand) AutocompleteArgs() complete.Predictor { + initVaultGenericFlags() + return complete.PredictAnything +} + +func (c *VaultGenericCommand) AutocompleteFlags() complete.Flags { + initVaultGenericFlags() + return c.Flags().Completions() +} + +func (c *VaultGenericCommand) Synopsis() string { + if extra := extraVaultGenericSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-generic-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultGenericCommand) Help() string { + initVaultGenericFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultGenericHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultGenericMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultGenericCommand) Flags() *base.FlagSets { + if len(flagsVaultGenericMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "vault-generic-type credential library", flagsVaultGenericMap, c.Func) + + extraVaultGenericFlagsFunc(c, set, f) + + return set +} + +func (c *VaultGenericCommand) Run(args []string) int { + initVaultGenericFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-generic-type credential library" + switch c.Func { + case "list": + c.plural = "vault-generic-type credential librarys" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultGenericMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentiallibraries.Option + + if strutil.StrListContains(flagsVaultGenericMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentiallibrariesClient := credentiallibraries.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultName()) + default: + opts = append(opts, credentiallibraries.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultDescription()) + default: + opts = append(opts, credentiallibraries.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentiallibraries.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraVaultGenericFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentiallibraries.CredentialLibrary + + var createResult *credentiallibraries.CredentialLibraryCreateResult + + var updateResult *credentiallibraries.CredentialLibraryUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentiallibrariesClient.Create(c.Context, "vault-generic", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentiallibrariesClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraVaultGenericActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultGenericActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *VaultGenericCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraVaultGenericActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultGenericSynopsisFunc = func(*VaultGenericCommand) string { return "" } + extraVaultGenericFlagsFunc = func(*VaultGenericCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultGenericFlagsHandlingFunc = func(*VaultGenericCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultGenericActions = func(_ *VaultGenericCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultGenericActionOutput = func(*VaultGenericCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go new file mode 100644 index 000000000..b2192b02b --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-generic_funcs.go @@ -0,0 +1,174 @@ +package credentiallibrariescmd + +import ( + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" +) + +func init() { + extraVaultGenericFlagsFunc = extraVaultGenericFlagsFuncImpl + extraVaultGenericActionsFlagsMapFunc = extraVaultGenericActionsFlagsMapFuncImpl + extraVaultGenericFlagsHandlingFunc = extraVaultGenericFlagHandlingFuncImpl +} + +const ( + pathFlagName = "vault-path" + httpMethodFlagName = "vault-http-method" + httpRequestBodyFlagName = "vault-http-request-body" + credentialTypeFlagName = "credential-type" + credentialMappingFlagName = "credential-mapping-override" +) + +type extraVaultGenericCmdVars struct { + flagPath string + flagHttpMethod string + flagHttpRequestBody string + flagCredentialType string + flagCredentialMapping []base.CombinedSliceFlagValue +} + +func extraVaultGenericActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialTypeFlagName, + credentialMappingFlagName, + }, + "update": { + pathFlagName, + httpMethodFlagName, + httpRequestBodyFlagName, + credentialMappingFlagName, + }, + } + return flags +} + +func extraVaultGenericFlagsFuncImpl(c *VaultGenericCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault Credential Library Options") + + for _, name := range flagsVaultGenericMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + case httpMethodFlagName: + f.StringVar(&base.StringVar{ + Name: httpMethodFlagName, + Target: &c.flagHttpMethod, + Usage: "The http method the library should use when communicating with vault.", + }) + case httpRequestBodyFlagName: + f.StringVar(&base.StringVar{ + Name: httpRequestBodyFlagName, + Target: &c.flagHttpRequestBody, + Usage: "The http request body the library uses to communicate with vault. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", + }) + case credentialTypeFlagName: + f.StringVar(&base.StringVar{ + Name: credentialTypeFlagName, + Target: &c.flagCredentialType, + Usage: "The type of credential this library will issue, defaults to Unspecified.", + }) + case credentialMappingFlagName: + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: credentialMappingFlagName, + Target: &c.flagCredentialMapping, + KvSplit: true, + Usage: "The credential mapping override.", + }) + } + } +} + +func extraVaultGenericFlagHandlingFuncImpl(c *VaultGenericCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryPath(c.flagPath)) + } + switch c.flagHttpMethod { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultCredentialLibraryHttpMethod()) + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryHttpMethod(c.flagHttpMethod)) + } + switch c.flagHttpRequestBody { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultCredentialLibraryHttpRequestBody()) + default: + rb, _ := parseutil.ParsePath(c.flagHttpRequestBody) + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryHttpRequestBody(rb)) + } + switch c.flagCredentialType { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultCredentialType()) + default: + *opts = append(*opts, credentiallibraries.WithCredentialType(c.flagCredentialType)) + } + switch len(c.flagCredentialMapping) { + case 0: + case 1: + if len(c.flagCredentialMapping[0].Keys) == 1 && c.flagCredentialMapping[0].Keys[0] == "null" && c.flagCredentialMapping[0].Value == nil { + *opts = append(*opts, credentiallibraries.DefaultCredentialMappingOverrides()) + break + } + fallthrough + default: + mappings := make(map[string]any, len(c.flagCredentialMapping)) + for _, mapping := range c.flagCredentialMapping { + switch { + case len(mapping.Keys) != 1 || mapping.Keys[0] == "" || mapping.Value == nil || mapping.Value.GetValue() == "": + // mapping override does not support key segments (e.g. 'x.y=z') + c.UI.Error("Credential mapping override must be in the format 'key=value', 'key=null' to clear field or 'null' to clear all.") + return false + case mapping.Value.GetValue() == "null": + // user provided 'key=null' indicating the field specific override should + // be cleared, set map value to nil + mappings[mapping.Keys[0]] = nil + default: + mappings[mapping.Keys[0]] = mapping.Value.GetValue() + } + } + *opts = append(*opts, credentiallibraries.WithCredentialMappingOverrides(mappings)) + } + + return true +} + +func (c *VaultGenericCommand) extraVaultGenericHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault -credential-store-id [options] [args]", + "", + " Create a vault-generic-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-generic -credential-store-id csvlt_1234567890 -vault-path "/some/path"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault [options] [args]", + "", + " Update a vault-generic-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-generic -id clvlt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go new file mode 100644 index 000000000..bdc891939 --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_credentiallibraries.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package credentiallibrariescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initVaultSshCertificateFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultSshCertificateActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultSshCertificateMap[k] = append(flagsVaultSshCertificateMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultSshCertificateCommand)(nil) + _ cli.CommandAutocomplete = (*VaultSshCertificateCommand)(nil) +) + +type VaultSshCertificateCommand struct { + *base.Command + + Func string + + plural string + + extraVaultSshCertificateCmdVars +} + +func (c *VaultSshCertificateCommand) AutocompleteArgs() complete.Predictor { + initVaultSshCertificateFlags() + return complete.PredictAnything +} + +func (c *VaultSshCertificateCommand) AutocompleteFlags() complete.Flags { + initVaultSshCertificateFlags() + return c.Flags().Completions() +} + +func (c *VaultSshCertificateCommand) Synopsis() string { + if extra := extraVaultSshCertificateSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-ssh-certificate-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultSshCertificateCommand) Help() string { + initVaultSshCertificateFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultSshCertificateHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultSshCertificateMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultSshCertificateCommand) Flags() *base.FlagSets { + if len(flagsVaultSshCertificateMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "vault-ssh-certificate-type credential library", flagsVaultSshCertificateMap, c.Func) + + extraVaultSshCertificateFlagsFunc(c, set, f) + + return set +} + +func (c *VaultSshCertificateCommand) Run(args []string) int { + initVaultSshCertificateFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-ssh-certificate-type credential library" + switch c.Func { + case "list": + c.plural = "vault-ssh-certificate-type credential librarys" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultSshCertificateMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentiallibraries.Option + + if strutil.StrListContains(flagsVaultSshCertificateMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentiallibrariesClient := credentiallibraries.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultName()) + default: + opts = append(opts, credentiallibraries.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultDescription()) + default: + opts = append(opts, credentiallibraries.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentiallibraries.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraVaultSshCertificateFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentiallibraries.CredentialLibrary + + var createResult *credentiallibraries.CredentialLibraryCreateResult + + var updateResult *credentiallibraries.CredentialLibraryUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentiallibrariesClient.Create(c.Context, "vault-ssh-certificate", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentiallibrariesClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraVaultSshCertificateActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultSshCertificateActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *VaultSshCertificateCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraVaultSshCertificateActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultSshCertificateSynopsisFunc = func(*VaultSshCertificateCommand) string { return "" } + extraVaultSshCertificateFlagsFunc = func(*VaultSshCertificateCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultSshCertificateFlagsHandlingFunc = func(*VaultSshCertificateCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultSshCertificateActions = func(_ *VaultSshCertificateCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultSshCertificateActionOutput = func(*VaultSshCertificateCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go new file mode 100644 index 000000000..23d588cab --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ssh-certificate_funcs.go @@ -0,0 +1,264 @@ +package credentiallibrariescmd + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" +) + +func init() { + extraVaultSshCertificateFlagsFunc = extraVaultSshCertificateFlagsFuncImpl + extraVaultSshCertificateActionsFlagsMapFunc = extraVaultSshCertificateActionsFlagsMapFuncImpl + extraVaultSshCertificateFlagsHandlingFunc = extraVaultSshCertificateFlagHandlingFuncImpl +} + +const ( + usernameName = "username" + keyTypeName = "key-type" + keyBitsName = "key-bits" + ttlName = "ttl" + keyIdName = "key-id" + criticalOptionsName = "critical-options" + piecewiseCriticalOptionsName = "critical-option" + extensionsName = "extensions" + piecewiseExtensionName = "extension" +) + +type extraVaultSshCertificateCmdVars struct { + flagPath string + flagUsername string + flagKeyType string + flagKeyBits string + flagTtl string + flagKeyId string + flagCriticalOptions string + flagCriticalOpts []base.CombinedSliceFlagValue + flagExtensions string + flagExtens []base.CombinedSliceFlagValue +} + +func extraVaultSshCertificateActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + usernameName, + keyTypeName, + keyBitsName, + ttlName, + keyIdName, + criticalOptionsName, + piecewiseCriticalOptionsName, + extensionsName, + piecewiseExtensionName, + }, + "update": { + pathFlagName, + usernameName, + keyTypeName, + keyBitsName, + ttlName, + keyIdName, + criticalOptionsName, + piecewiseCriticalOptionsName, + extensionsName, + piecewiseExtensionName, + }, + } + return flags +} + +func extraVaultSshCertificateFlagsFuncImpl(c *VaultSshCertificateCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault SSH Certificate Credential Library Options") + + for _, name := range flagsVaultSshCertificateMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + case usernameName: + f.StringVar(&base.StringVar{ + Name: usernameName, + Target: &c.flagUsername, + Usage: "The username to use with the ssh certificate.", + }) + case keyTypeName: + f.StringVar(&base.StringVar{ + Name: keyTypeName, + Target: &c.flagKeyType, + Usage: "The key type for the generated ssh private key. One of: ed25519, ecdsa, rsa.", + }) + case keyBitsName: + f.StringVar(&base.StringVar{ + Name: keyBitsName, + Target: &c.flagKeyBits, + Usage: "The number of bits when generating the ssh private key. Depends on key_type. If ed25519 this should not be set, or set to 0, if ecdsa one of 256, 384, 521, if rsa one of 2048, 3072, 4096.", + }) + case ttlName: + f.StringVar(&base.StringVar{ + Name: ttlName, + Target: &c.flagTtl, + Usage: "The time-to-live for the generated certificate.", + }) + case keyIdName: + f.StringVar(&base.StringVar{ + Name: keyIdName, + Target: &c.flagKeyId, + Usage: "The key id that the created certificate should have.", + }) + } + } + criticalOptsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsVaultSshCertificateMap[c.Func], + FullPopulationFlag: &c.flagCriticalOptions, + FullPopulationInputName: criticalOptionsName, + PiecewisePopulationFlag: &c.flagCriticalOpts, + PiecewisePopulationInputBaseName: piecewiseCriticalOptionsName, + PiecewiseNoProtoCompat: true, + } + common.PopulateCombinedSliceFlagValue(criticalOptsInput) + + extensionsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsVaultSshCertificateMap[c.Func], + FullPopulationFlag: &c.flagExtensions, + FullPopulationInputName: extensionsName, + PiecewisePopulationFlag: &c.flagExtens, + PiecewisePopulationInputBaseName: piecewiseExtensionName, + PiecewiseNoProtoCompat: true, + } + common.PopulateCombinedSliceFlagValue(extensionsInput) +} + +func extraVaultSshCertificateFlagHandlingFuncImpl(c *VaultSshCertificateCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultCredentialLibraryPath(c.flagPath)) + } + switch c.flagUsername { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryUsername(c.flagUsername)) + } + switch c.flagKeyType { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyType()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyType(c.flagKeyType)) + } + switch c.flagKeyBits { + case "": + case "0", "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyBits()) + default: + var final uint32 + keyBits, err := strconv.ParseUint(c.flagKeyBits, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagKeyBits, err)) + return false + } + final = uint32(keyBits) + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyBits(final)) + } + switch c.flagTtl { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryTtl()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryTtl(c.flagTtl)) + } + switch c.flagKeyId { + case "": + case "null": + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryKeyId()) + default: + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyId(c.flagKeyId)) + } + + if err := common.HandleAttributeFlags( + c.Command, + piecewiseCriticalOptionsName, + c.flagCriticalOptions, + c.flagCriticalOpts, + func() { + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryCriticalOptions()) + }, + func(in map[string]any) { + inn := make(map[string]string, len(in)) + for k, v := range in { + switch vv := v.(type) { + case nil: + inn[k] = "" + case string: + inn[k] = vv + default: + continue + } + } + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryCriticalOptions(inn)) + }); err != nil { + return false + } + if err := common.HandleAttributeFlags( + c.Command, + piecewiseExtensionName, + c.flagExtensions, + c.flagExtens, + func() { + *opts = append(*opts, credentiallibraries.DefaultVaultSSHCertificateCredentialLibraryExtensions()) + }, + func(in map[string]any) { + inn := make(map[string]string, len(in)) + for k, v := range in { + switch vv := v.(type) { + case nil: + inn[k] = "" + case string: + inn[k] = vv + default: + continue + } + } + *opts = append(*opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryExtensions(inn)) + }); err != nil { + return false + } + + return true +} + +func (c *VaultSshCertificateCommand) extraVaultSshCertificateHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault-ssh-certificate -credential-store-id [options] [args]", + "", + " Create a vault-ssh-certificate-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-ssh-certificate -credential-store-id csvlt_1234567890 -vault-path "/ssh/sign/role" -username user`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault-ssh-certificate [options] [args]", + "", + " Update a vault-ssh-certificate-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-ssh-certificate -id clvsclt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go index 84a176816..7fdb932eb 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_credentiallibraries.gen.go @@ -203,7 +203,7 @@ func (c *VaultCommand) Run(args []string) int { switch c.Func { case "create": - createResult, err = credentiallibrariesClient.Create(c.Context, c.FlagCredentialStoreId, opts...) + createResult, err = credentiallibrariesClient.Create(c.Context, "vault", c.FlagCredentialStoreId, opts...) if exitCode := c.checkFuncError(err); exitCode > 0 { return exitCode } diff --git a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go index d066a2cde..867e0e85c 100644 --- a/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/vault_funcs.go @@ -12,39 +12,10 @@ func init() { extraVaultFlagsHandlingFunc = extraVaultFlagHandlingFuncImpl } -const ( - pathFlagName = "vault-path" - httpMethodFlagName = "vault-http-method" - httpRequestBodyFlagName = "vault-http-request-body" - credentialTypeFlagName = "credential-type" - credentialMappingFlagName = "credential-mapping-override" -) - -type extraVaultCmdVars struct { - flagPath string - flagHttpMethod string - flagHttpRequestBody string - flagCredentialType string - flagCredentialMapping []base.CombinedSliceFlagValue -} +type extraVaultCmdVars extraVaultGenericCmdVars func extraVaultActionsFlagsMapFuncImpl() map[string][]string { - flags := map[string][]string{ - "create": { - pathFlagName, - httpMethodFlagName, - httpRequestBodyFlagName, - credentialTypeFlagName, - credentialMappingFlagName, - }, - "update": { - pathFlagName, - httpMethodFlagName, - httpRequestBodyFlagName, - credentialMappingFlagName, - }, - } - return flags + return extraVaultGenericActionsFlagsMapFuncImpl() } func extraVaultFlagsFuncImpl(c *VaultCommand, set *base.FlagSets, _ *base.FlagSet) { @@ -150,6 +121,7 @@ func (c *VaultCommand) extraVaultHelpFunc(_ map[string]func() string) string { switch c.Func { case "create": helpStr = base.WrapForHelpText([]string{ + "Deprecated: use 'boundary credential-libraries create vault-generic' instead.", "Usage: boundary credential-libraries create vault -credential-store-id [options] [args]", "", " Create a vault-type credential library. Example:", @@ -161,6 +133,7 @@ func (c *VaultCommand) extraVaultHelpFunc(_ map[string]func() string) string { case "update": helpStr = base.WrapForHelpText([]string{ + "Deprecated: use 'boundary credential-libraries update vault-generic' instead.", "Usage: boundary credential-libraries update vault [options] [args]", "", " Update a vault-type credential library given its ID. Example:", diff --git a/internal/cmd/common/flags.go b/internal/cmd/common/flags.go index f907545ed..07d6ead52 100644 --- a/internal/cmd/common/flags.go +++ b/internal/cmd/common/flags.go @@ -139,6 +139,11 @@ type CombinedSliceFlagValuePopulationInput struct { // combination method, e.g. "attr" will be used to build "string-attr"; also // used for generating help texts PiecewisePopulationInputBaseName string + + // If ProtoCompat is true, the key will be validated against proto3 syntax + // requirements for identifiers. If the string is split via KeyDelimiter, each + // segment will be evaluated independently. + PiecewiseNoProtoCompat bool } func PopulateCombinedSliceFlagValue(input CombinedSliceFlagValuePopulationInput) { @@ -163,7 +168,7 @@ func PopulateCombinedSliceFlagValue(input CombinedSliceFlagValuePopulationInput) Target: input.PiecewisePopulationFlag, KvSplit: true, KeyDelimiter: &keyDelimiter, - ProtoCompatKey: true, + ProtoCompatKey: !input.PiecewiseNoProtoCompat, KeyOnlyAllowed: true, Usage: fmt.Sprintf( "A key=value pair to add to the request's %s map. "+ diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 00adaddef..a7df2d470 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -227,18 +227,51 @@ var inputStructs = map[string][]*cmdInfo{ HasId: true, }, { - ResourceType: resource.CredentialLibrary.String(), - Pkg: "credentiallibraries", - StdActions: []string{"create", "update"}, - SubActionPrefix: "vault", - HasExtraCommandVars: true, - SkipNormalHelp: true, - HasExtraHelpFunc: true, - HasId: true, - HasName: true, - HasDescription: true, - Container: "CredentialStore", - VersionedActions: []string{"update"}, + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-generic", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-ssh-certificate", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, }, diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 0c8726eef..edee5fbf7 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -30,6 +30,7 @@ const ( UnspecifiedType Type = "unspecified" UsernamePasswordType Type = "username_password" SshPrivateKeyType Type = "ssh_private_key" + SshCertificateType Type = "ssh_certificate" JsonType Type = "json" ) @@ -97,6 +98,13 @@ type Request struct { Purpose Purpose } +// SshCertificate is a credential containing a client certificate, username, +// and SSH private key. +type SshCertificate interface { + SshPrivateKey + Certificate() []byte +} + // Issuer issues dynamic credentials. type Issuer interface { // Issue issues dynamic credentials for a session from the requested diff --git a/internal/credential/vault/credential.go b/internal/credential/vault/credential.go index 10f390f8e..5ca58ece9 100644 --- a/internal/credential/vault/credential.go +++ b/internal/credential/vault/credential.go @@ -1,10 +1,8 @@ package vault import ( - "database/sql" "time" - "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db/sanitize" "github.com/hashicorp/boundary/internal/db/sentinel" @@ -46,14 +44,11 @@ type Credential struct { expiration time.Duration `gorm:"-"` } -func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { +func newCredential(libraryId, externalId string, tokenHmac []byte, expiration time.Duration) (*Credential, error) { const op = "vault.newCredential" if libraryId == "" { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no library id") } - if sessionId == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no session id") - } if len(tokenHmac) == 0 { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no tokenHmac") } @@ -69,7 +64,6 @@ func newCredential(libraryId, sessionId, externalId string, tokenHmac []byte, ex expiration: expiration.Round(time.Second), Credential: &store.Credential{ LibraryId: libraryId, - SessionId: sessionId, ExternalId: externalId, TokenHmac: tokenHmac, Status: status, @@ -117,38 +111,6 @@ func (c *Credential) oplog(op oplog.OpType) oplog.Metadata { return metadata } -func (c *Credential) insertQuery() (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("token_hmac", c.TokenHmac), - sql.Named("external_id", c.ExternalId), - sql.Named("is_renewable", c.IsRenewable), - sql.Named("status", c.Status), - sql.Named("last_renewal_time", "now()"), - } - switch { - case c.expiration == 0: - query = insertCredentialWithInfiniteExpirationQuery - default: - query = insertCredentialWithExpirationQuery - queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) - } - return -} - -func (c *Credential) updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) { - queryValues = []any{ - sql.Named("public_id", c.PublicId), - sql.Named("library_id", c.LibraryId), - sql.Named("session_id", c.SessionId), - sql.Named("purpose", string(purpose)), - } - query = updateSessionCredentialQuery - return -} - func (c *Credential) updateExpirationQuery() (query string, queryValues []any) { queryValues = []any{ int(c.expiration.Round(time.Second).Seconds()), diff --git a/internal/credential/vault/credential_test.go b/internal/credential/vault/credential_test.go index 07b6e795b..9666d9f4e 100644 --- a/internal/credential/vault/credential_test.go +++ b/internal/credential/vault/credential_test.go @@ -56,17 +56,6 @@ func TestCredential_New(t *testing.T) { want: nil, wantErr: true, }, - { - name: "missing-session-id", - args: args{ - libraryId: lib.GetPublicId(), - externalId: "some/vault/credential", - tokenHmac: token.GetTokenHmac(), - expiration: 5 * time.Minute, - }, - want: nil, - wantErr: true, - }, { name: "missing-tokenHmac", args: args{ @@ -144,7 +133,7 @@ func TestCredential_New(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() - got, err := newCredential(tt.args.libraryId, tt.args.sessionId, + got, err := newCredential(tt.args.libraryId, tt.args.externalId, tt.args.tokenHmac, tt.args.expiration) if tt.wantErr { assert.Error(err) @@ -162,7 +151,8 @@ func TestCredential_New(t *testing.T) { tt.want.PublicId = id got.PublicId = id - query, queryValues := got.insertQuery() + query, queryValues := insertQuery(got, tt.args.sessionId) + require.NoError(err) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) diff --git a/internal/credential/vault/fields.go b/internal/credential/vault/fields.go index a3fab7300..f4e9ec076 100644 --- a/internal/credential/vault/fields.go +++ b/internal/credential/vault/fields.go @@ -9,6 +9,18 @@ const ( httpMethodField = "HttpMethod" httpRequestBodyField = "HttpRequestBody" + usernameField = "Username" + keyTypeField = "KeyType" + keyBitsField = "KeyBits" + ttlField = "Ttl" + keyIdField = "KeyId" + // CriticalOptionsField represents the field mask indicating a critical option + // update has been requested. + CriticalOptionsField = "CriticalOptions" + // ExtensionsField represents the field mask indicating an extension + // update has been requested. + ExtensionsField = "Extensions" + certificateField = "Certificate" certificateKeyField = "CertificateKey" vaultAddressField = "VaultAddress" diff --git a/internal/credential/vault/options.go b/internal/credential/vault/options.go index cc21022fd..c97a46b78 100644 --- a/internal/credential/vault/options.go +++ b/internal/credential/vault/options.go @@ -34,6 +34,13 @@ type options struct { withOverridePrivateKeyAttribute string withOverridePrivateKeyPassphraseAttribute string withMappingOverride MappingOverride + + withKeyType string + withKeyBits uint32 + withTtl string + withKeyId string + withCriticalOptions string + withExtensions string } func getDefaultOptions() options { @@ -174,3 +181,48 @@ func WithMappingOverride(m MappingOverride) Option { o.withMappingOverride = m } } + +// WithKeyType provides an optional ssh private key type to use +// with a ssh certificate credential library. Must be rsa, ed25519, or ecdsa. +func WithKeyType(t string) Option { + return func(o *options) { + o.withKeyType = t + } +} + +// WithKeyBits provides an optional number of bits used to generate an ssh private key. +func WithKeyBits(b uint32) Option { + return func(o *options) { + o.withKeyBits = b + } +} + +// WithTtl provides an optional requested time to live for a generated ssh certificate. +func WithTtl(t string) Option { + return func(o *options) { + o.withTtl = t + } +} + +// WithKeyId provides an optional key id for a created certificate. +func WithKeyId(i string) Option { + return func(o *options) { + o.withKeyId = i + } +} + +// WithCriticalOptions provides an optional map of the critical options +// that the certificate should be signed for. +func WithCriticalOptions(s string) Option { + return func(o *options) { + o.withCriticalOptions = s + } +} + +// WithExtensions provides a optional map of the extensions +// that the certificate should be signed for. +func WithExtensions(s string) Option { + return func(o *options) { + o.withExtensions = s + } +} diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 274194126..8b99947d5 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -2,14 +2,25 @@ package vault import ( "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" "database/sql" + "encoding/base64" + "encoding/json" + "encoding/pem" "fmt" + "regexp" "strings" "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey" "github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword" + "github.com/hashicorp/boundary/internal/db/sentinel" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" @@ -17,6 +28,8 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" vault "github.com/hashicorp/vault/api" + "github.com/mikesmitty/edkey" + "golang.org/x/crypto/ssh" "google.golang.org/protobuf/proto" ) @@ -25,14 +38,16 @@ var _ credential.Dynamic = (*baseCred)(nil) type baseCred struct { *Credential - lib *issueCredentialLibrary + lib issuingCredentialLibrary secretData map[string]any } func (bc *baseCred) Secret() credential.SecretData { return bc.secretData } func (bc *baseCred) Library() credential.Library { return bc.lib } -func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.Purpose } +func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.GetPurpose() } func (bc *baseCred) getExpiration() time.Duration { return bc.expiration } +func (bc *baseCred) getCredential() *Credential { return bc.Credential } +func (bc *baseCred) isRevokable() bool { return bc.ExternalId != sentinel.ExternalIdNone } // convert converts bc to a specific credential type if bc is not // UnspecifiedType. @@ -67,7 +82,12 @@ func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) { return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) } - uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PasswordAttribute + lib, ok := bc.lib.(*genericIssuingCredentialLibrary) + if !ok { + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary")) + } + + uAttr, pAttr := lib.UsernameAttribute, lib.PasswordAttribute if uAttr == "" { uAttr = "username" } @@ -109,7 +129,12 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type")) } - uAttr, pkAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PrivateKeyAttribute, bc.lib.PrivateKeyPassphraseAttribute + lib, ok := bc.lib.(*genericIssuingCredentialLibrary) + if !ok { + return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary")) + } + + uAttr, pkAttr, pAttr := lib.UsernameAttribute, lib.PrivateKeyAttribute, lib.PrivateKeyPassphraseAttribute if uAttr == "" { uAttr = "username" } @@ -132,12 +157,25 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err }, nil } -var _ credential.Library = (*issueCredentialLibrary)(nil) +type sshCertCred struct { + *sshPrivateKeyCred + certificate []byte +} -// A issueCredentialLibrary contains all the values needed to connect to Vault and +func (c *sshCertCred) Certificate() []byte { return c.certificate } + +var _ credential.Library = (*genericIssuingCredentialLibrary)(nil) + +type issuingCredentialLibrary interface { + credential.Library + GetPurpose() credential.Purpose + retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error) +} + +// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and // retrieve credentials. -type issueCredentialLibrary struct { - PublicId string `gorm:"primary_key"` +type genericIssuingCredentialLibrary struct { + PublicId string StoreId string Name string Description string @@ -147,7 +185,7 @@ type issueCredentialLibrary struct { VaultPath string HttpMethod string HttpRequestBody []byte - CredType string `gorm:"column:credential_type"` + CredType string ProjectId string VaultAddress string Namespace string @@ -167,13 +205,13 @@ type issueCredentialLibrary struct { PasswordAttribute string PrivateKeyAttribute string PrivateKeyPassphraseAttribute string - Purpose credential.Purpose `gorm:"-"` + Purpose credential.Purpose } -func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { +func (pl *genericIssuingCredentialLibrary) clone() *genericIssuingCredentialLibrary { // The 'append(a[:0:0], a...)' comes from // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F - return &issueCredentialLibrary{ + return &genericIssuingCredentialLibrary{ PublicId: pl.PublicId, StoreId: pl.StoreId, CredType: pl.CredType, @@ -208,15 +246,16 @@ func (pl *issueCredentialLibrary) clone() *issueCredentialLibrary { } } -func (pl *issueCredentialLibrary) GetPublicId() string { return pl.PublicId } -func (pl *issueCredentialLibrary) GetStoreId() string { return pl.StoreId } -func (pl *issueCredentialLibrary) GetName() string { return pl.Name } -func (pl *issueCredentialLibrary) GetDescription() string { return pl.Description } -func (pl *issueCredentialLibrary) GetVersion() uint32 { return pl.Version } -func (pl *issueCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } -func (pl *issueCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *genericIssuingCredentialLibrary) GetPublicId() string { return pl.PublicId } +func (pl *genericIssuingCredentialLibrary) GetStoreId() string { return pl.StoreId } +func (pl *genericIssuingCredentialLibrary) GetName() string { return pl.Name } +func (pl *genericIssuingCredentialLibrary) GetDescription() string { return pl.Description } +func (pl *genericIssuingCredentialLibrary) GetVersion() uint32 { return pl.Version } +func (pl *genericIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime } +func (pl *genericIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime } +func (pl *genericIssuingCredentialLibrary) GetPurpose() credential.Purpose { return pl.Purpose } -func (pl *issueCredentialLibrary) CredentialType() credential.Type { +func (pl *genericIssuingCredentialLibrary) CredentialType() credential.Type { switch ct := pl.CredType; ct { case "": return credential.UnspecifiedType @@ -225,41 +264,8 @@ func (pl *issueCredentialLibrary) CredentialType() credential.Type { } } -func (pl *issueCredentialLibrary) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { - const op = "vault.(issueCredentialLibrary).decrypt" - - if pl.CtToken != nil { - type ptk struct { - Token []byte `wrapping:"pt,token_data"` - CtToken []byte `wrapping:"ct,token_data"` - } - ptkv := &ptk{ - CtToken: pl.CtToken, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) - } - pl.Token = ptkv.Token - } - - if pl.CtClientKey != nil && pl.ClientCert != nil { - type pck struct { - Key []byte `wrapping:"pt,key_data"` - CtKey []byte `wrapping:"ct,key_data"` - } - pckv := &pck{ - CtKey: pl.CtClientKey, - } - if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { - return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) - } - pl.ClientKey = pckv.Key - } - return nil -} - -func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, error) { - const op = "vault.(issueCredentialLibrary).client" +func (pl *genericIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(genericIssuingCredentialLibrary).client" clientConfig := &clientConfig{ Addr: pl.VaultAddress, Token: pl.Token, @@ -284,15 +290,15 @@ func (pl *issueCredentialLibrary) client(ctx context.Context) (vaultClient, erro type dynamicCred interface { credential.Dynamic getExpiration() time.Duration - insertQuery() (query string, queryValues []any) - updateSessionQuery(purpose credential.Purpose) (query string, queryValues []any) + getCredential() *Credential + isRevokable() bool } // retrieveCredential retrieves a dynamic credential from Vault for the // given sessionId. // // Supported options: credential.WithTemplateData -func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string, opt ...credential.Option) (dynamicCred, error) { +func (pl *genericIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) { opts, err := credential.GetOpts(opt...) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -358,7 +364,7 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } leaseDuration := time.Duration(secret.LeaseDuration) * time.Second - cred, err := newCredential(pl.GetPublicId(), sessionId, secret.LeaseID, pl.TokenHmac, leaseDuration) + cred, err := newCredential(pl.GetPublicId(), secret.LeaseID, pl.TokenHmac, leaseDuration) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -374,11 +380,11 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err } // TableName returns the table name for gorm. -func (pl *issueCredentialLibrary) TableName() string { +func (pl *genericIssuingCredentialLibrary) TableName() string { return "credential_vault_library_issue_credentials" } -func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]*issueCredentialLibrary, error) { +func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]issuingCredentialLibrary, error) { const op = "vault.(Repository).getIssueCredLibraries" mapper, err := newMapper(requests) @@ -405,9 +411,9 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } defer rows.Close() - var libs []*issueCredentialLibrary + var libs []*privateCredentialLibraryAllTypes for rows.Next() { - var lib issueCredentialLibrary + var lib privateCredentialLibraryAllTypes if err := r.reader.ScanRows(ctx, rows, &lib); err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed")) } @@ -422,6 +428,7 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede } } + var decryptedLibs []issuingCredentialLibrary for _, pl := range libs { databaseWrapper, err := r.kms.GetWrapper(ctx, pl.ProjectId, kms.KeyPurposeDatabase) if err != nil { @@ -431,9 +438,209 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede if err := pl.decrypt(ctx, databaseWrapper); err != nil { return nil, errors.Wrap(ctx, err, op) } + decryptedLibs = append(decryptedLibs, pl.toTypedIssuingCredentialLibrary()) } - return libs, nil + return decryptedLibs, nil +} + +// privateCredentialLibraryAllTypes is a clone of genericIssuingCredentialLibrary. Contains +// all the values needed to connect to Vault and retrieve credentials. +type privateCredentialLibraryAllTypes struct { + PublicId string `gorm:"primary_key"` + StoreId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultPath string + HttpMethod string + HttpRequestBody []byte + CredType string `gorm:"column:credential_type"` + ProjectId string + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + Token TokenSecret + CtToken []byte + TokenHmac []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + UsernameAttribute string + PasswordAttribute string + PrivateKeyAttribute string + PrivateKeyPassphraseAttribute string + Purpose credential.Purpose `gorm:"-"` + KeyType string + KeyBits int + Username string + Ttl string + KeyId string + CriticalOptions []byte + Extensions []byte + CredLibType string +} + +func (pl *privateCredentialLibraryAllTypes) GetPublicId() string { return pl.PublicId } + +func (pl *privateCredentialLibraryAllTypes) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "vault.(privateCredentialLibraryAllTypes).decrypt" + + if pl.CtToken != nil { + type ptk struct { + Token []byte `wrapping:"pt,token_data"` + CtToken []byte `wrapping:"ct,token_data"` + } + ptkv := &ptk{ + CtToken: pl.CtToken, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token")) + } + pl.Token = ptkv.Token + } + + if pl.CtClientKey != nil && pl.ClientCert != nil { + type pck struct { + Key []byte `wrapping:"pt,key_data"` + CtKey []byte `wrapping:"ct,key_data"` + } + pckv := &pck{ + CtKey: pl.CtClientKey, + } + if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate")) + } + pl.ClientKey = pckv.Key + } + return nil +} + +func (pl *privateCredentialLibraryAllTypes) clone() *privateCredentialLibraryAllTypes { + // The 'append(a[:0:0], a...)' comes from + // https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F + return &privateCredentialLibraryAllTypes{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp), + UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp), + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...), + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: append(pl.CaCert[:0:0], pl.CaCert...), + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...), + Token: append(pl.Token[:0:0], pl.Token...), + CtToken: append(pl.CtToken[:0:0], pl.CtToken...), + TokenKeyId: pl.TokenKeyId, + ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...), + ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...), + CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...), + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + KeyType: pl.KeyType, + KeyBits: pl.KeyBits, + Username: pl.Username, + Ttl: pl.Ttl, + KeyId: pl.KeyId, + CriticalOptions: pl.CriticalOptions, + Extensions: pl.Extensions, + CredLibType: pl.CredLibType, + } +} + +func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() issuingCredentialLibrary { + switch pl.CredLibType { + case "ssh-signed-cert": + return &sshCertIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + Username: pl.Username, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + KeyType: pl.KeyType, + KeyBits: pl.KeyBits, + Ttl: pl.Ttl, + KeyId: pl.KeyId, + CriticalOptions: pl.CriticalOptions, + Extensions: pl.Extensions, + } + default: + return &genericIssuingCredentialLibrary{ + PublicId: pl.PublicId, + StoreId: pl.StoreId, + CredType: pl.CredType, + UsernameAttribute: pl.UsernameAttribute, + PasswordAttribute: pl.PasswordAttribute, + PrivateKeyAttribute: pl.PrivateKeyAttribute, + PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute, + Name: pl.Name, + Description: pl.Description, + CreateTime: pl.CreateTime, + UpdateTime: pl.UpdateTime, + Version: pl.Version, + ProjectId: pl.ProjectId, + VaultPath: pl.VaultPath, + HttpMethod: pl.HttpMethod, + HttpRequestBody: pl.HttpRequestBody, + VaultAddress: pl.VaultAddress, + Namespace: pl.Namespace, + CaCert: pl.CaCert, + TlsServerName: pl.TlsServerName, + TlsSkipVerify: pl.TlsSkipVerify, + WorkerFilter: pl.WorkerFilter, + TokenHmac: pl.TokenHmac, + Token: pl.Token, + CtToken: pl.CtToken, + TokenKeyId: pl.TokenKeyId, + ClientCert: pl.ClientCert, + ClientKey: pl.ClientKey, + CtClientKey: pl.CtClientKey, + ClientKeyId: pl.ClientKeyId, + Purpose: pl.Purpose, + } + } } // requestMap takes a slice of credential requests and provides of list of @@ -478,3 +685,320 @@ func (m *requestMap) libIds() []string { func (m *requestMap) get(libraryId string) []credential.Purpose { return m.ids[libraryId] } + +type sshCertIssuingCredentialLibrary struct { + PublicId string + StoreId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + VaultPath string + CredType string + ProjectId string + VaultAddress string + Namespace string + CaCert []byte + TlsServerName string + TlsSkipVerify bool + WorkerFilter string + Token TokenSecret + CtToken []byte + TokenHmac []byte + TokenKeyId string + ClientCert []byte + ClientKey KeySecret + CtClientKey []byte + ClientKeyId string + Username string + KeyType string + KeyBits int + KeyId string + Ttl string + CriticalOptions []byte + Extensions []byte + Purpose credential.Purpose +} + +func (lib *sshCertIssuingCredentialLibrary) GetPublicId() string { return lib.PublicId } +func (lib *sshCertIssuingCredentialLibrary) GetStoreId() string { return lib.StoreId } +func (lib *sshCertIssuingCredentialLibrary) GetName() string { return lib.Name } +func (lib *sshCertIssuingCredentialLibrary) GetDescription() string { return lib.Description } +func (lib *sshCertIssuingCredentialLibrary) GetVersion() uint32 { return lib.Version } +func (lib *sshCertIssuingCredentialLibrary) GetPurpose() credential.Purpose { return lib.Purpose } +func (lib *sshCertIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { + return lib.CreateTime +} + +func (lib *sshCertIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { + return lib.UpdateTime +} + +func (lib *sshCertIssuingCredentialLibrary) CredentialType() credential.Type { + switch ct := lib.CredType; ct { + case "": + return credential.UnspecifiedType + default: + return credential.Type(ct) + } +} + +func (lib *sshCertIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) { + const op = "vault.(genericIssuingCredentialLibrary).client" + clientConfig := &clientConfig{ + Addr: lib.VaultAddress, + Token: lib.Token, + CaCert: lib.CaCert, + TlsServerName: lib.TlsServerName, + TlsSkipVerify: lib.TlsSkipVerify, + Namespace: lib.Namespace, + } + + if lib.ClientKey != nil { + clientConfig.ClientCert = lib.ClientCert + clientConfig.ClientKey = lib.ClientKey + } + + client, err := vaultClientFactoryFn(ctx, clientConfig, WithWorkerFilter(lib.WorkerFilter)) + if err != nil { + return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to create vault client")) + } + return client, nil +} + +func generatePublicPrivateKeys(ctx context.Context, keyType string, keyBits int) (string, []byte, error) { + const op = "vault.generatePublicPrivateKeys" + pemBlock := pem.Block{} + var sshKey ssh.PublicKey + + switch keyType { + case KeyTypeRsa: + pemBlock.Type = "RSA PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + key, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + pemBlock.Bytes = x509.MarshalPKCS1PrivateKey(key) + + case KeyTypeEd25519: + pemBlock.Type = "OPENSSH PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + if sshKey, err = ssh.NewPublicKey(pubKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if pemBlock.Bytes = edkey.MarshalED25519PrivateKey(privKey); pemBlock.Bytes == nil { + return "", nil, errors.New(ctx, errors.Encode, op, "failed to marshal ed25519 private key") + } + + case KeyTypeEcdsa: + pemBlock.Type = "EC PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go + + var curve elliptic.Curve + switch keyBits { + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyBits. when KeyType=ecdsa, KeyBits must be one of: 256, 384, or 521") + } + + key, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + if pemBlock.Bytes, err = x509.MarshalECPrivateKey(key); err != nil { + return "", nil, errors.Wrap(ctx, err, op) + } + + default: + return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyType, must be one of: \"rsa\", \"ed25519\", or \"ecdsa\"") + } + + publicKey := base64.StdEncoding.EncodeToString(sshKey.Marshal()) + privateKey := pem.EncodeToMemory(&pemBlock) + if privateKey == nil { + return "", nil, errors.New(ctx, errors.Encode, op, "failed to encode private key to PEM format") + } + return publicKey, privateKey, nil +} + +type sshCertVaultBody struct { + KeyType string `json:"key_type,omitempty"` // must be "rsa", "ed25519", or "ecdsa" + KeyBits int `json:"key_bits,omitempty"` // with key_type=rsa, allowed values are: 2048 (default), 3072, or 4096; with key_type=ecdsa, allowed values are: 256 (default), 384, or 521; ignored with key_type=ed25519 + PublicKey string `json:"public_key,omitempty"` + TTL string `json:"ttl,omitempty"` + ValidPrincipals string `json:"valid_principals,omitempty"` // this needs to be "generated" off of the username provided in config + CertType string `json:"cert_type,omitempty"` // this should always be "user" + KeyId string `json:"key_id,omitempty"` // this will be loaded directly from lib + CriticalOptions map[string]string `json:"critical_options,omitempty"` // this will be loaded directly from lib + Extensions map[string]string `json:"extensions,omitempty"` // this will be loaded directly from lib +} + +var vaultPathRegexp = regexp.MustCompile(`^.+\/(sign|issue)\/[^\/\\\s]+$`) + +// retrieveCredential retrieves a dynamic connection credential from Vault +// for a specific session and connection. +// +// Supported options: credential.WithTemplateData +func (lib *sshCertIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) { + opts, err := credential.GetOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + // Get the credential ID early. No need to get a secret from Vault + // if there is no way to save it in the database. + credId, err := newCredentialId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + client, err := lib.client(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + // build as much of the payload as we can, since sign/issue share many attributes + // Template the username + tplate, err := template.New(ctx, lib.Username) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + username, err := tplate.Generate(ctx, opts.WithTemplateData) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var criticalOptions map[string]string + if lib.CriticalOptions != nil { + if json.Unmarshal(lib.CriticalOptions, &criticalOptions) != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + + var extensions map[string]string + if lib.Extensions != nil { + if json.Unmarshal(lib.Extensions, &extensions) != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + + payload := sshCertVaultBody{ + ValidPrincipals: username, + CertType: "user", + CriticalOptions: criticalOptions, + Extensions: extensions, + TTL: lib.Ttl, + KeyId: lib.KeyId, + } + + var privateKey credential.PrivateKey + var secret *vault.Secret + + match := vaultPathRegexp.FindStringSubmatch(lib.VaultPath) + if len(match) < 2 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "vault path was not in an expected format. expected path containing \"sign\" or \"issue\"") + } + + // by definition, if match exists, then match[1] == "sign" or "issue" + switch match[1] { + case "sign": + payload.PublicKey, privateKey, err = generatePublicPrivateKeys(ctx, lib.KeyType, lib.KeyBits) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + secret, err = client.post(ctx, lib.VaultPath, body) + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + case "issue": + payload.KeyBits = lib.KeyBits + if lib.KeyType == KeyTypeEcdsa { + // this is a special case where internal to boundary, we refer to the crypto + // library name, but vault refers to it simply as "ec" for "Elliptic Curve" + payload.KeyType = "ec" + } else { + payload.KeyType = lib.KeyType + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + secret, err = client.post(ctx, lib.VaultPath, body) + if err != nil { + // TODO(mgaffney) 05/2021: detect if the error is because of an + // expired or invalid token + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + pk, ok := secret.Data["private_key"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a private key or response was not in the expected format") + } + + privateKey = []byte(pk) + } + + leaseDuration := time.Duration(secret.LeaseDuration) * time.Second + cred, err := newCredential(lib.GetPublicId(), secret.LeaseID, lib.TokenHmac, leaseDuration) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cred.PublicId = credId + cred.IsRenewable = secret.Renewable // possibly set to just false + + // same location for both + cert, ok := secret.Data["signed_key"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a signed key or response was not in the expected format") + } + + return &sshCertCred{ + sshPrivateKeyCred: &sshPrivateKeyCred{ + baseCred: &baseCred{ + Credential: cred, + lib: lib, + secretData: secret.Data, + }, + username: username, + privateKey: privateKey, + }, + certificate: []byte(cert), + }, nil +} diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index fc156232b..ed0fff6bf 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -2,7 +2,9 @@ package vault import ( "context" + "fmt" "testing" + "time" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" @@ -10,6 +12,8 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" + "github.com/hashicorp/boundary/internal/util" + "github.com/hashicorp/boundary/internal/util/template" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,7 +67,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) { opts = append(opts, WithClientCert(clientCert)) } - _, token := v.CreateToken(t) + _, token := v.CreateToken(t, WithTokenPeriod(time.Hour)) credStoreIn, err := NewCredentialStore(prj.GetPublicId(), v.Addr, []byte(token), opts...) assert.NoError(err) @@ -269,7 +273,10 @@ func TestRepository_getPrivateLibraries(t *testing.T) { require.NotNil(gotLibs) assert.Len(gotLibs, len(libs)) - for _, got := range gotLibs { + for _, gotLib := range gotLibs { + got, ok := gotLib.(*genericIssuingCredentialLibrary) + require.True(ok) + assert.Equal(TokenSecret(token), got.Token) if tt.tls == TestClientTLS { require.NotNil(got.ClientKey) @@ -414,7 +421,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "library-not-username-password-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -423,7 +430,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -435,7 +442,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-no-password-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -447,7 +454,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -463,7 +470,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -483,7 +490,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -502,7 +509,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -521,7 +528,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-username", }, @@ -537,7 +544,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-password-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "missing-password", }, @@ -553,7 +560,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -568,7 +575,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -580,7 +587,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-username-default-password-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -595,7 +602,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-no-passsword-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -610,7 +617,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -626,7 +633,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -639,7 +646,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -656,7 +663,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), }, secretData: map[string]any{ @@ -675,7 +682,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", PasswordAttribute: "test-password", @@ -698,7 +705,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-default-username-override-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), PasswordAttribute: "test-password", }, @@ -720,7 +727,7 @@ func TestBaseToUsrPass(t *testing.T) { { name: "valid-kv2-override-username-default-password", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UsernamePasswordType), UsernameAttribute: "test-username", }, @@ -779,7 +786,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "library-not-ssh-private-key-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.UnspecifiedType), }, }, @@ -788,7 +795,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -800,7 +807,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -812,7 +819,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -828,7 +835,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -846,7 +853,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -866,7 +873,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -890,7 +897,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -909,7 +916,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, @@ -928,7 +935,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-username-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-username", }, @@ -944,7 +951,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-pk-override", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "missing-pk", }, @@ -960,7 +967,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-metadata-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -975,7 +982,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-data-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -987,7 +994,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-username-default-pk-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1002,7 +1009,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-no-pk-default-username-attribute", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1017,7 +1024,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-metadata-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1033,7 +1040,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-invalid-data-type", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1046,7 +1053,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "invalid-kv2-additional-field", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1063,7 +1070,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1082,7 +1089,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), }, secretData: map[string]any{ @@ -1103,7 +1110,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1126,7 +1133,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-attributes-with-passphrase", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", PrivateKeyAttribute: "test-pk", @@ -1153,7 +1160,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-default-username-override-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), PrivateKeyAttribute: "test-pk", }, @@ -1175,7 +1182,7 @@ func TestBaseToSshPriKey(t *testing.T) { { name: "valid-kv2-override-username-default-pk", given: &baseCred{ - lib: &issueCredentialLibrary{ + lib: &genericIssuingCredentialLibrary{ CredType: string(credential.SshPrivateKeyType), UsernameAttribute: "test-username", }, @@ -1212,3 +1219,171 @@ func TestBaseToSshPriKey(t *testing.T) { }) } } + +func TestRepository_sshCertIssuingCredentialLibrary_retrieveCredential(t *testing.T) { + t.Parallel() + + // create test vault server + v := NewTestVaultServer(t, WithTestVaultTLS(TestNoTLS), WithVaultVersion("1.12.2")) + require.NotNil(t, v) + + vc := v.client(t).cl + mounts, err := vc.Sys().ListMounts() + assert.NoError(t, err) + require.NotEmpty(t, mounts) + beforeCount := len(mounts) + + // enable ssh secrets engine + v.MountSSH(t, WithAllowedExtension("permit-pty")) + mounts, err = vc.Sys().ListMounts() + fmt.Printf("mounts: %#v\n", mounts) + assert.NoError(t, err) + require.NotEmpty(t, mounts) + afterCount := len(mounts) + assert.Greater(t, afterCount, beforeCount) + + // create and setup db + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + sec, token := v.CreateToken(t, WithPolicies([]string{"default", "boundary-controller", "ssh"}), WithTokenPeriod(time.Hour)) + + sche := scheduler.TestScheduler(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + cs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), v.Addr, token, sec.Auth.Accessor) + + tests := []struct { + name string + username string + vaulthPath string + opts []Option + retOpts []credential.Option + expected map[string]string + }{ + { + name: "vault sign boundary ed25519 key", + username: "username", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeEd25519)}, + }, + { + name: "vault sign boundary rsa(4096) key", + username: "username-2-electric-boogaloo", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(4096)}, + }, + { + name: "vault sign boundary ec(521) key", + username: "username-3-the-namening", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(521)}, + }, + { + name: "vault issue ed25519 cert", + username: "username-4-revengeance", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEd25519)}, + }, + { + name: "vault issue rsa(4096) cert", + username: "username-5-this-time-its-personal", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(4096)}, + }, + { + name: "vault issue ec(521) cert", + username: "username-6-the-holiday-episode", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(521)}, + }, + { + name: "vault issue rsa(2048) cert with critical options and extensions", + username: "username-7-because-789", + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(2048), WithCriticalOptions("{ \"force-commnad\": \"/bin/some-script\" }"), WithExtensions("{ \"permit-pty\": \"\" }")}, + }, + { + name: "vault sign boundary rsa(3072) key with critical options and extensions", + username: "username-7-because-789", + vaulthPath: "ssh/sign/boundary", + opts: []Option{WithKeyType(KeyTypeRsa), WithKeyBits(3072), WithCriticalOptions("{ \"force-commnad\": \"/bin/some-script\" }"), WithExtensions("{ \"permit-pty\": \"\" }")}, + }, + { + name: "vault issue ec(256) cert with template username", + username: "username-8-{{ .User.Name }}", + expected: map[string]string{ + "username": "username-8-revenge-of-the-template", + }, + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(256)}, + retOpts: []credential.Option{credential.WithTemplateData(template.Data{User: template.User{Name: util.Pointer("revenge-of-the-template")}})}, + }, + { + name: "vault issue ec(384) cert with disallowed extension", + username: "username-10-because-789", + expected: map[string]string{ + "error": "extensions [permit-port-forwarding] are not on allowed list", + }, + vaulthPath: "ssh/issue/boundary", + opts: []Option{WithKeyType(KeyTypeEcdsa), WithKeyBits(384), WithExtensions("{ \"permit-port-forwarding\": \"\" }")}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + // create ssh library + lib, err := NewSSHCertificateCredentialLibrary(cs.GetPublicId(), tt.vaulthPath, tt.username, tt.opts...) + require.NoError(err) + require.NotNil(lib) + lib.PublicId, err = newSSHCertificateCredentialLibraryId() + require.NoError(err) + + _, err = rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + return iw.Create(ctx, lib) + }, + ) + require.NoError(err) + + req := credential.Request{ + SourceId: lib.GetPublicId(), + Purpose: "doesn't matter", + } + + libs, err := repo.getIssueCredLibraries(ctx, []credential.Request{req}) + require.NoError(err) + require.NotEmpty(libs) + require.Equal(1, len(libs)) + + cred, err := libs[0].retrieveCredential(ctx, "op", tt.retOpts...) + if retErr, ok := tt.expected["error"]; ok { + require.ErrorContains(err, retErr) + return // retrieveCredential failed (as expected) don't do the rest of the checks + } + require.NoError(err) + + cert, ok := cred.(*sshCertCred) + require.True(ok) + + if usr, ok := tt.expected["username"]; ok { + require.Equal(usr, cert.username) + } + + // TODO: somehow check that the ssh cert matches the private key + // for now, manually check that private key and cert match and that sign/issue both return correct formats + fmt.Printf("test: %s\npriv:\n%s\ncert:\n%s\n", tt.name, string(cert.privateKey), string(cert.certificate)) + + // ensure credential matches expected SshCertificate + _, ok = cred.(credential.SshCertificate) + require.True(ok) + }) + } +} diff --git a/internal/credential/vault/public_ids.go b/internal/credential/vault/public_ids.go index 4cbec8374..0587c8534 100644 --- a/internal/credential/vault/public_ids.go +++ b/internal/credential/vault/public_ids.go @@ -8,18 +8,27 @@ import ( ) func init() { - if err := subtypes.Register(credential.Domain, Subtype, CredentialStorePrefix, CredentialLibraryPrefix, DynamicCredentialPrefix); err != nil { + if err := subtypes.Register(credential.Domain, Subtype, CredentialStorePrefix, DynamicCredentialPrefix); err != nil { + panic(err) + } + if err := subtypes.Register(credential.Domain, GenericLibrarySubtype, CredentialLibraryPrefix); err != nil { + panic(err) + } + if err := subtypes.Register(credential.Domain, SSHCertificateLibrarySubtype, SSHCertificateCredentialLibraryPrefix); err != nil { panic(err) } } // PublicId prefixes for the resources in the vault package. const ( - CredentialStorePrefix = "csvlt" - CredentialLibraryPrefix = "clvlt" - DynamicCredentialPrefix = "cdvlt" + CredentialStorePrefix = "csvlt" + CredentialLibraryPrefix = "clvlt" + DynamicCredentialPrefix = "cdvlt" + SSHCertificateCredentialLibraryPrefix = "clvsclt" - Subtype = subtypes.Subtype("vault") + Subtype = subtypes.Subtype("vault") + GenericLibrarySubtype = subtypes.Subtype("vault-generic") + SSHCertificateLibrarySubtype = subtypes.Subtype("vault-ssh-certificate") ) func newCredentialStoreId() (string, error) { @@ -45,3 +54,11 @@ func newCredentialLibraryId() (string, error) { } return id, nil } + +func newSSHCertificateCredentialLibraryId() (string, error) { + id, err := db.NewPublicId(SSHCertificateCredentialLibraryPrefix) + if err != nil { + return "", errors.WrapDeprecated(err, "vault.newSSHCertificateCredentialLibraryPrefix") + } + return id, nil +} diff --git a/internal/credential/vault/repository_credentials.go b/internal/credential/vault/repository_credentials.go index 8d72895ac..f84a824d6 100644 --- a/internal/credential/vault/repository_credentials.go +++ b/internal/credential/vault/repository_credentials.go @@ -2,6 +2,7 @@ package vault import ( "context" + "database/sql" "fmt" "time" @@ -13,6 +14,38 @@ import ( var _ credential.Issuer = (*Repository)(nil) +func insertQuery(c *Credential, sessionId string) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("token_hmac", c.TokenHmac), + sql.Named("external_id", c.ExternalId), + sql.Named("is_renewable", c.IsRenewable), + sql.Named("status", c.Status), + sql.Named("last_renewal_time", "now()"), + } + switch { + case c.expiration == 0: + query = insertCredentialWithInfiniteExpirationQuery + default: + query = insertCredentialWithExpirationQuery + queryValues = append(queryValues, sql.Named("expiration_time", int(c.expiration.Round(time.Second).Seconds()))) + } + return +} + +func updateSessionQuery(c *Credential, sessionId string, purpose credential.Purpose) (query string, queryValues []any) { + queryValues = []any{ + sql.Named("public_id", c.PublicId), + sql.Named("library_id", c.LibraryId), + sql.Named("session_id", sessionId), + sql.Named("purpose", string(purpose)), + } + query = updateSessionCredentialQuery + return +} + // Issue issues and returns dynamic credentials from Vault for all of the // requests and assigns them to sessionId. func (r *Repository) Issue(ctx context.Context, sessionId string, requests []credential.Request, opt ...credential.Option) ([]credential.Dynamic, error) { @@ -37,11 +70,17 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre var minLease time.Duration runJobsInterval := r.scheduler.GetRunJobsInterval() for _, lib := range libs { - cred, err := lib.retrieveCredential(ctx, op, sessionId, opt...) + cred, err := lib.retrieveCredential(ctx, op, opt...) if err != nil { return nil, err } + creds = append(creds, cred) + if !cred.isRevokable() { + // No need to persist since the credential cannot be revoked nor renewed + continue + } + if cred.getExpiration() < runJobsInterval { event.WriteError(ctx, op, fmt.Errorf("WARNING: credential will expire before job scheduler can run"), @@ -54,8 +93,18 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre if minLease > cred.getExpiration() { minLease = cred.getExpiration() } - insertQuery, insertQueryValues := cred.insertQuery() - updateQuery, updateQueryValues := cred.updateSessionQuery(lib.Purpose) + + underlyingCred := cred.getCredential() + + insertQuery, insertQueryValues := insertQuery(underlyingCred, sessionId) + if err != nil { + return nil, err + } + updateQuery, updateQueryValues := updateSessionQuery(underlyingCred, sessionId, cred.Purpose()) + if err != nil { + return nil, err + } + if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { rowsInserted, err := w.Exec(ctx, insertQuery, insertQueryValues) @@ -80,8 +129,6 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre ); err != nil { return nil, errors.Wrap(ctx, err, op) } - - creds = append(creds, cred) } // Best effort update next run time of credential renewal job, but an error should not diff --git a/internal/credential/vault/repository_credentials_test.go b/internal/credential/vault/repository_credentials_test.go index 998ec15ee..5fe236f4d 100644 --- a/internal/credential/vault/repository_credentials_test.go +++ b/internal/credential/vault/repository_credentials_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -21,6 +22,95 @@ import ( "github.com/stretchr/testify/require" ) +// since we're not using gorm tags to retrieve this, it's faster and easier to just make a new struct than import usrPassCred from the vault package +type revokableCred struct { + PublicId string + LibraryId string + SessionId string + TokenHmac []byte + ExternalId string + IsRenewable bool + Status string + LastRenewalTime *timestamp.Timestamp + ExpirationTime *timestamp.Timestamp +} + +func lookupDbCred(t *testing.T, ctx context.Context, rw *db.Db, dc credential.Dynamic) *revokableCred { + rows, err := rw.Query(ctx, ` + select + public_id, + library_id, + session_id, + token_hmac, + external_id, + is_renewable, + status, + last_renewal_time, + expiration_time + from credential_vault_credential + where public_id = ?; + `, []any{dc.GetPublicId()}) + require.NoError(t, err) + rowCount := 0 + + got := revokableCred{} + + for rows.Next() { + rowCount++ + require.NoError(t, rows.Scan( + &got.PublicId, + &got.LibraryId, + &got.SessionId, + &got.TokenHmac, + &got.ExternalId, + &got.IsRenewable, + &got.Status, + &got.LastRenewalTime, + &got.ExpirationTime, + )) + } + // Should never get more than one that matches, but can get 0 + assert.LessOrEqual(t, rowCount, 1) + + if rowCount == 0 { + return nil + } + + return &got +} + +type libT int + +const ( + libDB libT = iota + libUsrPassDB + libErrUsrPassDB + libPKI + libErrPKI + libKV + libErrKV + libUsrPassKV + libSshPkKV + libExpiredToken +) + +type testLib struct { + PublicId string + HasLease bool + IsRenewable bool +} + +type testLibMap map[libT]testLib + +func (m testLibMap) GetByPublicId(id string) (testLib, bool) { + for _, v := range m { + if v.PublicId == id { + return v, true + } + } + return testLib{}, false +} + func TestRepository_IssueCredentials(t *testing.T) { t.Parallel() ctx := context.Background() @@ -80,21 +170,7 @@ func TestRepository_IssueCredentials(t *testing.T) { assert.Equal(t, 1, num) v.RevokeToken(t, expToken) - type libT int - const ( - libDB libT = iota - libUsrPassDB - libErrUsrPassDB - libPKI - libErrPKI - libKV - libErrKV - libUsrPassKV - libSshPkKV - libExpiredToken - ) - - libs := make(map[libT]string) + libs := make(testLibMap) { libPath := path.Join("database", "creds", "opened") libIn, err := vault.NewCredentialLibrary(origStore.GetPublicId(), libPath) @@ -103,7 +179,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libDB] = lib.GetPublicId() + libs[libDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("pki", "issue", "boundary") @@ -113,7 +189,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libPKI] = lib.GetPublicId() + libs[libPKI] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: false} } { @@ -124,7 +200,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrPKI] = lib.GetPublicId() + libs[libErrPKI] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -134,7 +210,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libKV] = lib.GetPublicId() + libs[libKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true} } { libPath := path.Join("secret", "data", "fake-secret") @@ -144,7 +220,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrKV] = lib.GetPublicId() + libs[libErrKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false, IsRenewable: true} } { libPath := path.Join("database", "creds", "opened") @@ -157,7 +233,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libUsrPassDB] = lib.GetPublicId() + libs[libUsrPassDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("database", "creds", "opened") @@ -174,7 +250,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libErrUsrPassDB] = lib.GetPublicId() + libs[libErrUsrPassDB] = testLib{PublicId: lib.GetPublicId(), HasLease: true, IsRenewable: true} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -187,7 +263,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libUsrPassKV] = lib.GetPublicId() + libs[libUsrPassKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } { libPath := path.Join("secret", "data", "my-sshpk-secret") @@ -200,7 +276,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libSshPkKV] = lib.GetPublicId() + libs[libSshPkKV] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } { libPath := path.Join("secret", "data", "my-up-secret") @@ -213,7 +289,7 @@ func TestRepository_IssueCredentials(t *testing.T) { lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn) assert.NoError(t, err) require.NotNil(t, lib) - libs[libExpiredToken] = lib.GetPublicId() + libs[libExpiredToken] = testLib{PublicId: lib.GetPublicId(), HasLease: false} } at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) @@ -250,7 +326,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -260,11 +336,11 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, { - SourceId: libs[libPKI], + SourceId: libs[libPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -274,7 +350,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrPKI], + SourceId: libs[libErrPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -285,11 +361,11 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2nil, requests: []credential.Request{ { - SourceId: libs[libDB], + SourceId: libs[libDB].PublicId, Purpose: credential.BrokeredPurpose, }, { - SourceId: libs[libPKI], + SourceId: libs[libPKI].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -300,7 +376,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libKV], + SourceId: libs[libKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -310,7 +386,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libUsrPassDB], + SourceId: libs[libUsrPassDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -320,7 +396,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrKV], + SourceId: libs[libErrKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -331,7 +407,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libErrUsrPassDB], + SourceId: libs[libErrUsrPassDB].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -342,7 +418,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libUsrPassKV], + SourceId: libs[libUsrPassKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -352,7 +428,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libSshPkKV], + SourceId: libs[libSshPkKV].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -362,7 +438,7 @@ func TestRepository_IssueCredentials(t *testing.T) { convertFn: rc2dc, requests: []credential.Request{ { - SourceId: libs[libExpiredToken], + SourceId: libs[libExpiredToken].PublicId, Purpose: credential.BrokeredPurpose, }, }, @@ -406,6 +482,19 @@ func TestRepository_IssueCredentials(t *testing.T) { assert.Fail("do not want UsernamePassword credential from library with credential type Unspecified") } } + if lib, ok := libs.GetByPublicId(dc.Library().GetPublicId()); ok { + retrieved := lookupDbCred(t, ctx, rw, dc) + if lib.HasLease { + // we also want to retrieve the cred from the db and make sure it's the same as the one returned by Issue + require.NotNil(retrieved) + require.Equal(retrieved.SessionId, sess.GetPublicId()) + require.Equal(retrieved.PublicId, dc.GetPublicId()) + assert.NotEmpty(retrieved.ExternalId) + assert.Equal(retrieved.IsRenewable, lib.IsRenewable) + } else { + assert.Nil(retrieved) + } + } } }) } diff --git a/internal/credential/vault/repository_ssh_certificate_credential_library.go b/internal/credential/vault/repository_ssh_certificate_credential_library.go new file mode 100644 index 000000000..f26b2ef60 --- /dev/null +++ b/internal/credential/vault/repository_ssh_certificate_credential_library.go @@ -0,0 +1,363 @@ +package vault + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/go-dbw" +) + +// CreateSSHCertificateCredentialLibrary inserts l into the repository and returns a new +// SSHCertificateCredentialLibrary containing the credential library's PublicId. l is not +// changed. l must contain a valid StoreId. l must not contain a PublicId. +// The PublicId is generated and assigned by this method. +// +// Both l.Name and l.Description are optional. If l.Name is set, it must be +// unique within l.StoreId. +// +// Both l.CreateTime and l.UpdateTime are ignored. +func (r *Repository) CreateSSHCertificateCredentialLibrary(ctx context.Context, projectId string, l *SSHCertificateCredentialLibrary, _ ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).CreateSSHCertificateCredentialLibrary" + if l == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil SSHCertificateCredentialLibrary") + } + if l.SSHCertificateCredentialLibrary == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded l") + } + if l.StoreId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no store id") + } + if l.VaultPath == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no vault path") + } + if l.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + if l.Username == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no username") + } + + l = l.clone() + + if l.KeyType == "" { + l.KeyType = KeyTypeEd25519 + } + + if l.KeyBits == KeyBitsDefault { + l.KeyBits = l.getDefaultKeyBits() + } + + if l.GetCredentialType() == "" { + l.SSHCertificateCredentialLibrary.CredentialType = string(credential.SshCertificateType) + } + if l.GetCredentialType() != string(credential.SshCertificateType) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid credential type") + } + + id, err := newSSHCertificateCredentialLibraryId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + l.setId(id) + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var newSSHCertificateCredentialLibrary *SSHCertificateCredentialLibrary + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + var msgs []*oplog.Message + ticket, err := w.GetTicket(ctx, l) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + // insert credential library + newSSHCertificateCredentialLibrary = l.clone() + var lOplogMsg oplog.Message + if err := w.Create(ctx, newSSHCertificateCredentialLibrary, db.NewOplogMsg(&lOplogMsg)); err != nil { + return errors.Wrap(ctx, err, op) + } + msgs = append(msgs, &lOplogMsg) + + metadata := l.oplog(oplog.OpType_OP_TYPE_CREATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in credential store: %s: name %s already exists", l.StoreId, l.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in credential store: %s", l.StoreId))) + } + return newSSHCertificateCredentialLibrary, nil +} + +// UpdateSSHCertificateCredentialLibrary updates the repository entry for l.PublicId with +// the values in l for the fields listed in fieldMaskPaths. It returns a +// new SSHCertificateCredentialLibrary containing the updated values and a count of the +// number of records updated. l is not changed. +// +// l must contain a valid PublicId. Name, Description, VaultPath, Username, +// KeyType, KeyBits, Ttl, KeyId, CriticalOptions, and Extensions can be updated. If +// l.Name is set to a non-empty string, it must be unique within l.StoreId. +// +// An attribute of l will be set to NULL in the database if the attribute +// in l is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateSSHCertificateCredentialLibrary(ctx context.Context, projectId string, l *SSHCertificateCredentialLibrary, version uint32, fieldMaskPaths []string, _ ...Option) (*SSHCertificateCredentialLibrary, int, error) { + const op = "vault.(Repository).UpdateSSHCertificateCredentialLibrary" + if l == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing SSHCertificateCredentialLibrary") + } + if l.SSHCertificateCredentialLibrary == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded SSHCertificateCredentialLibrary") + } + if l.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + l = l.clone() + + var keyTypeChange, keyBitChangeDefault bool + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(vaultPathField, f): + case strings.EqualFold(usernameField, f): + case strings.EqualFold(keyTypeField, f): + keyTypeChange = true + case strings.EqualFold(keyBitsField, f): + keyBitChangeDefault = l.KeyBits == KeyBitsDefault + case strings.EqualFold(ttlField, f): + case strings.EqualFold(keyIdField, f): + case strings.EqualFold(CriticalOptionsField, f): + case strings.EqualFold(ExtensionsField, f): + + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + + if keyTypeChange && l.KeyType == "" { + l.KeyType = KeyTypeEd25519 + } + + if keyTypeChange && keyBitChangeDefault { + l.KeyBits = l.getDefaultKeyBits() + } + + origLib, err := r.LookupSSHCertificateCredentialLibrary(ctx, l.PublicId) + switch { + case err != nil: + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + case origLib == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("credential library %s", l.PublicId)) + } + + if keyBitChangeDefault && !keyTypeChange { + l.KeyBits = origLib.getDefaultKeyBits() + } + + var dbMask, nullFields []string + dbMask, nullFields = dbw.BuildUpdatePaths( + map[string]any{ + nameField: l.Name, + descriptionField: l.Description, + vaultPathField: l.VaultPath, + usernameField: l.Username, + keyTypeField: l.KeyType, + keyBitsField: l.KeyBits, + ttlField: l.Ttl, + keyIdField: l.KeyId, + CriticalOptionsField: l.CriticalOptions, + ExtensionsField: l.Extensions, + }, + fieldMaskPaths, + []string{keyBitsField}, + ) + + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsUpdated int + var returnedCredentialLibrary *SSHCertificateCredentialLibrary + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(rr db.Reader, w db.Writer) error { + var msgs []*oplog.Message + ticket, err := w.GetTicket(ctx, l) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + l := l.clone() + var lOplogMsg oplog.Message + + // Update the credential library table + switch { + case len(dbMask) == 0 && len(nullFields) == 0: + // the credential library's fields are not being updated, + // just one of it's child objects, so we just need to + // update the library's version. + l.Version = version + 1 + rowsUpdated, err = w.Update(ctx, l, []string{"Version"}, nil, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library version")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library version and %d rows updated", rowsUpdated)) + } + default: + rowsUpdated, err = w.Update(ctx, l, dbMask, nullFields, db.NewOplogMsg(&lOplogMsg), db.WithVersion(&version)) + if err != nil { + if errors.IsUniqueError(err) { + return errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId)) + } + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + switch rowsUpdated { + case 1: + case 0: + return nil + default: + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated credential library and %d rows updated", rowsUpdated)) + } + } + msgs = append(msgs, &lOplogMsg) + + metadata := l.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + + accl := allocSSHCertificateCredentialLibrary() + accl.PublicId = l.PublicId + if err = rr.LookupById(ctx, accl); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library")) + } + returnedCredentialLibrary = accl + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", l.Name, l.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(l.PublicId)) + } + + return returnedCredentialLibrary, rowsUpdated, nil +} + +// LookupSSHCertificateCredentialLibrary returns the SSHCertificateCredentialLibrary for publicId. +// Returns nil, nil if no SSHCertificateCredentialLibrary is found for publicId. +func (r *Repository) LookupSSHCertificateCredentialLibrary(ctx context.Context, publicId string, _ ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).LookupCredentialLibrary" + if publicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + l := allocSSHCertificateCredentialLibrary() + l.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, l); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", publicId))) + } + return l, nil +} + +// ListSSHCertificateCredentialLibraries returns a slice of SSHCertificateCredentialLibraries for the +// storeId. WithLimit is the only option supported. +func (r *Repository) ListSSHCertificateCredentialLibraries(ctx context.Context, storeId string, opt ...Option) ([]*SSHCertificateCredentialLibrary, error) { + const op = "vault.(Repository).ListSSHCertificateCredentialLibraries" + if storeId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no storeId") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var libs []*SSHCertificateCredentialLibrary + err := r.reader.SearchWhere(ctx, &libs, "store_id = ?", []any{storeId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return libs, nil +} + +// DeleteSSHCertificateCredentialLibrary deletes publicId from the repository and returns +// the number of records deleted. +func (r *Repository) DeleteSSHCertificateCredentialLibrary(ctx context.Context, projectId string, publicId string, _ ...Option) (int, error) { + const op = "vault.(Repository).DeleteSSHCertificateCredentialLibrary" + if publicId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + if projectId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + + l := allocSSHCertificateCredentialLibrary() + l.PublicId = publicId + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dl := l.clone() + rowsDeleted, err = w.Delete(ctx, dl, db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_DELETE))) + if err == nil && rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 CredentialLibrary would have been deleted") + } + return err + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", l.PublicId))) + } + + return rowsDeleted, nil +} diff --git a/internal/credential/vault/repository_ssh_certificate_credential_library_test.go b/internal/credential/vault/repository_ssh_certificate_credential_library_test.go new file mode 100644 index 000000000..55898d7b3 --- /dev/null +++ b/internal/credential/vault/repository_ssh_certificate_credential_library_test.go @@ -0,0 +1,1948 @@ +package vault + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "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/scheduler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_CreateSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + tests := []struct { + name string + in *SSHCertificateCredentialLibrary + opts []Option + want *SSHCertificateCredentialLibrary + wantErr errors.Code + }{ + { + name: "nil-SSHCertificateCredentialLibrary", + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-SSHCertificateCredentialLibrary", + in: &SSHCertificateCredentialLibrary{}, + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-store-id", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary("", "/ssh/sign/foo", "name") + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-public-id-set", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + ) + s.PublicId = "abcd_OOOOOOOOOO" + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-no-vault-path", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "", + "name", + ) + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "invalid-vault-path-invalid-endpoint", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/not-sign/foo", + "name", + ) + return s + }(), + wantErr: errors.CheckConstraint, + }, + { + name: "invalid-no-username", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "", + ) + return s + }(), + wantErr: errors.InvalidParameter, + }, + { + name: "valid-no-options", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-with-name", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "test-name-repo", + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-with-description", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithDescription("test-description-repo"), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + Description: "test-description-repo", + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + }, + }, + }, + { + name: "valid-key-type-ed25519", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: KeyBitsDefault, + }, + }, + }, + { + name: "valid-key-type-ecdsa-0", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + }, + }, + }, + { + name: "valid-key-type-ecdsa-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + }, + }, + }, + { + name: "valid-key-type-ecdsa-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa384, + }, + }, + }, + { + name: "valid-key-type-ecdsa-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa521, + }, + }, + }, + { + name: "valid-key-type-rsa-0", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsDefault), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + }, + { + name: "valid-key-type-rsa-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + }, + { + name: "valid-key-type-rsa-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa3072, + }, + }, + }, + { + name: "valid-key-type-rsa-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa4096, + }, + }, + }, + { + name: "invalid-key-type-key-bits-ecdsa-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ecdsa-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ecdsa-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-rsa-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-256", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-384", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-521", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsEcdsa521), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-2048", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa2048), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-3072", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "invalid-key-type-key-bits-ed25519-4096", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + WithKeyBits(KeyBitsRsa4096), + ) + return s + }(), + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "valid-critical-options", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithCriticalOptions(`{"force-command": "/bin/foo"}`), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + CriticalOptions: `{"force-command": "/bin/foo"}`, + }, + }, + }, + { + name: "valid-extensions", + in: func() *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + cs.GetPublicId(), + "/ssh/sign/foo", + "name", + WithExtensions(`{"permit-X11-forwarding": "", "permit-pty": ""}`), + ) + return s + }(), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeEd25519, + KeyBits: 0, + Extensions: `{"permit-X11-forwarding": "", "permit-pty": ""}`, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(err) + require.NotNil(repo) + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in, tt.opts...) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.Name) + assert.Equal(tt.want.Description, got.Description) + assert.Equal(tt.want.KeyType, got.KeyType) + assert.Equal(tt.want.KeyBits, got.KeyBits) + assert.Equal(tt.want.KeyId, got.KeyId) + assert.Equal(tt.want.Ttl, got.Ttl) + assert.Equal(tt.want.CriticalOptions, got.CriticalOptions) + assert.Equal(tt.want.Extensions, got.Extensions) + assert.Equal(got.CredentialType(), credential.SshCertificateType) + assert.Equal(got.CreateTime, got.UpdateTime) + + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(err) + require.NotNil(repo) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + KeyType: KeyTypeEd25519, + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + Username: "name", + }, + } + + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err: %q got: %q", errors.NotUnique, err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-stores", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + csA, csB := css[0], css[1] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEd25519, + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + Username: "name", + }, + } + in2 := in.clone() + + in.StoreId = csA.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.StoreId = csB.GetPublicId() + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in2) + require.NoError(err) + require.NotNil(got2) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got2.GetPublicId()) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) +} + +func TestRepository_LookupSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + cs := css[0] + csWithExpiredToken := css[1] + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []any{ExpiredToken, csWithExpiredToken.Token().TokenHmac}) + require.NoError(t, err) + require.Equal(t, 1, rows) + + tests := []struct { + name string + in *SSHCertificateCredentialLibrary + }{ + { + name: "valid-no-options", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + }, + + { + name: "valid-with-expired-cred-store-token", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: csWithExpiredToken.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + }, + { + name: "valid-ssh-certificate-credential-type", + in: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: cs.GetPublicId(), + VaultPath: "/ssh/sign/foo", + Username: "name", + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + assert.NoError(err) + require.NotEmpty(orig) + // test + got, err := repo.LookupSSHCertificateCredentialLibrary(ctx, orig.GetPublicId()) + assert.NoError(err) + require.NotEmpty(got) + assert.Equal(orig.Name, got.Name) + assert.Equal(orig.Description, got.Description) + assert.Equal(orig.CredentialType(), got.CredentialType()) + }) + } + } + + t.Run("empty-public-id", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + // test + got, err := repo.LookupCredentialLibrary(ctx, "") + wantErr := errors.InvalidParameter + assert.Truef(errors.Match(errors.T(wantErr), err), "want err: %q got: %q", wantErr, err) + assert.Nil(got) + }) + + t.Run("not-found", func(t *testing.T) { + // setup + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + badId, err := newSSHCertificateCredentialLibraryId() + assert.NoError(err) + require.NotNil(badId) + // test + got, err := repo.LookupCredentialLibrary(ctx, badId) + assert.NoError(err) + assert.Empty(got) + }) +} + +func TestRepository_ListSSHCertificateCredentialLibraries_Limits(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + sche := scheduler.TestScheduler(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + const count = 10 + libs := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), count) + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "with-no-limits", + wantLen: count, + }, + { + name: "with-repo-limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "with-negative-repo-limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "with-list-limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "with-negative-list-limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "with-repo-smaller-than-list-limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "with-repo-larger-than-list-limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSSHCertificateCredentialLibraries(ctx, libs[0].StoreId, tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} + +func TestRepository_UpdateSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + changeVaultPath := func(p string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.VaultPath = p + return l + } + } + + changeName := func(n string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Name = n + return l + } + } + + changeDescription := func(d string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Description = d + return l + } + } + + changeUsername := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Username = s + return l + } + } + + changeKeyType := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyType = s + return l + } + } + + changeKeyBits := func(b uint32) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyBits = b + return l + } + } + + changeTtl := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Ttl = s + return l + } + } + + changeKeyId := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.KeyId = s + return l + } + } + + changeCriticalOptions := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.CriticalOptions = s + return l + } + } + + changeExtensions := func(s string) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.Extensions = s + return l + } + } + + makeNil := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return nil + } + } + + makeEmbeddedNil := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return &SSHCertificateCredentialLibrary{} + } + } + + deletePublicId := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.PublicId = "" + return l + } + } + + nonExistentPublicId := func() func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + l.PublicId = "abcd_OOOOOOOOOO" + return l + } + } + + combine := func(fns ...func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary) func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + return func(l *SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary { + for _, fn := range fns { + l = fn(l) + } + return l + } + } + + tests := []struct { + name string + origFn func(string) *SSHCertificateCredentialLibrary + chgFn func(*SSHCertificateCredentialLibrary) *SSHCertificateCredentialLibrary + masks []string + want *SSHCertificateCredentialLibrary + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: makeNil(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: makeEmbeddedNil(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: deletePublicId(), + masks: []string{nameField, descriptionField}, + wantErr: errors.InvalidPublicId, + }, + { + name: "updating-non-existent-credential-library", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: combine(nonExistentPublicId(), changeName("test-update-name-repo")), + masks: []string{nameField}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "StoreId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + ) + return s + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{nameField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithDescription("test-description-repo"), + ) + return s + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{descriptionField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{nameField, descriptionField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-username", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeUsername("update-name"), + masks: []string{usernameField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "update-name", + }, + }, + wantCount: 1, + }, + { + name: "change-keytype", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEd25519), + ) + return s + }, + chgFn: combine(changeKeyType(KeyTypeEcdsa), changeKeyBits(KeyBitsDefault)), + masks: []string{keyTypeField, keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsEcdsa384), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa384, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits-to-default-ecdsa", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa384), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsDefault), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa256, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keybits-to-default-rsa", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeRsa), + WithKeyBits(KeyBitsRsa3072), + ) + return s + }, + chgFn: changeKeyBits(KeyBitsDefault), + masks: []string{keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-ttl", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithTtl("10s"), + ) + return s + }, + chgFn: changeTtl("1h"), + masks: []string{ttlField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Ttl: "1h", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-keyid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyId("id"), + ) + return s + }, + chgFn: changeKeyId("update-id"), + masks: []string{keyIdField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + KeyId: "update-id", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-critical-options", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithCriticalOptions("options"), + ) + return s + }, + chgFn: changeCriticalOptions("update-options"), + masks: []string{CriticalOptionsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + CriticalOptions: "update-options", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-extensions", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithExtensions("extensions"), + ) + return s + }, + chgFn: changeExtensions("update-extensions"), + masks: []string{ExtensionsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Extensions: "update-extensions", + VaultPath: "/ssh/sign/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{nameField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{descriptionField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{descriptionField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithName("test-name-repo"), + WithDescription("test-description-repo"), + ) + return s + }, + masks: []string{nameField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + Name: "test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-vault-path", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath("/ssh/issue/foo"), + masks: []string{vaultPathField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/issue/foo", + Username: "name", + }, + }, + wantCount: 1, + }, + { + name: "change-vault-path-invalid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath("/ssh/not-sign/foo"), + masks: []string{vaultPathField}, + wantErr: errors.CheckConstraint, + }, + { + name: "delete-vault-path", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + ) + return s + }, + chgFn: changeVaultPath(""), + masks: []string{vaultPathField}, + wantErr: errors.NotNull, + }, + { + name: "change-key-type-invalid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: changeKeyType(KeyTypeEd25519), + masks: []string{keyTypeField}, + wantErr: errors.NotSpecificIntegrity, + }, + { + name: "change-key-type-bits-valid", + origFn: func(csId string) *SSHCertificateCredentialLibrary { + s, _ := NewSSHCertificateCredentialLibrary( + csId, + "/ssh/sign/foo", + "name", + WithKeyType(KeyTypeEcdsa), + WithKeyBits(KeyBitsEcdsa256), + ) + return s + }, + chgFn: combine(changeKeyType(KeyTypeRsa), changeKeyBits(KeyBitsRsa2048)), + masks: []string{keyTypeField, keyBitsField}, + want: &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + VaultPath: "/ssh/sign/foo", + Username: "name", + KeyType: KeyTypeRsa, + KeyBits: KeyBitsRsa2048, + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + ttOrig := tt.origFn(cs.GetPublicId()) + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), ttOrig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + got, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 1, tt.masks) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(ttOrig.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(ttOrig, got) + assert.Equal(ttOrig.StoreId, got.StoreId) + underlyingDB, err := conn.SqlDB(ctx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + + switch tt.want.Name { + case "": + dbassert.IsNull(got, "name") + default: + assert.Equal(tt.want.Name, got.Name) + } + + switch tt.want.Description { + case "": + dbassert.IsNull(got, "description") + default: + assert.Equal(tt.want.Description, got.Description) + } + + switch tt.want.Username { + case "": + dbassert.IsNull(got, "Username") + default: + assert.Equal(tt.want.Username, got.Username) + } + + switch tt.want.KeyType { + case "": + assert.Equal(got.KeyType, KeyTypeEd25519) + default: + assert.Equal(tt.want.KeyType, got.KeyType) + } + + switch tt.want.KeyBits { + case (0): + assert.Zero(got.KeyBits) + default: + assert.Equal(tt.want.KeyBits, got.KeyBits) + } + + switch tt.want.Ttl { + case "": + dbassert.IsNull(got, "Ttl") + default: + assert.Equal(tt.want.Ttl, got.Ttl) + } + + switch tt.want.KeyId { + case "": + dbassert.IsNull(got, "KeyId") + default: + assert.Equal(tt.want.KeyId, got.KeyId) + } + + switch tt.want.CriticalOptions { + case "": + dbassert.IsNull(got, "CriticalOptions") + default: + assert.Equal(tt.want.CriticalOptions, got.CriticalOptions) + } + + switch tt.want.Extensions { + case "": + dbassert.IsNull(got, "Extensions") + default: + assert.Equal(tt.want.Extensions, got.Extensions) + } + + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + name := "test-dup-name" + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + libs := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 2) + + lA, lB := libs[0], libs[1] + + lA.Name = name + got1, gotCount1, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lA, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got1) + assert.Equal(name, got1.Name) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, lA.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + + lB.Name = name + got2, gotCount2, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lB, 1, []string{"name"}) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + assert.Equal(db.NoRowsAffected, gotCount2, "row count") + err = db.TestVerifyOplog(t, rw, lB.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + + t.Run("valid-duplicate-names-diff-CredentialStores", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + csA, csB := css[0], css[1] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + }, + } + in2 := in.clone() + + in.StoreId = csA.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2.StoreId = csB.GetPublicId() + in2.Name = "first-name" + got2, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + got2.Name = got.Name + got3, gotCount3, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), got2, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got3) + assert.NotSame(got2, got3) + assert.Equal(got.Name, got3.Name) + assert.Equal(got2.Description, got3.Description) + assert.Equal(1, gotCount3, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, got2.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("valid-update-with-expired-store-token", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1) + cs := css[0] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + Name: "test-name-repo", + }, + } + + in.StoreId = cs.GetPublicId() + got, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(got) + + // Expire the credential store Vault token + rows, err := rw.Exec(context.Background(), + "update credential_vault_token set status = ? where token_hmac = ?", + []any{ExpiredToken, cs.Token().TokenHmac}) + require.NoError(err) + require.Equal(1, rows) + + got.Name = "new-name" + updated, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), got, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(updated) + assert.Equal("new-name", updated.Name) + assert.Equal(1, gotCount) + }) + + t.Run("change-project-id", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + css := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + + csA, csB := css[0], css[1] + + lA := TestSSHCertificateCredentialLibraries(t, conn, wrapper, csA.GetPublicId(), 1)[0] + lB := TestSSHCertificateCredentialLibraries(t, conn, wrapper, csB.GetPublicId(), 1)[0] + + assert.NotEqual(lA.StoreId, lB.StoreId) + orig := lA.clone() + + lA.StoreId = lB.StoreId + assert.Equal(lA.StoreId, lB.StoreId) + + got1, gotCount1, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lA, 1, []string{"name"}) + + assert.NoError(err) + require.NotNil(got1) + assert.Equal(orig.StoreId, got1.StoreId) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, lA.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("change-key-type-bits-consecutive", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + + in := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + Username: "name", + VaultPath: "/ssh/sign/foo", + KeyType: KeyTypeEcdsa, + KeyBits: KeyBitsEcdsa521, + }, + } + in.StoreId = cs.GetPublicId() + + orig, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), in) + assert.NoError(err) + require.NotNil(orig) + + orig.KeyBits = KeyBitsEcdsa256 + + got, gotCount, err := repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 1, []string{keyBitsField}) + require.NoError(err) + assert.Empty(in.PublicId) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + orig.KeyType = KeyTypeRsa + orig.KeyBits = KeyBitsDefault + + got, gotCount, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 2, []string{keyTypeField, keyBitsField}) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + orig.KeyBits = KeyBitsRsa3072 + + got, gotCount, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), orig, 3, []string{keyTypeField}) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, SSHCertificateCredentialLibraryPrefix, got.GetPublicId()) + assert.Equal(1, gotCount, "row count") + assert.NotSame(orig, got) + assert.Equal(orig.StoreId, got.StoreId) + + assert.Equal(KeyTypeRsa, orig.KeyType) + assert.Equal(uint32(KeyBitsRsa3072), orig.KeyBits) + }) +} + +func TestRepository_DeleteSSHCertificateCredentialLibrary(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + l := TestSSHCertificateCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 1)[0] + + badId, err := newSSHCertificateCredentialLibraryId() + require.NoError(t, err) + require.NotNil(t, badId) + + tests := []struct { + name string + in string + want int + wantErr errors.Code + }{ + { + name: "found", + in: l.GetPublicId(), + want: 1, + }, + { + name: "not-found", + in: badId, + }, + { + name: "empty-public-id", + in: "", + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, sche) + assert.NoError(err) + require.NotNil(repo) + + got, err := repo.DeleteSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), tt.in) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + assert.Equal(tt.want, got, "row count") + + cl, err := repo.LookupSSHCertificateCredentialLibrary(ctx, tt.in) + assert.Empty(err) + assert.Empty(cl) + }) + } + } +} diff --git a/internal/credential/vault/setup_manual_test.go b/internal/credential/vault/setup_manual_test.go index aa4fff3fc..def2593c9 100644 --- a/internal/credential/vault/setup_manual_test.go +++ b/internal/credential/vault/setup_manual_test.go @@ -74,7 +74,7 @@ func TestSetupSleepyDevEnvironment(t *testing.T) { require.NoError(t, err) require.NotNil(t, store) libClient := credentiallibraries.NewClient(client) - lib, err := libClient.Create(ctx, store.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath(path.Join("database", "creds", "opened")), + lib, err := libClient.Create(ctx, store.Item.Type, store.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath(path.Join("database", "creds", "opened")), credentiallibraries.WithVaultCredentialLibraryHttpMethod("GET"), ) require.NoError(t, err) diff --git a/internal/credential/vault/ssh_certificate_credential_library.go b/internal/credential/vault/ssh_certificate_credential_library.go new file mode 100644 index 000000000..9d78bf5c4 --- /dev/null +++ b/internal/credential/vault/ssh_certificate_credential_library.go @@ -0,0 +1,120 @@ +package vault + +import ( + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +const ( + KeyTypeEcdsa = "ecdsa" + KeyTypeEd25519 = "ed25519" + KeyTypeRsa = "rsa" + + KeyBitsDefault = 0 + + KeyBitsEcdsa256 = 256 + KeyBitsEcdsa384 = 384 + KeyBitsEcdsa521 = 521 + + KeyBitsRsa2048 = 2048 + KeyBitsRsa3072 = 3072 + KeyBitsRsa4096 = 4096 +) + +// SSHCertificateCredentialLibrary is a credential library that issues +// ssh certificate using the vault ssh secret engine. +// See: https://developer.hashicorp.com/vault/api-docs/secret/ssh#sign-ssh-key +type SSHCertificateCredentialLibrary struct { + *store.SSHCertificateCredentialLibrary + tableName string `gorm:"-"` +} + +// NewSSHCertificateCredentialLibrary creates a new in memory SSHCertificateCredentialLibrary +// for a Vault backend at vaultPath assigned to storeId. The SSH username field must be set. +// Name, description, key type, key bits, ttl, key id, critical options, and extensions +// are the only valid options. All other options are ignored. +func NewSSHCertificateCredentialLibrary(storeId string, vaultPath string, username string, opt ...Option) (*SSHCertificateCredentialLibrary, error) { + const op = "vault.NewSSHCertificateCredentialLibrary" + opts := getOpts(opt...) + + l := &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + VaultPath: vaultPath, + Username: username, + KeyType: opts.withKeyType, + KeyBits: opts.withKeyBits, + Ttl: opts.withTtl, + KeyId: opts.withKeyId, + CriticalOptions: opts.withCriticalOptions, + Extensions: opts.withExtensions, + CredentialType: string(credential.SshCertificateType), + }, + } + + return l, nil +} + +func allocSSHCertificateCredentialLibrary() *SSHCertificateCredentialLibrary { + return &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{}, + } +} + +func (l *SSHCertificateCredentialLibrary) clone() *SSHCertificateCredentialLibrary { + cp := proto.Clone(l.SSHCertificateCredentialLibrary) + return &SSHCertificateCredentialLibrary{ + SSHCertificateCredentialLibrary: cp.(*store.SSHCertificateCredentialLibrary), + } +} + +func (l *SSHCertificateCredentialLibrary) setId(i string) { + l.PublicId = i +} + +// TableName returns the table name. +func (l *SSHCertificateCredentialLibrary) TableName() string { + if l.tableName != "" { + return l.tableName + } + return "credential_vault_ssh_cert_library" +} + +// SetTableName sets the table name. +func (l *SSHCertificateCredentialLibrary) SetTableName(n string) { + l.tableName = n +} + +func (l *SSHCertificateCredentialLibrary) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{l.PublicId}, + "resource-type": []string{"credential-vault-ssh-cert-library"}, + "op-type": []string{op.String()}, + } + if l.StoreId != "" { + metadata["store-id"] = []string{l.StoreId} + } + return metadata +} + +func (l *SSHCertificateCredentialLibrary) getDefaultKeyBits() uint32 { + switch l.KeyType { + case KeyTypeEcdsa: + return KeyBitsEcdsa256 + case KeyTypeRsa: + return KeyBitsRsa2048 + default: + return KeyBitsDefault + } +} + +// CredentialType returns the type of credential the library retrieves. +func (l *SSHCertificateCredentialLibrary) CredentialType() credential.Type { + return credential.Type(l.SSHCertificateCredentialLibrary.CredentialType) +} + +var _ credential.Library = (*SSHCertificateCredentialLibrary)(nil) diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index 7a198ac30..06bd6a14e 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -629,6 +629,210 @@ func (x *CredentialLibrary) GetCredentialType() string { return "" } +type SSHCertificateCredentialLibrary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // store_id of the owning vault credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + StoreId string `protobuf:"bytes,6,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // vault_path is the path in Vault to request credentials from. + // It must be set. + // @inject_tag: `gorm:"not_null"` + VaultPath string `protobuf:"bytes,8,opt,name=vault_path,json=vaultPath,proto3" json:"vault_path,omitempty" gorm:"not_null"` + // username is the username to use when making an SSH connection. + // It must be set. + // @inject_tag: `gorm:"not_null"` + Username string `protobuf:"bytes,9,opt,name=username,proto3" json:"username,omitempty" gorm:"not_null"` + // key_type specifies the key type to use when generating an SSH private key. + // Values must be "rsa", "ed25519", or "ecdsa". + // @inject_tag: `gorm:"not_null"` + KeyType string `protobuf:"bytes,10,opt,name=key_type,json=keyType,proto3" json:"key_type,omitempty" gorm:"not_null"` + // key_bits specifies the number of bits to use to generate an SSH private key. + // Not used if key_type is ed25519. + // @inject_tag: `gorm:"not_null"` + KeyBits uint32 `protobuf:"varint,11,opt,name=key_bits,json=keyBits,proto3" json:"key_bits,omitempty" gorm:"not_null"` + // ttl specifies the requested time to live for the certificate. + // @inject_tag: `gorm:"default:null"` + Ttl string `protobuf:"bytes,12,opt,name=ttl,proto3" json:"ttl,omitempty" gorm:"default:null"` + // key_id specifies the key id that the created certificate should have. + // @inject_tag: `gorm:"default:null"` + KeyId string `protobuf:"bytes,13,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"default:null"` + // critical_options specifies a map of the critical options that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + CriticalOptions string `protobuf:"bytes,14,opt,name=critical_options,json=criticalOptions,proto3" json:"critical_options,omitempty" gorm:"default:null"` + // extensions specifies a map of the extensions that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + Extensions string `protobuf:"bytes,15,opt,name=extensions,proto3" json:"extensions,omitempty" gorm:"default:null"` + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + CredentialType string `protobuf:"bytes,16,opt,name=credential_type,json=credentialType,proto3" json:"credential_type,omitempty" gorm:"default:null"` +} + +func (x *SSHCertificateCredentialLibrary) Reset() { + *x = SSHCertificateCredentialLibrary{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SSHCertificateCredentialLibrary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SSHCertificateCredentialLibrary) ProtoMessage() {} + +func (x *SSHCertificateCredentialLibrary) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SSHCertificateCredentialLibrary.ProtoReflect.Descriptor instead. +func (*SSHCertificateCredentialLibrary) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{4} +} + +func (x *SSHCertificateCredentialLibrary) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *SSHCertificateCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *SSHCertificateCredentialLibrary) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SSHCertificateCredentialLibrary) GetVaultPath() string { + if x != nil { + return x.VaultPath + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyType() string { + if x != nil { + return x.KeyType + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyBits() uint32 { + if x != nil { + return x.KeyBits + } + return 0 +} + +func (x *SSHCertificateCredentialLibrary) GetTtl() string { + if x != nil { + return x.Ttl + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCriticalOptions() string { + if x != nil { + return x.CriticalOptions + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetExtensions() string { + if x != nil { + return x.Extensions + } + return "" +} + +func (x *SSHCertificateCredentialLibrary) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + type Credential struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -688,7 +892,7 @@ type Credential struct { func (x *Credential) Reset() { *x = Credential{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -701,7 +905,7 @@ func (x *Credential) String() string { func (*Credential) ProtoMessage() {} func (x *Credential) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -714,7 +918,7 @@ func (x *Credential) ProtoReflect() protoreflect.Message { // Deprecated: Use Credential.ProtoReflect.Descriptor instead. func (*Credential) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{4} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{5} } func (x *Credential) GetPublicId() string { @@ -832,7 +1036,7 @@ type UsernamePasswordOverride struct { func (x *UsernamePasswordOverride) Reset() { *x = UsernamePasswordOverride{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -845,7 +1049,7 @@ func (x *UsernamePasswordOverride) String() string { func (*UsernamePasswordOverride) ProtoMessage() {} func (x *UsernamePasswordOverride) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -858,7 +1062,7 @@ func (x *UsernamePasswordOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use UsernamePasswordOverride.ProtoReflect.Descriptor instead. func (*UsernamePasswordOverride) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{5} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{6} } func (x *UsernamePasswordOverride) GetLibraryId() string { @@ -923,7 +1127,7 @@ type SshPrivateKeyOverride struct { func (x *SshPrivateKeyOverride) Reset() { *x = SshPrivateKeyOverride{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -936,7 +1140,7 @@ func (x *SshPrivateKeyOverride) String() string { func (*SshPrivateKeyOverride) ProtoMessage() {} func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6] + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -949,7 +1153,7 @@ func (x *SshPrivateKeyOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use SshPrivateKeyOverride.ProtoReflect.Descriptor instead. func (*SshPrivateKeyOverride) Descriptor() ([]byte, []int) { - return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{6} + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{7} } func (x *SshPrivateKeyOverride) GetLibraryId() string { @@ -1146,72 +1350,131 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by 0x0f, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, - 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, - 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, - 0x61, 0x63, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xaa, 0x07, 0x0a, 0x1f, 0x53, 0x53, + 0x48, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, + 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 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, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 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, 0x19, 0x0a, 0x08, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x3f, 0x0a, 0x0a, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x20, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x09, 0x56, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x70, 0x61, 0x74, 0x68, 0x52, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, + 0x74, 0x68, 0x12, 0x3f, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x23, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x07, 0x4b, 0x65, 0x79, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x0d, 0x42, 0x22, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x42, + 0x69, 0x74, 0x73, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x42, 0x69, 0x74, + 0x73, 0x12, 0x2b, 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x19, + 0xc2, 0xdd, 0x29, 0x15, 0x0a, 0x03, 0x54, 0x74, 0x6c, 0x12, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x74, 0x74, 0x6c, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x12, 0x35, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, + 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x05, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x11, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x5d, 0x0a, 0x10, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, + 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x32, 0xc2, 0xdd, 0x29, 0x2e, 0x0a, 0x0f, 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x47, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xc2, 0xdd, 0x29, 0x23, 0x0a, 0x0a, + 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x15, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, + 0x0f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x22, 0xc3, 0x04, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x48, 0x6d, 0x61, 0x63, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 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, 0x0f, - 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, - 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, - 0x97, 0x01, 0x0a, 0x18, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x53, 0x73, - 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x4f, 0x76, 0x65, 0x72, 0x72, - 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x13, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x1d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, - 0x68, 0x72, 0x61, 0x73, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, - 0x5a, 0x43, 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, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x49, 0x64, 0x12, 0x56, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x6e, + 0x65, 0x77, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 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, 0x0f, 0x6c, 0x61, 0x73, + 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x53, 0x0a, 0x0f, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x65, 0x6e, 0x65, 0x77, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x97, 0x01, 0x0a, + 0x18, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, + 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, + 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x53, 0x73, 0x68, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x2d, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x32, + 0x0a, 0x15, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, + 0x73, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, 0x5a, 0x43, 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, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1226,36 +1489,39 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP() return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescData } -var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []interface{}{ - (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore - (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token - (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate - (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary - (*Credential)(nil), // 4: controller.storage.credential.vault.store.v1.Credential - (*UsernamePasswordOverride)(nil), // 5: controller.storage.credential.vault.store.v1.UsernamePasswordOverride - (*SshPrivateKeyOverride)(nil), // 6: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride - (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp + (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore + (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token + (*ClientCertificate)(nil), // 2: controller.storage.credential.vault.store.v1.ClientCertificate + (*CredentialLibrary)(nil), // 3: controller.storage.credential.vault.store.v1.CredentialLibrary + (*SSHCertificateCredentialLibrary)(nil), // 4: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary + (*Credential)(nil), // 5: controller.storage.credential.vault.store.v1.Credential + (*UsernamePasswordOverride)(nil), // 6: controller.storage.credential.vault.store.v1.UsernamePasswordOverride + (*SshPrivateKeyOverride)(nil), // 7: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride + (*timestamp.Timestamp)(nil), // 8: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 7, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 9: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 10: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 11: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 12: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 8, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 14: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_controller_storage_credential_vault_store_v1_vault_proto_init() } @@ -1313,7 +1579,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Credential); i { + switch v := v.(*SSHCertificateCredentialLibrary); i { case 0: return &v.state case 1: @@ -1325,7 +1591,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UsernamePasswordOverride); i { + switch v := v.(*Credential); i { case 0: return &v.state case 1: @@ -1337,6 +1603,18 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { } } file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UsernamePasswordOverride); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SshPrivateKeyOverride); i { case 0: return &v.state @@ -1355,7 +1633,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/vault/supported.go b/internal/credential/vault/supported.go index 90b040217..37a8563a0 100644 --- a/internal/credential/vault/supported.go +++ b/internal/credential/vault/supported.go @@ -75,9 +75,14 @@ func gotNewServer(t testing.TB, opt ...TestOption) *TestVaultServer { pool: pool, } + vaultVersion := DefaultVaultVersion + if opts.vaultVersion != "" { + vaultVersion = opts.vaultVersion + } + dockerOptions := &dockertest.RunOptions{ Repository: "vault", - Tag: DefaultVaultVersion, + Tag: vaultVersion, Env: []string{fmt.Sprintf("VAULT_DEV_ROOT_TOKEN_ID=%s", server.RootToken)}, } diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index bec523201..acdac4c60 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -140,6 +140,38 @@ func TestCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, stor return libs } +// TestSSHCertificateCredentialLibraries creates count number of vault ssh +// certificate credential libraries in the provided DB with the provided store +// id. If any errors are encountered during the creation of the credential +// libraries, the test will fail. +func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*SSHCertificateCredentialLibrary { + t.Helper() + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + var libs []*SSHCertificateCredentialLibrary + + for i := 0; i < count; i++ { + lib, err := NewSSHCertificateCredentialLibrary(storeId, fmt.Sprintf("ssh/sign/role-%d", i), "username", WithKeyType(KeyTypeEd25519)) + assert.NoError(err) + require.NotNil(lib) + id, err := newSSHCertificateCredentialLibraryId() + assert.NoError(err) + require.NotEmpty(id) + lib.PublicId = id + + ctx := context.Background() + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + return iw.Create(ctx, lib) + }, + ) + + require.NoError(err2) + libs = append(libs, lib) + } + return libs +} + // TestCredentials creates count number of vault credentials in the provided DB with // the provided library id and session id. If any errors are encountered // during the creation of the credentials, the test will fail. @@ -168,7 +200,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar var credentials []*Credential for i := 0; i < count; i++ { - credential, err := newCredential(lib.GetPublicId(), sessionId, fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) + credential, err := newCredential(lib.GetPublicId(), fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute) assert.NoError(err) require.NotNil(credential) @@ -177,7 +209,7 @@ func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, librar require.NotNil(id) credential.PublicId = id - query, queryValues := credential.insertQuery() + query, queryValues := insertQuery(credential, sessionId) rows, err2 := rw.Exec(ctx, query, queryValues) assert.Equal(1, rows) assert.NoError(err2) @@ -477,17 +509,19 @@ type TestOption func(testing.TB, *testOptions) // options = how options are represented type testOptions struct { - orphan bool - periodic bool - renewable bool - policies []string - mountPath string - roleName string - vaultTLS TestVaultTLS - dockerNetwork bool - skipCleanup bool - tokenPeriod time.Duration - clientKey *ecdsa.PrivateKey + orphan bool + periodic bool + renewable bool + policies []string + allowedExtensions []string + mountPath string + roleName string + vaultTLS TestVaultTLS + dockerNetwork bool + skipCleanup bool + tokenPeriod time.Duration + clientKey *ecdsa.PrivateKey + vaultVersion string } func getDefaultTestOptions(t testing.TB) testOptions { @@ -593,6 +627,24 @@ func WithDontCleanUp() TestOption { } } +// WithVaultVersion sets the version of vault that will be started. +// defaults to the value stored in vault.supported.DefaultVaultVersion +func WithVaultVersion(s string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.vaultVersion = s + } +} + +// WithAllowedExtension tells vault to allow a specific SSH extension +// to be used by vault's ssh secrets engine +func WithAllowedExtension(e string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.allowedExtensions = append(o.allowedExtensions, e) + } +} + // TestOrphanToken sets the token orphan option to b. // The orphan option is true by default. func TestOrphanToken(b bool) TestOption { @@ -752,6 +804,95 @@ func (v *TestVaultServer) addPolicy(t testing.TB, name string, pc pathCapabiliti require.NoError(vc.Sys().PutPolicy(name, policy)) } +// MountSSH mounts the Vault SSH secret engine and initializes it by +// generating a root certificate authority and creating a default role on +// the mount. The root CA is returned. +// +// The default mount path is ssh and the default role name is boundary. +// WithTestMountPath, WithTestRoleName, and WithAllowedExtension are the +// test options supported. extensions are combined into allowed_extensions. +// allowed_extensions defaults to "*" (allow all) +// The role is defined as: +// +// { +// "key_type": "ca", +// "allowed_users": "*", +// "allowed_extensions": "*", +// "allow_user_certificates": true, +// "ttl": "12h0m0s" +// } +// +// MountSSH also adds a Vault policy named 'ssh' to v and adds it to the +// standard set of polices attached to tokens created with v.CreateToken. +// The policy is defined as: +// +// path "mountPath/*" { +// capabilities = ["create", "read", "update", "delete", "list"] +// } +func (v *TestVaultServer) MountSSH(t testing.TB, opt ...TestOption) *vault.Secret { + require := require.New(t) + opts := getTestOpts(t, opt...) + vc := v.client(t).cl + + // Mount SSH + maxTTL := 24 * time.Hour + if t, ok := t.(*testing.T); ok { + if deadline, ok := t.Deadline(); ok { + maxTTL = time.Until(deadline) * 2 + } + } + + defaultTTL := maxTTL / 2 + t.Logf("maxTTL: %s, defaultTTL: %s", maxTTL, defaultTTL) + mountInput := &vault.MountInput{ + Type: "ssh", + Description: t.Name(), + Config: vault.MountConfigInput{ + DefaultLeaseTTL: defaultTTL.String(), + MaxLeaseTTL: maxTTL.String(), + }, + } + mountPath := opts.mountPath + if mountPath == "" { + mountPath = "ssh/" + } + // this is the call that actually enables the secrets engine + require.NoError(vc.Sys().Mount(mountPath, mountInput)) + + policyPath := fmt.Sprintf("%s*", mountPath) + pc := pathCapabilities{ + policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability, + } + v.addPolicy(t, "ssh", pc) + + // Generate a root CA + caPath := path.Join(mountPath, "config/ca") + caOptions := map[string]any{ + "generate_signing_key": true, + } + s, err := vc.Logical().Write(caPath, caOptions) + require.NoError(err) + require.NotEmpty(s) + + // Create default role + rolePath := path.Join(mountPath, "roles", opts.roleName) + allowExtensions := "*" + if len(opts.allowedExtensions) > 0 { + allowExtensions = strings.Join(opts.allowedExtensions, ",") + } + roleOptions := map[string]any{ + "key_type": "ca", + "allowed_users": "*", + "allowed_extensions": allowExtensions, + "allow_user_certificates": true, + "ttl": defaultTTL.String(), + } + _, err = vc.Logical().Write(rolePath, roleOptions) + require.NoError(err) + + return s +} + // MountPKI mounts the Vault PKI secret engine and initializes it by // generating a root certificate authority and creating a default role on // the mount. The root CA is returned. diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index c8c3d03e1..d85d6f515 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -2,6 +2,7 @@ package credentiallibraries import ( "context" + "encoding/json" "fmt" "strings" @@ -33,6 +34,11 @@ const ( httpMethodField = "attributes.http_method" httpRequestBodyField = "attributes.http_request_body" credentialMappingPathField = "credential_mapping_overrides" + sshCertUsernameField = "attributes.username" + keyTypeField = "attributes.key_type" + keyBitsField = "attributes.key_bits" + criticalOptionsField = "attributes.critical_options" + extensionsField = "attributes.extensions" domain = "credential" ) @@ -45,7 +51,8 @@ const ( ) var ( - maskManager handlers.MaskManager + maskManager handlers.MaskManager + sshCertMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -62,6 +69,18 @@ var ( action.Create, action.List, } + + validCredentialTypesVaultGeneric = []credential.Type{ + credential.UsernamePasswordType, + credential.SshPrivateKeyType, + credential.UnspecifiedType, + } + + validKeyTypes = []string{ + vault.KeyTypeEcdsa, + vault.KeyTypeEd25519, + vault.KeyTypeRsa, + } ) func init() { @@ -70,6 +89,10 @@ func init() { handlers.MaskSource{&pb.CredentialLibrary{}, &pb.VaultCredentialLibraryAttributes{}}); err != nil { panic(err) } + if sshCertMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.SSHCertificateCredentialLibrary{}}, + handlers.MaskSource{&pb.CredentialLibrary{}, &pb.VaultSSHCertificateCredentialLibraryAttributes{}}); err != nil { + panic(err) + } } // Service handles request as described by the pbs.CredentialLibraryServiceServer interface. @@ -242,11 +265,23 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre if err != nil { return nil, err } - cur, err := repo.LookupCredentialLibrary(ctx, req.Id) - if err != nil { - return nil, err + var currentCredentialType credential.Type + var mo vault.MappingOverride + switch subtypes.SubtypeFromId(domain, req.GetId()) { + case vault.SSHCertificateLibrarySubtype: + cur, err := repo.LookupSSHCertificateCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType = credential.Type(cur.GetCredentialType()) + default: + cur, err := repo.LookupCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType = credential.Type(cur.GetCredentialType()) + mo = cur.MappingOverride } - currentCredentialType := credential.Type(cur.GetCredentialType()) if err := validateUpdateRequest(req, currentCredentialType); err != nil { return nil, err @@ -255,7 +290,7 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre if authResults.Error != nil { return nil, authResults.Error } - cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem(), currentCredentialType, cur.MappingOverride) + cl, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem(), currentCredentialType, mo) if err != nil { return nil, err } @@ -298,16 +333,28 @@ func (s Service) DeleteCredentialLibrary(ctx context.Context, req *pbs.DeleteCre return nil, nil } -func (s Service) listFromRepo(ctx context.Context, storeId string) ([]*vault.CredentialLibrary, error) { +func (s Service) listFromRepo(ctx context.Context, storeId string) ([]credential.Library, error) { const op = "credentiallibraries.(Service).listFromRepo" repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) } - csl, err := repo.ListCredentialLibraries(ctx, storeId) + genCsl, err := repo.ListCredentialLibraries(ctx, storeId) if err != nil { return nil, errors.Wrap(ctx, err, op) } + certCsl, err := repo.ListSSHCertificateCredentialLibraries(ctx, storeId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + csl := make([]credential.Library, 0, len(genCsl)+len(certCsl)) + for _, s := range genCsl { + csl = append(csl, s) + } + for _, s := range certCsl { + csl = append(csl, s) + } return csl, nil } @@ -317,32 +364,67 @@ func (s Service) getFromRepo(ctx context.Context, id string) (credential.Library if err != nil { return nil, errors.Wrap(ctx, err, op) } - cs, err := repo.LookupCredentialLibrary(ctx, id) - if err != nil && !errors.IsNotFoundError(err) { - return nil, errors.Wrap(ctx, err, op) + switch subtypes.SubtypeFromId(domain, id) { + case vault.GenericLibrarySubtype: + cs, err := repo.LookupCredentialLibrary(ctx, id) + if err != nil && !errors.IsNotFoundError(err) { + return nil, errors.Wrap(ctx, err, op) + } + if cs == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("credential library %q not found", id)) + } + return cs, err + case vault.SSHCertificateLibrarySubtype: + cs, err := repo.LookupSSHCertificateCredentialLibrary(ctx, id) + if err != nil && !errors.IsNotFoundError(err) { + return nil, errors.Wrap(ctx, err, op) + } + if cs == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("ssh certificate credential library %q not found", id)) + } + return cs, err } - if cs == nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("credential library %q not found", id)) - } - return cs, err + return nil, errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential library subtype") } func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.CredentialLibrary) (credential.Library, error) { const op = "credentiallibraries.(Service).createInRepo" - cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - repo, err := s.repoFn() - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - out, err := repo.CreateCredentialLibrary(ctx, scopeId, cl) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential library")) - } - if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential library but no error returned from repository.") + var out credential.Library + switch subtypes.SubtypeFromType(domain, item.GetType()) { + case vault.SSHCertificateLibrarySubtype: + cl, err := toStorageVaultSSHCertificateLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rl, err := repo.CreateSSHCertificateCredentialLibrary(ctx, scopeId, cl) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create ssh certificate credential library")) + } + if rl == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ssh certificate credential library but no error returned from repository.") + } + out = rl + default: + cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + rl, err := repo.CreateCredentialLibrary(ctx, scopeId, cl) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential library")) + } + if rl == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential library but no error returned from repository.") + } + out = rl } return out, nil } @@ -372,26 +454,53 @@ func (s Service) updateInRepo( item.CredentialMappingOverrides = mappingStruct } - cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - cl.PublicId = id - - dbMasks = append(dbMasks, maskManager.Translate(masks)...) - if len(dbMasks) == 0 { - return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) - } + var out credential.Library + rowsUpdated := 0 repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(ctx, err, op) } - out, rowsUpdated, err := repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) - } - if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + switch subtypes.SubtypeFromId(domain, id) { + case vault.SSHCertificateLibrarySubtype: + dbMasks = append(dbMasks, sshCertMaskManager.Translate(masks)...) + if getMapUpdate(criticalOptionsField, masks) { + dbMasks = append(dbMasks, vault.CriticalOptionsField) + } + if getMapUpdate(extensionsField, masks) { + dbMasks = append(dbMasks, vault.ExtensionsField) + } + if len(dbMasks) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + cl, err := toStorageVaultSSHCertificateLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cl.PublicId = id + out, rowsUpdated, err = repo.UpdateSSHCertificateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + } + default: + dbMasks = append(dbMasks, maskManager.Translate(masks)...) + if len(dbMasks) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + cl, err := toStorageVaultLibrary(item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cl.PublicId = id + out, rowsUpdated, err = repo.UpdateCredentialLibrary(ctx, projId, cl, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential library")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential Library %q doesn't exist or incorrect version provided.", id) + } } return out, nil } @@ -402,7 +511,13 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, if err != nil { return false, err } - rows, err := repo.DeleteCredentialLibrary(ctx, scopeId, id) + rows := 0 + switch subtypes.SubtypeFromId(domain, id) { + case vault.SSHCertificateLibrarySubtype: + rows, err = repo.DeleteSSHCertificateCredentialLibrary(ctx, scopeId, id) + default: + rows, err = repo.DeleteCredentialLibrary(ctx, scopeId, id) + } if err != nil { if errors.IsNotFoundError(err) { return false, nil @@ -428,9 +543,9 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. parentId = id default: opts = append(opts, auth.WithId(id)) - + parentId = id switch subtypes.SubtypeFromId(domain, id) { - case vault.Subtype: + case vault.GenericLibrarySubtype: cl, err := repo.LookupCredentialLibrary(ctx, id) if err != nil { res.Error = err @@ -441,6 +556,17 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return res } parentId = cl.GetStoreId() + case vault.SSHCertificateLibrarySubtype: + cl, err := repo.LookupSSHCertificateCredentialLibrary(ctx, id) + if err != nil { + res.Error = err + return res + } + if cl == nil { + res.Error = handlers.NotFoundError() + return res + } + parentId = cl.GetStoreId() default: res.Error = errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential library subtype from id") return res @@ -515,7 +641,7 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra out.AuthorizedActions = opts.WithAuthorizedActions } switch subtypes.SubtypeFromId(domain, in.GetPublicId()) { - case vault.Subtype: + case vault.GenericLibrarySubtype: vaultIn, ok := in.(*vault.CredentialLibrary) if !ok { return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault credential library") @@ -568,6 +694,47 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra VaultCredentialLibraryAttributes: attrs, } } + case vault.SSHCertificateLibrarySubtype: + vaultIn, ok := in.(*vault.SSHCertificateCredentialLibrary) + if !ok { + return nil, errors.NewDeprecated(errors.Internal, op, "unable to cast to vault ssh certificate credential library") + } + // We don't check for mapping overrides here -- this subtype does not currently support them. + out.CredentialType = vaultIn.GetCredentialType() + if outputFields.Has(globals.AttributesField) { + attrs := &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String(vaultIn.GetVaultPath()), + } + if vaultIn.GetUsername() != "" { + attrs.Username = wrapperspb.String(vaultIn.GetUsername()) + } + if vaultIn.GetKeyType() != "" { + attrs.KeyType = wrapperspb.String(vaultIn.GetKeyType()) + } + if vaultIn.GetKeyBits() != 0 { + attrs.KeyBits = &wrapperspb.UInt32Value{Value: vaultIn.GetKeyBits()} + } + if vaultIn.GetTtl() != "" { + attrs.Ttl = wrapperspb.String(vaultIn.GetTtl()) + } + if vaultIn.GetKeyId() != "" { + attrs.KeyId = wrapperspb.String(vaultIn.GetKeyId()) + } + if vaultIn.GetCriticalOptions() != "" { + co := make(map[string]string) + json.Unmarshal([]byte(vaultIn.GetCriticalOptions()), &co) + attrs.CriticalOptions = co + } + if vaultIn.GetExtensions() != "" { + e := make(map[string]string) + json.Unmarshal([]byte(vaultIn.GetExtensions()), &e) + attrs.Extensions = e + } + out.Attrs = &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: attrs, + } + } + } return &out, nil } @@ -582,7 +749,11 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault opts = append(opts, vault.WithDescription(in.GetDescription().GetValue())) } - attrs := in.GetVaultCredentialLibraryAttributes() + attrs := in.GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = in.GetVaultCredentialLibraryAttributes() + } if attrs.GetHttpMethod() != nil { opts = append(opts, vault.WithMethod(vault.Method(strings.ToUpper(attrs.GetHttpMethod().GetValue())))) } @@ -631,13 +802,66 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault return cs, err } +func toStorageVaultSSHCertificateLibrary(storeId string, in *pb.CredentialLibrary) (out *vault.SSHCertificateCredentialLibrary, err error) { + const op = "credentiallibraries.toStorageVaultSSHCertificateLibrary" + var opts []vault.Option + if in.GetName() != nil { + opts = append(opts, vault.WithName(in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, vault.WithDescription(in.GetDescription().GetValue())) + } + opts = append(opts, vault.WithCredentialType(credential.Type(in.GetCredentialType()))) + + attrs := in.GetVaultSshCertificateCredentialLibraryAttributes() + if attrs.GetKeyType() != nil { + opts = append(opts, vault.WithKeyType(attrs.GetKeyType().GetValue())) + } + if attrs.GetKeyBits() != nil { + opts = append(opts, vault.WithKeyBits(attrs.GetKeyBits().GetValue())) + } + if attrs.GetTtl() != nil { + opts = append(opts, vault.WithTtl(attrs.GetTtl().GetValue())) + } + if attrs.GetKeyId() != nil { + opts = append(opts, vault.WithKeyId(attrs.GetKeyId().GetValue())) + } + if attrs.GetCriticalOptions() != nil { + co, err := json.Marshal(attrs.GetCriticalOptions()) + if err != nil { + return nil, err + } + opts = append(opts, vault.WithCriticalOptions(string(co))) + } + if attrs.GetExtensions() != nil { + e, err := json.Marshal(attrs.GetExtensions()) + if err != nil { + return nil, err + } + opts = append(opts, vault.WithExtensions(string(e))) + } + + cs, err := vault.NewSSHCertificateCredentialLibrary(storeId, attrs.GetPath().GetValue(), attrs.GetUsername().GetValue(), opts...) + if err != nil { + return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential library")) + } + return cs, err +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // - The path passed in is correctly formatted // - All required parameters are set // - There are no conflicting parameters provided func validateGetRequest(req *pbs.GetCredentialLibraryRequest) error { - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix) + prefix := "" + switch subtypes.SubtypeFromId(domain, req.GetId()) { + case vault.SSHCertificateLibrarySubtype: + prefix = vault.SSHCertificateCredentialLibraryPrefix + default: + prefix = vault.CredentialLibraryPrefix + } + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, prefix) } func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { @@ -645,23 +869,82 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { badFields := map[string]string{} switch subtypes.SubtypeFromId(domain, req.GetItem().GetCredentialStoreId()) { case vault.Subtype: - if t := req.GetItem().GetType(); t != "" && subtypes.SubtypeFromType(domain, t) != vault.Subtype { + t := req.GetItem().GetType() + if t == "" { + badFields[globals.TypeField] = "This is a required field." + } + + // support older vault generic libraries with 'vault.Subtype'. + if subtypes.SubtypeFromType(domain, t) == vault.Subtype { + req.GetItem().Type = vault.GenericLibrarySubtype.String() + t = vault.GenericLibrarySubtype.String() + } + + if subtypes.SubtypeFromType(domain, t) != vault.GenericLibrarySubtype && + subtypes.SubtypeFromType(domain, t) != vault.SSHCertificateLibrarySubtype { badFields[globals.CredentialStoreIdField] = "If included, type must match that of the credential store." } - attrs := req.GetItem().GetVaultCredentialLibraryAttributes() - if attrs == nil { - badFields[attributesPathField] = "This is a required field." + + switch subtypes.SubtypeFromType(domain, req.GetItem().GetType()) { + case vault.GenericLibrarySubtype: + isValidCred := false + ct := req.GetItem().GetCredentialType() + for _, t := range validCredentialTypesVaultGeneric { + if ct == "" || ct == string(t) { + isValidCred = true + break + } + } + if !isValidCred { + badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", ct) + } + + attrs := req.GetItem().GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = req.GetItem().GetVaultCredentialLibraryAttributes() + if attrs == nil { + badFields[attributesPathField] = "This is a required field." + } + } + if attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field." + } + if m := attrs.GetHttpMethod(); m != nil && !strutil.StrListContains([]string{"GET", "POST"}, strings.ToUpper(m.GetValue())) { + badFields[httpMethodField] = "If set, value must be 'GET' or 'POST'." + } + if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) != "POST" { + badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) + } + validateMapping(badFields, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) + case vault.SSHCertificateLibrarySubtype: + if req.GetItem().GetCredentialType() != "" { + badFields[globals.CredentialTypeField] = fmt.Sprintf("This field is read only and cannot be set.") + } + + attrs := req.GetItem().GetVaultSshCertificateCredentialLibraryAttributes() + if attrs == nil { + badFields[attributesPathField] = "This is a required field." + } + if attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field." + } + if attrs.GetUsername().GetValue() == "" { + badFields[sshCertUsernameField] = "This is a required field." + } + if (attrs.GetKeyType() == nil) != (attrs.GetKeyBits() == nil) { + if attrs.GetKeyType() != nil && attrs.GetKeyType().GetValue() != vault.KeyTypeEd25519 { + badFields[keyTypeField] = fmt.Sprintf("If set, %q must also be set.", keyBitsField) + } + if attrs.GetKeyBits() != nil { + badFields[keyBitsField] = fmt.Sprintf("If set, %q must also be set.", keyTypeField) + } + } + if t := attrs.GetKeyType(); t != nil && !strutil.StrListContains(validKeyTypes, strings.ToLower(t.GetValue())) { + badFields[keyTypeField] = "If set, value must be 'ed25519', 'ecdsa', or 'rsa'." + } + validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) } - if attrs.GetPath().GetValue() == "" { - badFields[vaultPathField] = "This is a required field." - } - if m := attrs.GetHttpMethod(); m != nil && !strutil.StrListContains([]string{"GET", "POST"}, strings.ToUpper(m.GetValue())) { - badFields[httpMethodField] = "If set, value must be 'GET' or 'POST'." - } - if b := attrs.GetHttpRequestBody(); b != nil && strings.ToUpper(attrs.GetHttpMethod().GetValue()) != "POST" { - badFields[httpRequestBodyField] = fmt.Sprintf("Field can only be set if %q is set to the value 'POST'.", httpMethodField) - } - validateMapping(badFields, credential.Type(req.GetItem().GetCredentialType()), req.GetItem().CredentialMappingOverrides.AsMap()) default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." } @@ -670,17 +953,29 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { } func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCredentialType credential.Type) error { + prefix := "" + st := subtypes.SubtypeFromId(domain, req.GetId()) + switch st { + case vault.GenericLibrarySubtype: + prefix = vault.CredentialLibraryPrefix + case vault.SSHCertificateLibrarySubtype: + prefix = vault.SSHCertificateCredentialLibraryPrefix + } return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} - switch subtypes.SubtypeFromId(domain, req.GetId()) { - case vault.Subtype: - if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.Subtype { + switch st { + case vault.GenericLibrarySubtype: + if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.GenericLibrarySubtype { badFields[globals.TypeField] = "Cannot modify resource type." } if req.GetItem().GetCredentialType() != "" && req.GetItem().GetCredentialType() != string(currentCredentialType) { badFields[globals.CredentialTypeField] = "Cannot modify credential type." } - attrs := req.GetItem().GetVaultCredentialLibraryAttributes() + attrs := req.GetItem().GetVaultGenericCredentialLibraryAttributes() + if attrs == nil { + // fallback to attributes for older subtype + attrs = req.GetItem().GetVaultCredentialLibraryAttributes() + } if attrs != nil { if handlers.MaskContains(req.GetUpdateMask().GetPaths(), vaultPathField) && attrs.GetPath().GetValue() == "" { badFields[vaultPathField] = "This is a required field and cannot be set to empty." @@ -693,13 +988,33 @@ func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCrede } validateMapping(badFields, currentCredentialType, req.GetItem().CredentialMappingOverrides.AsMap()) } + case vault.SSHCertificateLibrarySubtype: + if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != vault.SSHCertificateLibrarySubtype { + badFields[globals.TypeField] = "Cannot modify resource type." + } + if req.GetItem().GetCredentialType() != "" && req.GetItem().GetCredentialType() != string(currentCredentialType) { + badFields[globals.CredentialTypeField] = "Cannot modify credential type." + } + attrs := req.GetItem().GetVaultSshCertificateCredentialLibraryAttributes() + if attrs != nil { + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), vaultPathField) && attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field and cannot be set to empty." + } + if u := attrs.GetUsername().GetValue(); handlers.MaskContains(req.GetUpdateMask().GetPaths(), sshCertUsernameField) && u == "" { + badFields[sshCertUsernameField] = "This is a required field and cannot be set to empty." + } + if t := attrs.GetKeyType(); t != nil && !strutil.StrListContains(validKeyTypes, strings.ToLower(t.GetValue())) { + badFields[keyTypeField] = "If set, value must be 'ed25519', 'ecdsa', or 'rsa'." + } + validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) + } } return badFields - }, vault.CredentialLibraryPrefix) + }, prefix) } func validateDeleteRequest(req *pbs.DeleteCredentialLibraryRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, vault.CredentialLibraryPrefix, vault.SSHCertificateCredentialLibraryPrefix) } func validateListRequest(req *pbs.ListCredentialLibrariesRequest) error { @@ -747,6 +1062,38 @@ func validateMapping(badFields map[string]string, credentialType credential.Type } } +// validateKeyBits appends to badFields if keyBits and keyType aren't accepted combinations for an SSHCertificateCredentialLibrary. +// If keyType is an empty string, validateKeyBits only validates keyBits. +func validateKeyBits(badFields map[string]string, keyBits uint32, keyType string) { + switch keyBits { + case vault.KeyBitsDefault: + case vault.KeyBitsEcdsa256, vault.KeyBitsEcdsa384, vault.KeyBitsEcdsa521: + if keyType != "" && keyType != vault.KeyTypeEcdsa { + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d for key type %s", keyBits, keyType) + } + case vault.KeyBitsRsa2048, vault.KeyBitsRsa3072, vault.KeyBitsRsa4096: + if keyType != "" && keyType != vault.KeyTypeRsa { + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d for key type %s", keyBits, keyType) + } + default: + badFields[keyBitsField] = fmt.Sprintf("Invalid bit size %d", keyBits) + } +} + +func getMapUpdate(field string, apiMasks []string) bool { + for _, m := range apiMasks { + if m == field { + return true + } + + fieldPrefix := fmt.Sprintf("%v.", field) + if s := strings.SplitN(m, fieldPrefix, 2); len(s) == 2 { + return true + } + } + return false +} + func getMappingUpdates(credentialType credential.Type, current vault.MappingOverride, new map[string]any, apiMasks []string) (map[string]any, bool) { ret := make(map[string]any) masks := make(map[string]bool) diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index 37c05350c..e1192e981 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -2,6 +2,7 @@ package credentiallibraries import ( "context" + "encoding/json" "fmt" "sort" "strings" @@ -65,7 +66,7 @@ func TestList(t *testing.T) { CreatedTime: l.GetCreateTime().GetTimestamp(), UpdatedTime: l.GetUpdateTime().GetTimestamp(), Version: l.GetVersion(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -75,7 +76,26 @@ func TestList(t *testing.T) { }, }) } - + for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { + wantLibraries = append(wantLibraries, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + CredentialType: l.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(l.GetKeyType()), + Path: wrapperspb.String(l.GetVaultPath()), + Username: wrapperspb.String(l.GetUsername()), + }, + }, + }) + } cases := []struct { name string req *pbs.ListCredentialLibrariesRequest @@ -101,12 +121,6 @@ func TestList(t *testing.T) { res: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[1:2]}, anonRes: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[1:2]}, }, - { - name: "Filter on Attribute", - req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibraries[2].GetVaultCredentialLibraryAttributes().GetPath().Value)}, - res: &pbs.ListCredentialLibrariesResponse{Items: wantLibraries[2:3]}, - anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes - }, { name: "Filter to No Libraries", req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: store.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, @@ -154,6 +168,119 @@ func TestList(t *testing.T) { } } +func TestList_Attributes(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + _, prj := iam.TestScopes(t, iamRepo) + + ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + storeGeneric, storeSSHCertificate := ts[0], ts[1] + var wantLibrariesGeneric []*pb.CredentialLibrary + var wantLibrariesSSHCertificate []*pb.CredentialLibrary + for _, l := range vault.TestCredentialLibraries(t, conn, wrapper, storeGeneric.GetPublicId(), 5) { + wantLibrariesGeneric = append(wantLibrariesGeneric, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.GenericLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String(l.GetVaultPath()), + HttpMethod: wrapperspb.String(l.GetHttpMethod()), + }, + }, + }) + } + for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, storeSSHCertificate.GetPublicId(), 10) { + wantLibrariesSSHCertificate = append(wantLibrariesSSHCertificate, &pb.CredentialLibrary{ + Id: l.GetPublicId(), + CredentialStoreId: l.GetStoreId(), + CredentialType: l.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: l.GetCreateTime().GetTimestamp(), + UpdatedTime: l.GetUpdateTime().GetTimestamp(), + Version: l.GetVersion(), + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(l.GetKeyType()), + Path: wrapperspb.String(l.GetVaultPath()), + Username: wrapperspb.String(l.GetUsername()), + }, + }, + }) + } + cases := []struct { + name string + req *pbs.ListCredentialLibrariesRequest + res *pbs.ListCredentialLibrariesResponse + anonRes *pbs.ListCredentialLibrariesResponse + err error + }{ + { + name: "Filter on Attribute Generic Library", + req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeGeneric.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesGeneric[2].GetVaultCredentialLibraryAttributes().GetPath().Value)}, + res: &pbs.ListCredentialLibrariesResponse{Items: wantLibrariesGeneric[2:3]}, + anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes + }, + { + name: "Filter on Attribute SSH Certificate Library", + req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeSSHCertificate.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesSSHCertificate[2].GetVaultSshCertificateCredentialLibraryAttributes().GetPath().Value)}, + res: &pbs.ListCredentialLibrariesResponse{Items: wantLibrariesSSHCertificate[2:3]}, + anonRes: &pbs.ListCredentialLibrariesResponse{}, // anonymous user does not have access to attributes + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s, err := NewService(repoFn, iamRepoFn) + require.NoError(t, err, "Couldn't create new host set service.") + + // Test non-anonymous listing + got, gErr := s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(t, gErr) + assert.True(t, errors.Is(gErr, tc.err), "ListCredentialLibrary(%q) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(t, gErr) + sort.Slice(got.Items, func(i, j int) bool { + return got.Items[i].GetId() < got.Items[j].GetId() + }) + want := proto.Clone(tc.res).(*pbs.ListCredentialLibrariesResponse) + sort.Slice(want.Items, func(i, j int) bool { + return want.Items[i].GetId() < want.Items[j].GetId() + }) + assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) + + // Test anonymous listing + got, gErr = s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) + require.NoError(t, gErr) + assert.Len(t, got.Items, len(tc.anonRes.Items)) + for _, item := range got.GetItems() { + require.Nil(t, item.CreatedTime) + require.Nil(t, item.UpdatedTime) + require.Zero(t, item.Version) + } + }) + } +} + func TestCreate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -186,6 +313,7 @@ func TestCreate(t *testing.T) { name: "missing vault path", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{}, }, @@ -197,6 +325,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Id", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Id: vault.CredentialLibraryPrefix + "_notallowed", Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -211,6 +340,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Created Time", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), CreatedTime: timestamppb.Now(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -225,6 +355,7 @@ func TestCreate(t *testing.T) { name: "Can't specify Update Time", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), UpdatedTime: timestamppb.Now(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ @@ -252,7 +383,7 @@ func TestCreate(t *testing.T) { { name: "parent id must be included", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -266,6 +397,7 @@ func TestCreate(t *testing.T) { name: "Cannot specify a http request body when http method is get", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -280,6 +412,7 @@ func TestCreate(t *testing.T) { name: "Invalid Method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -294,6 +427,7 @@ func TestCreate(t *testing.T) { name: "Request Body With GET Method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -308,6 +442,7 @@ func TestCreate(t *testing.T) { name: "Invalid credential type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -322,6 +457,7 @@ func TestCreate(t *testing.T) { name: "Invalid username_password mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -345,6 +481,7 @@ func TestCreate(t *testing.T) { name: "Using POST method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -363,7 +500,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -379,6 +516,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ @@ -399,7 +537,7 @@ func TestCreate(t *testing.T) { Description: &wrapperspb.StringValue{Value: "desc"}, Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -414,6 +552,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -431,7 +570,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -447,6 +586,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type with username mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -472,7 +612,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -496,6 +636,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary username_password type with username/password mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -522,7 +663,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -547,6 +688,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary ssh_private_key type", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -564,7 +706,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -580,6 +722,7 @@ func TestCreate(t *testing.T) { name: "Create a valid vault CredentialLibrary ssh_private_key type with mapping", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -607,7 +750,7 @@ func TestCreate(t *testing.T) { UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("something"), @@ -629,6 +772,38 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "Create a valid vault CredentialLibrary with the 'vault' subtype", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.Subtype.String(), + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + }}, + idPrefix: vault.CredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.GenericLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{ + VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + HttpMethod: wrapperspb.String("GET"), + }, + }, + AuthorizedActions: testAuthorizedActions, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -717,6 +892,11 @@ func TestGet(t *testing.T) { sshPkLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib1) require.NoError(t, err) + lib2, err := vault.NewSSHCertificateCredentialLibrary(store.GetPublicId(), "/ssh/sign/foo", "username") + require.NoError(t, err) + sshCertLib, err := repo.CreateSSHCertificateCredentialLibrary(context.Background(), prj.GetPublicId(), lib2) + require.NoError(t, err) + cases := []struct { name string id string @@ -731,7 +911,7 @@ func TestGet(t *testing.T) { Id: unspecifiedLib.GetPublicId(), CredentialStoreId: unspecifiedLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: unspecifiedLib.CreateTime.GetTimestamp(), UpdatedTime: unspecifiedLib.UpdateTime.GetTimestamp(), @@ -753,7 +933,7 @@ func TestGet(t *testing.T) { Id: userPassLib.GetPublicId(), CredentialStoreId: userPassLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: userPassLib.CreateTime.GetTimestamp(), UpdatedTime: userPassLib.UpdateTime.GetTimestamp(), @@ -785,7 +965,7 @@ func TestGet(t *testing.T) { Id: sshPkLib.GetPublicId(), CredentialStoreId: sshPkLib.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: sshPkLib.CreateTime.GetTimestamp(), UpdatedTime: sshPkLib.UpdateTime.GetTimestamp(), @@ -810,6 +990,30 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-ssh-certificate", + id: sshCertLib.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: sshCertLib.GetPublicId(), + CredentialStoreId: sshCertLib.GetStoreId(), + CredentialType: sshCertLib.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.SSHCertificateLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: sshCertLib.CreateTime.GetTimestamp(), + UpdatedTime: sshCertLib.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(sshCertLib.GetKeyType()), + Path: wrapperspb.String(sshCertLib.GetVaultPath()), + Username: wrapperspb.String(sshCertLib.GetUsername()), + }, + }, + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -863,6 +1067,7 @@ func TestDelete(t *testing.T) { store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + vl2 := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] s, err := NewService(repoFn, iamRepoFn) require.NoError(t, err) @@ -876,6 +1081,10 @@ func TestDelete(t *testing.T) { name: "success", id: vl.GetPublicId(), }, + { + name: "success-ssh-cert", + id: vl2.GetPublicId(), + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", vault.CredentialLibraryPrefix), @@ -1673,3 +1882,668 @@ func TestUpdate(t *testing.T) { assert.Nil(t, cl.GetItem().GetVaultCredentialLibraryAttributes().GetHttpRequestBody()) }) } + +func TestCreate_SSHCertificateCredentialLibrary(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + + _, prj := iam.TestScopes(t, iamRepo) + store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] + defaultCL := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + defaultCreated := defaultCL.GetCreateTime().GetTimestamp() + + cases := []struct { + name string + req *pbs.CreateCredentialLibraryRequest + res *pbs.CreateCredentialLibraryResponse + idPrefix string + err error + wantErr bool + }{ + { + name: "missing vault path", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Username: wrapperspb.String("username"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + wantErr: true, + }, + { + name: "missing username", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + wantErr: true, + }, + { + name: "Can't specify Id", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Id: vault.SSHCertificateCredentialLibraryPrefix + "_notallowed", + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + CreatedTime: timestamppb.Now(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + UpdatedTime: timestamppb.Now(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Type and parent id must match", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: "static", + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "parent id must be included", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid credential type", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + }, + }, + CredentialType: "fake-type", + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid key type key bits combination", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("something"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + KeyBits: wrapperspb.UInt32(256), + }, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Create a valid vault SSHCertificateCredentialLibrary", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.SSHCertificateCredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + AuthorizedActions: testAuthorizedActions, + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + { + name: "Seting key type ed25119 with key bits nil", + req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.SSHCertificateLibrarySubtype.String(), + Name: &wrapperspb.StringValue{Value: "name2"}, + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + }}, + idPrefix: vault.SSHCertificateCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", vault.SSHCertificateCredentialLibraryPrefix), + Item: &pb.CredentialLibrary{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Name: &wrapperspb.StringValue{Value: "name2"}, + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/sign/foo"), + Username: wrapperspb.String("username"), + KeyType: wrapperspb.String(vault.KeyTypeEd25519), + }, + }, + AuthorizedActions: testAuthorizedActions, + CredentialType: string(credential.SshCertificateType), + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + s, err := NewService(repoFn, iamRepoFn) + require.NoError(err, "Error when getting new credential store service.") + + got, gErr := s.CreateCredentialLibrary(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) + if tc.wantErr || tc.err != nil { + require.Error(gErr) + if tc.err != nil { + assert.True(errors.Is(gErr, tc.err), "CreateCredentialLibrary(...) got error %v, wanted %v", gErr, tc.err) + } + return + } + require.NoError(gErr) + if tc.res == nil { + require.Nil(got) + } + if got != nil { + assert.Contains(got.GetUri(), tc.res.Uri) + assert.True(strings.HasPrefix(got.GetItem().GetId(), tc.idPrefix)) + gotCreateTime := got.GetItem().GetCreatedTime() + gotUpdateTime := got.GetItem().GetUpdatedTime() + + // Verify it is a credential store created after the test setup's default credential store + assert.True(gotCreateTime.AsTime().After(defaultCreated.AsTime()), "New credential store should have been created after default credential store. Was created %v, which is after %v", gotCreateTime, defaultCreated) + assert.True(gotUpdateTime.AsTime().After(defaultCreated.AsTime()), "New credential store should have been updated after default credential store. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) + + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdate_SSHCertificateCredentialLibrary(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*vault.Repository, error) { + return vault.NewRepository(rw, rw, kms, sche) + } + + _, prj := iam.TestScopes(t, iamRepo) + ctx := auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()) + + s, err := NewService(repoFn, iamRepoFn) + require.NoError(t, err) + cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) + store, diffStore := cs[0], cs[1] + + freshLibrary := func(opt ...vault.Option) (*vault.SSHCertificateCredentialLibrary, func()) { + repo, err := repoFn() + require.NoError(t, err) + lib, err := vault.NewSSHCertificateCredentialLibrary(store.GetPublicId(), "/ssh/sign/foo", "username", opt...) + require.NoError(t, err) + + vl, err := repo.CreateSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), lib) + require.NoError(t, err) + clean := func() { + _, err := s.DeleteCredentialLibrary(ctx, &pbs.DeleteCredentialLibraryRequest{Id: vl.GetPublicId()}) + require.NoError(t, err) + } + return vl, clean + } + + fieldmask := func(paths ...string) *fieldmaskpb.FieldMask { + return &fieldmaskpb.FieldMask{Paths: paths} + } + + v := vault.NewTestVaultServer(t, vault.WithTestVaultTLS(vault.TestClientTLS)) + _, token := v.CreateToken(t) + _ = token + + testExtensionBytes, _ := json.Marshal(map[string]string{"permit-pty": ""}) + testCriticalOptionsBytes, _ := json.Marshal(map[string]string{"option-a": "set-a"}) + + successCases := []struct { + name string + opts []vault.Option + req *pbs.UpdateCredentialLibraryRequest + res func(*pb.CredentialLibrary) *pb.CredentialLibrary + }{ + { + name: "name and description", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("name", "description"), + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("basic"), + Description: wrapperspb.String("basic"), + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.Name = wrapperspb.String("basic") + out.Description = wrapperspb.String("basic") + return out + }, + }, + { + name: "update username", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(sshCertUsernameField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Username: wrapperspb.String("changed-username"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Username = wrapperspb.String("changed-username") + return out + }, + }, + { + name: "update key type and key bits", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.key_type", "attributes.key_bits"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(vault.KeyTypeRsa), + KeyBits: wrapperspb.UInt32(2048), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyType = wrapperspb.String("rsa") + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyBits = wrapperspb.UInt32(2048) + return out + }, + }, + { + name: "update path", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(vaultPathField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + Path: wrapperspb.String("/ssh/issue/foo"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Path = wrapperspb.String("/ssh/issue/foo") + return out + }, + }, + { + name: "update other attr fields", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.ttl", "attributes.key_id", "attributes.critical_options.some", "attributes.extensions.permity-pty"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyId: wrapperspb.String("id"), + CriticalOptions: map[string]string{"some": "option"}, + Extensions: map[string]string{"permit-pty": ""}, + Ttl: wrapperspb.String("2m"), + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().Ttl = wrapperspb.String("2m") + out.GetVaultSshCertificateCredentialLibraryAttributes().KeyId = wrapperspb.String("id") + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"some": "option"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = map[string]string{"permit-pty": ""} + return out + }, + }, + { + name: "clear critical options, extensions fields", + opts: []vault.Option{vault.WithExtensions(string(testExtensionBytes)), vault.WithCriticalOptions(string(testCriticalOptionsBytes))}, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options", "attributes.extensions.permit-pty"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: nil, + Extensions: nil, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = nil + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = nil + return out + }, + }, + { + name: "set critical options while clearing extensions", + opts: []vault.Option{vault.WithExtensions(string(testExtensionBytes)), vault.WithCriticalOptions(string(testCriticalOptionsBytes))}, + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options.option-b", "attributes.extensions"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: map[string]string{"option-b": "set-b"}, + Extensions: nil, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"option-b": "set-b"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = nil + return out + }, + }, + { + name: "set multiple critical options, extensions", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask("attributes.critical_options.option-a", "attributes.critical_options.option-b", "attributes.critical_options.option-c", + "attributes.extensions.permit-pty", "attributes.extensions.permit-port-forwarding", "attributes.extensions.permit-X11-forwarding"), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + CriticalOptions: map[string]string{"option-a": "set-a", "option-b": "set-b", "option-c": "set-c"}, + Extensions: map[string]string{"permit-pty": "", "permit-port-forwarding": "", "permit-X11-forwarding": ""}, + }, + }, + }, + }, + res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary { + out := proto.Clone(in).(*pb.CredentialLibrary) + out.GetVaultSshCertificateCredentialLibraryAttributes().CriticalOptions = map[string]string{"option-a": "set-a", "option-b": "set-b", "option-c": "set-c"} + out.GetVaultSshCertificateCredentialLibraryAttributes().Extensions = map[string]string{"permit-pty": "", "permit-port-forwarding": "", "permit-X11-forwarding": ""} + return out + }, + }, + } + + for _, tc := range successCases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + st, cleanup := freshLibrary(tc.opts...) + defer cleanup() + + if tc.req.Item.GetVersion() == 0 { + tc.req.Item.Version = 1 + } + if tc.req.GetId() == "" { + tc.req.Id = st.GetPublicId() + } + resToChange, err := s.GetCredentialLibrary(ctx, &pbs.GetCredentialLibraryRequest{Id: st.GetPublicId()}) + require.NoError(err) + want := &pbs.UpdateCredentialLibraryResponse{Item: tc.res(resToChange.GetItem())} + + got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) + require.NoError(gErr) + require.NotNil(got) + + gotUpdateTime := got.GetItem().GetUpdatedTime() + created := st.GetCreateTime().GetTimestamp() + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + want.Item.UpdatedTime = got.Item.UpdatedTime + + assert.EqualValues(2, got.Item.Version) + want.Item.Version = 2 + + assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + }) + } + + vl, cleanup := freshLibrary() + defer cleanup() + + errCases := []struct { + name string + path string + item *pb.CredentialLibrary + }{ + { + name: "read only type", + path: "type", + item: &pb.CredentialLibrary{Type: "something"}, + }, + { + name: "read only store_id", + path: "store_id", + item: &pb.CredentialLibrary{CredentialStoreId: diffStore.GetPublicId()}, + }, + { + name: "read only updated_time", + path: "updated_time", + item: &pb.CredentialLibrary{UpdatedTime: timestamppb.Now()}, + }, + { + name: "read only created_time", + path: "created_time", + item: &pb.CredentialLibrary{UpdatedTime: timestamppb.Now()}, + }, + { + name: "read only authorized actions", + path: "authorized actions", + item: &pb.CredentialLibrary{AuthorizedActions: append(testAuthorizedActions, "another")}, + }, + { + name: "read only credential type", + path: "credential_type", + item: &pb.CredentialLibrary{CredentialType: string(credential.UsernamePasswordType)}, + }, + } + for _, tc := range errCases { + t.Run(tc.name, func(t *testing.T) { + req := &pbs.UpdateCredentialLibraryRequest{ + Id: vl.GetPublicId(), + Item: tc.item, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, + } + req.Item.Version = vl.Version + + got, gErr := s.UpdateCredentialLibrary(ctx, req) + assert.Error(t, gErr) + assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) + assert.Nil(t, got) + }) + } + vErrs := []struct { + name string + req *pbs.UpdateCredentialLibraryRequest + errContains string + }{ + { + name: "invalid key type key bits combination", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyBitsField, keyTypeField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String(vault.KeyTypeEcdsa), + KeyBits: wrapperspb.UInt32(2048), + }, + }, + }, + }, + errContains: "Invalid bit size 2048 for key type ecdsa", + }, + { + name: "invalid key type", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyTypeField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyType: wrapperspb.String("unknown-type"), + }, + }, + }, + }, + errContains: "If set, value must be 'ed25519', 'ecdsa', or 'rsa'.", + }, + { + name: "invalid key bits", + req: &pbs.UpdateCredentialLibraryRequest{ + UpdateMask: fieldmask(keyBitsField), + Item: &pb.CredentialLibrary{ + Type: vault.SSHCertificateLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes{ + VaultSshCertificateCredentialLibraryAttributes: &pb.VaultSSHCertificateCredentialLibraryAttributes{ + KeyBits: wrapperspb.UInt32(1234), + }, + }, + }, + }, + errContains: "Invalid bit size 1234", + }, + } + for _, tc := range vErrs { + t.Run(tc.name, func(t *testing.T) { + scl, cleanup := freshLibrary() + defer cleanup() + + if tc.req.Item.GetVersion() == 0 { + tc.req.Item.Version = 1 + } + if tc.req.GetId() == "" { + tc.req.Id = scl.GetPublicId() + } + got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) + if gErr != nil { + assert.Contains(t, gErr.Error(), tc.errContains) + assert.Nil(t, got) + } + }) + } +} diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index f68097150..ad8f1184c 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -33,7 +33,16 @@ func dynamicToWorkerCredential(ctx context.Context, cred credential.Dynamic) (se }, }, } - + case credential.SshCertificate: + workerCred = &serverpb.Credential{ + Credential: &serverpb.Credential_SshCertificate{ + SshCertificate: &serverpb.SshCertificate{ + Username: c.Username(), + PrivateKey: string(c.PrivateKey()), + Certificate: string(c.Certificate()), + }, + }, + } case credential.SshPrivateKey: workerCred = &serverpb.Credential{ Credential: &serverpb.Credential_SshPrivateKey{ diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 0d18ee19f..a1735b179 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -1810,6 +1810,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix) { @@ -1857,6 +1858,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix) { @@ -1908,6 +1910,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc for _, cl := range req.GetInjectedApplicationCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), vault.CredentialLibraryPrefix, + vault.SSHCertificateCredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix, diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index d48fe1287..63ec28b42 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -2516,6 +2516,7 @@ func TestAuthorizeSession(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Library Name"), Description: wrapperspb.String("Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: &wrapperspb.StringValue{ @@ -2629,7 +2630,7 @@ func TestAuthorizeSession(t *testing.T) { Name: clsResp.GetItem().GetName().GetValue(), Description: clsResp.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), }, }}, // TODO: validate the contents of the authorization token is what is expected @@ -2758,6 +2759,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Usernamepassword Library"), Description: wrapperspb.String("Usernamepassword Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-userpass")), @@ -2772,6 +2774,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Unspecified Library"), Description: wrapperspb.String("Unspecified Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-userpass")), @@ -2789,6 +2792,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("Usernamepassword Mapping Library"), Description: wrapperspb.String("Usernamepassword Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-userpass")), @@ -2860,6 +2864,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key Library"), Description: wrapperspb.String("SSH Private Key Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk")), @@ -2878,6 +2883,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key Mapping Library"), Description: wrapperspb.String("SSH Private Key Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk")), @@ -2900,6 +2906,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key With Passphrase Library"), Description: wrapperspb.String("SSH Private Key Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk-with-pass")), @@ -2919,6 +2926,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { CredentialStoreId: vaultStore.GetPublicId(), Name: wrapperspb.String("SSH Private Key With Passphrase Mapping Library"), Description: wrapperspb.String("SSH Private Key Mapping Library Description"), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk-with-pass")), @@ -2955,7 +2963,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUnspecified.GetItem().GetName().GetValue(), Description: clsRespUnspecified.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), }, }, }, @@ -2971,7 +2979,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUsernamePassword.GetItem().GetName().GetValue(), Description: clsRespUsernamePassword.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.UsernamePasswordType), }, Credential: func() *structpb.Struct { @@ -2997,7 +3005,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespUsernamePasswordWithMapping.GetItem().GetName().GetValue(), Description: clsRespUsernamePasswordWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.UsernamePasswordType), }, Credential: func() *structpb.Struct { @@ -3075,7 +3083,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKey.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKey.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3101,7 +3109,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithMapping.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3154,7 +3162,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithPass.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithPass.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3181,7 +3189,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { Name: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetName().GetValue(), Description: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetDescription().GetValue(), CredentialStoreId: vaultStore.GetPublicId(), - Type: vault.Subtype.String(), + Type: vault.GenericLibrarySubtype.String(), CredentialType: string(credential.SshPrivateKeyType), }, Credential: func() *structpb.Struct { @@ -3392,6 +3400,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("database", "creds", "opened")), @@ -3416,6 +3425,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ CredentialStoreId: store.GetPublicId(), Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String("bad path"), @@ -3440,6 +3450,7 @@ func TestAuthorizeSession_Errors(t *testing.T) { clsResp, err := credService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{ CredentialStoreId: expiredStore.GetPublicId(), Description: wrapperspb.String(fmt.Sprintf("Library Description for target %q", tar.GetName())), + Type: vault.GenericLibrarySubtype.String(), Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{ VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{ Path: wrapperspb.String(path.Join("database", "creds", "opened")), diff --git a/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql b/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql index 0d2838fcc..554572213 100644 --- a/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql +++ b/internal/db/schema/migrations/oss/postgres/16/02_wh_credential_dimension.up.sql @@ -65,6 +65,7 @@ begin; and o.public_id = p.parent_id and o.type = 'org'; + -- Replaced in 63/03_wh_ssh_cert_library.up.sql create view whx_credential_dimension_target as select key, credential_purpose, diff --git a/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql b/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql index 24bedceaf..3c846b06e 100644 --- a/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql +++ b/internal/db/schema/migrations/oss/postgres/16/03_wh_credential_dimension.up.sql @@ -4,6 +4,7 @@ begin; -- with the current values in the operational tables for the given parameters. IF the values -- between operational tables and the wh_credential_dimension differ, a new row is inserted in -- the wh_credential_dimension to match the current values in the operational tables. + -- Replaced in 63/03_wh_ssh_cert_library.up.sql create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) returns wh_dim_key as $$ declare diff --git a/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql b/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql index 6b5673ad2..b19952e69 100644 --- a/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql +++ b/internal/db/schema/migrations/oss/postgres/39/01_static_ssh_private_key_creds.up.sql @@ -6,6 +6,7 @@ begin; -- Add new constraint that only allows known types -- This replaces the constraint defined in 36/01_credential_type_up + -- Replaced in 63/01_credential_vault_ssh_cert_library alter table credential_type_enm add constraint only_predefined_credential_types_allowed check ( diff --git a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql index 59c323a53..c219a52da 100644 --- a/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql +++ b/internal/db/schema/migrations/oss/postgres/44/03_targets.up.sql @@ -157,6 +157,7 @@ begin; -- The whx_credential_dimension_source view shows the current values in the -- operational tables of the credential dimension. -- Replaces whx_credential_dimension_source defined in 16/02_wh_credential_dimension.up.sql + -- Replaced in 63/03_wh_ssh_cert_library.up.sql drop view whx_credential_dimension_source; create view whx_credential_dimension_source as select -- id is the first column in the target view diff --git a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql index 430918693..3ed79b07e 100644 --- a/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql +++ b/internal/db/schema/migrations/oss/postgres/56/02_add_data_key_foreign_key_references.up.sql @@ -188,6 +188,7 @@ begin; 'The view returns the current token for the store, if the Vault token has expired this view will return an empty token_hmac and a token_status of ''expired'' ' 'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.'; + -- Replaced in 63/02_add_ssh_cert_to_vault_cred_library_view.up.sql create view credential_vault_library_issue_credentials as with password_override (library_id, username_attribute, password_attribute) as ( diff --git a/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql b/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql new file mode 100644 index 000000000..6ec697947 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/01_credential_vault_ssh_cert_library.up.sql @@ -0,0 +1,170 @@ +begin; + + -- drop constraint so we can add ssh_certificate + alter table credential_type_enm + drop constraint only_predefined_credential_types_allowed; + + -- Add new constraint that only allows known types + -- This replaces the constraint defined in 39/01_static_ssh_private_key_creds_up + alter table credential_type_enm + add constraint only_predefined_credential_types_allowed + check ( + name in ( + 'unspecified', + 'username_password', + 'ssh_private_key', + 'ssh_certificate' + ) + ); + + insert into credential_type_enm (name) + values ('ssh_certificate'); + + create table credential_vault_ssh_cert_key_type_enm ( + name text primary key + constraint only_predefined_key_types_allowed + check ( + name in ( + 'ed25519', + 'ecdsa', + 'rsa' + ) + ) + ); + comment on table credential_vault_ssh_cert_key_type_enm is + 'credential_vault_ssh_cert_key_type_enm is an enumeration table for the ssh key type. '; + + insert into credential_vault_ssh_cert_key_type_enm (name) + values + ('ed25519'), + ('ecdsa'), + ('rsa'); + + create table credential_vault_ssh_cert_key_bits_enm ( + bits int primary key + constraint only_predefined_key_bits_allowed + check ( + bits in ( + 0, + 2048, + 3072, + 4096, + 256, + 384, + 521 + ) + ) + ); + comment on table credential_vault_ssh_cert_key_bits_enm is + 'credential_vault_ssh_cert_key_bits_enm is an enumeration table for the ssh key bits. '; + + insert into credential_vault_ssh_cert_key_bits_enm (bits) + values + (0), + (2048), + (3072), + (4096), + (256), + (384), + (521); + + create table credential_vault_ssh_cert_valid_key_type_key_bits ( + key_type text not null + constraint credential_vault_ssh_cert_key_type_enm_fkey + references credential_vault_ssh_cert_key_type_enm (name), + key_bits int not null + constraint credential_vault_ssh_cert_key_bits_enm_fkey + references credential_vault_ssh_cert_key_bits_enm (bits), + constraint credential_vault_ssh_cert_valid_key_type_key_bits_uq + unique(key_type, key_bits) + ); + + insert into credential_vault_ssh_cert_valid_key_type_key_bits (key_type, key_bits) + values + ('ed25519', 0), + ('ecdsa', 256), + ('ecdsa', 384), + ('ecdsa', 521), + ('rsa', 2048), + ('rsa', 3072), + ('rsa', 4096); + + create table credential_vault_ssh_cert_library ( + public_id wt_public_id primary key, + store_id wt_public_id not null + constraint credential_vault_store_fkey + references credential_vault_store (public_id) + on delete cascade + on update cascade, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + vault_path text not null + constraint vault_path_must_not_be_empty + check(length(trim(vault_path)) > 0) + constraint vault_path_must_be_sign_or_issue + check(vault_path ~ '^.+\/(sign|issue)\/[^\/\\\s]+$'), + username text not null + constraint username_must_not_be_empty + check(length(trim(username)) > 0), + key_type text not null, + key_bits int not null, + ttl text, + key_id text, + critical_options bytea, + extensions bytea, + credential_type text, + project_id wt_public_id not null, + constraint credential_vault_ssh_cert_library_store_id_name_uq + unique(store_id, name), + constraint credential_vault_ssh_cert_library_store_id_public_id_uq + unique(store_id, public_id), + constraint credential_library_fkey + foreign key (project_id, store_id, public_id, credential_type) + references credential_library (project_id, store_id, public_id, credential_type) + on delete cascade + on update cascade, + constraint credential_vault_ssh_cert_valid_key_type_key_bits_fkey + foreign key (key_type, key_bits) + references credential_vault_ssh_cert_valid_key_type_key_bits(key_type, key_bits) + ); + comment on table credential_vault_ssh_cert_library is + 'credential_vault_ssh_cert_library a credential library that issues credentials from a vault ssh secret backend.'; + + create function default_ssh_certificate_credential_type() returns trigger + as $$ + begin + if new.credential_type is distinct from 'ssh_certificate' then + raise warning 'credential_vault_ssh_cert_library only supports ssh_certificate credentials'; + new.credential_type = 'ssh_certificate'; + end if; + return new; + end; + $$ language plpgsql; + comment on function default_ssh_certificate_credential_type is + 'default_ssh_certificate_credential_type ensures the credential_type is set to ssh_certificate'; + + create trigger default_ssh_certificate_credential_type before insert on credential_vault_ssh_cert_library + for each row execute procedure default_ssh_certificate_credential_type(); + create trigger insert_credential_library_subtype before insert on credential_vault_ssh_cert_library + for each row execute procedure insert_credential_library_subtype(); + create trigger default_create_time_column before insert on credential_vault_ssh_cert_library + for each row execute procedure default_create_time(); + create trigger delete_credential_library_subtype after delete on credential_vault_ssh_cert_library + for each row execute procedure delete_credential_library_subtype(); + create trigger immutable_columns before update on credential_vault_ssh_cert_library + for each row execute procedure immutable_columns('public_id', 'store_id', 'project_id', 'credential_type', 'create_time'); + create trigger update_time_column before update on credential_vault_ssh_cert_library + for each row execute procedure update_time_column(); + create trigger update_version_column after update on credential_vault_ssh_cert_library + for each row execute procedure update_version_column(); + create trigger before_insert_credential_vault_library before insert on credential_vault_ssh_cert_library + for each row execute procedure before_insert_credential_vault_library(); + + insert into oplog_ticket (name, version) + values + ('credential_vault_ssh_cert_library', 1); + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql b/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql new file mode 100644 index 000000000..4b09d965b --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/02_add_ssh_cert_to_vault_cred_library_view.up.sql @@ -0,0 +1,111 @@ +begin; + + -- Replaces view from 56/02_add_data_key_foreign_key_references.up.sql + drop view credential_vault_library_issue_credentials; + create view credential_vault_library_issue_credentials as + with + password_override (library_id, username_attribute, password_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')) + from credential_vault_library_username_password_mapping_override + ), + ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(private_key_attribute, wt_to_sentinel('no override')), + nullif(private_key_passphrase_attribute, wt_to_sentinel('no override')) + from credential_vault_library_ssh_private_key_mapping_override + ) + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + library.http_method as http_method, + library.http_request_body as http_request_body, + library.credential_type as credential_type, + null as key_type, + null as key_bits, + null as username, + null as ttl, + null as key_id, + null as critical_options, + null as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + coalesce(upasso.username_attribute,sshpk.username_attribute) + as username_attribute, + upasso.password_attribute as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, + 'generic' as cred_lib_type -- used to switch on + from credential_vault_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join password_override upasso + on library.public_id = upasso.library_id + left join ssh_private_key_override sshpk + on library.public_id = sshpk.library_id + union + select library.public_id as public_id, + library.store_id as store_id, + library.name as name, + library.description as description, + library.create_time as create_time, + library.update_time as update_time, + library.version as version, + library.vault_path as vault_path, + null as http_method, + null as http_request_body, + library.credential_type as credential_type, + library.key_type as key_type, + library.key_bits as key_bits, + library.username as username, + library.ttl as ttl, + library.key_id as key_id, + library.critical_options as critical_options, + library.extensions as extensions, + store.project_id as project_id, + store.vault_address as vault_address, + store.namespace as namespace, + store.ca_cert as ca_cert, + store.tls_server_name as tls_server_name, + store.tls_skip_verify as tls_skip_verify, + store.worker_filter as worker_filter, + store.ct_token as ct_token, -- encrypted + store.token_hmac as token_hmac, + store.token_status as token_status, + store.token_key_id as token_key_id, + store.client_cert as client_cert, + store.ct_client_key as ct_client_key, -- encrypted + store.client_key_id as client_key_id, + null as username_attribute, + null as password_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type -- used to switch on + from credential_vault_ssh_cert_library library + join credential_vault_store_client store + on library.store_id = store.public_id; + comment on view credential_vault_library_issue_credentials is + 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' + 'This view should not be used to retrieve data which will be returned external to boundary.'; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql b/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql new file mode 100644 index 000000000..f391cea0c --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/63/03_wh_ssh_cert_library.up.sql @@ -0,0 +1,254 @@ +begin; + alter table wh_credential_dimension + add column credential_library_username text, + add column credential_library_key_type_and_bits text + ; + + update wh_credential_dimension set + credential_library_username = 'Not Applicable', + credential_library_key_type_and_bits = 'Not Applicable'; + + update wh_credential_dimension set + credential_library_vault_http_request_body = 'Not Applicable' + where + credential_library_vault_http_method = 'GET' + and + credential_library_vault_http_request_body = 'None'; + + + alter table wh_credential_dimension + alter column credential_library_username type wh_dim_text, + alter column credential_library_key_type_and_bits type wh_dim_text + ; + + update wh_credential_dimension set + credential_library_type = 'vault generic credential library' + where + credential_library_type = 'vault credential library'; + + -- The whx_credential_dimension_source view shows the current values in the + -- operational tables of the credential dimension. + -- Replaces whx_credential_dimension_source defined in 44/03_targets.up.sql + drop view whx_credential_dimension_source; + create view whx_credential_dimension_source as + with vault_generic_library as ( + select vcl.public_id as public_id, + 'vault generic credential library' as type, + coalesce(vcl.name, 'None') as name, + coalesce(vcl.description, 'None') as description, + vcl.vault_path as vault_path, + vcl.http_method as http_method, + case + when vcl.http_method = 'GET' then 'Not Applicable' + else coalesce(vcl.http_request_body::text, 'None') + end as http_request_body, + 'Not Applicable' as username, + 'Not Applicable' as key_type_and_bits + from credential_vault_library as vcl + ), + vault_ssh_cert_library as ( + select vsccl.public_id as public_id, + 'vault ssh certificate credential library' as type, + coalesce(vsccl.name, 'None') as name, + coalesce(vsccl.description, 'None') as description, + vsccl.vault_path as vault_path, + 'Not Applicable' as http_method, + 'Not Applicable' as http_request_body, + vsccl.username as username, + case + when vsccl.key_type = 'ed25519' then vsccl.key_type + else vsccl.key_type || '-' || vsccl.key_bits::text + end as key_type_and_bits + from credential_vault_ssh_cert_library as vsccl + ), + final as ( + select s.public_id as session_id, + scd.credential_purpose as credential_purpose, + cl.public_id as credential_library_id, + coalesce(vcl.type, vsccl.type) as credential_library_type, + coalesce(vcl.name, vsccl.name) as credential_library_name, + coalesce(vcl.description, vsccl.description) as credential_library_description, + coalesce(vcl.vault_path, vsccl.vault_path) as credential_library_vault_path, + coalesce(vcl.http_method, vsccl.http_method) as credential_library_vault_http_method, + coalesce(vcl.http_request_body, vsccl.http_request_body) as credential_library_vault_http_request_body, + coalesce(vcl.username, vsccl.username) as credential_library_username, + coalesce(vcl.key_type_and_bits, vsccl.key_type_and_bits) as credential_library_key_type_and_bits, + cs.public_id as credential_store_id, + case + when vcs is null then 'None' + else 'vault credential store' + end as credential_store_type, + coalesce(vcs.name, 'None') as credential_store_name, + coalesce(vcs.description, 'None') as credential_store_description, + coalesce(vcs.namespace, 'None') as credential_store_vault_namespace, + coalesce(vcs.vault_address, 'None') as credential_store_vault_address, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(tt.name, 'None') as target_name, + coalesce(tt.description, 'None') as target_description, + coalesce(tt.default_port, 0) as target_default_port_number, + tt.session_max_seconds as target_session_max_seconds, + tt.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from session_credential_dynamic as scd + join session as s on scd.session_id = s.public_id + join credential_library as cl on scd.library_id = cl.public_id + join credential_store as cs on cl.store_id = cs.public_id + join target as t on s.target_id = t.public_id + join iam_scope as p on p.public_id = t.project_id and p.type = 'project' + join iam_scope as o on p.parent_id = o.public_id and o.type = 'org' + left join vault_generic_library as vcl on cl.public_id = vcl.public_id + left join vault_ssh_cert_library as vsccl on cl.public_id = vsccl.public_id + left join credential_vault_store as vcs on cs.public_id = vcs.public_id + left join target_tcp as tt on t.public_id = tt.public_id + ) + select session_id, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_library_username, + credential_library_key_type_and_bits, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from final; + + -- Replaces view in 16/02_wh_credential_dimension.up.sql + drop view whx_credential_dimension_target; + create view whx_credential_dimension_target as + select key, + credential_purpose, + credential_library_id, + credential_library_type, + credential_library_name, + credential_library_description, + credential_library_vault_path, + credential_library_vault_http_method, + credential_library_vault_http_request_body, + credential_library_username, + credential_library_key_type_and_bits, + credential_store_id, + credential_store_type, + credential_store_name, + credential_store_description, + credential_store_vault_namespace, + credential_store_vault_address, + target_id, + target_type, + target_name, + target_description, + target_default_port_number, + target_session_max_seconds, + target_session_connection_limit, + project_id, + project_name, + project_description, + organization_id, + organization_name, + organization_description + from wh_credential_dimension + where current_row_indicator = 'Current' + ; + + -- Replaces function in 16/03_wh_credential_dimension.up.sql + drop function wh_upsert_credential_dimension; + create function wh_upsert_credential_dimension(p_session_id wt_public_id, p_library_id wt_public_id, p_credential_purpose wh_dim_text) returns wh_dim_key + as $$ + declare + src whx_credential_dimension_target%rowtype; + target whx_credential_dimension_target%rowtype; + new_row wh_credential_dimension%rowtype; + t_id wt_public_id; + begin + select s.target_id into strict t_id + from session as s + where s.public_id = p_session_id; + + select * into target + from whx_credential_dimension_target as t + where t.credential_library_id = p_library_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + select + target.key, t.credential_purpose, + t.credential_library_id, t.credential_library_type, t.credential_library_name, t.credential_library_description, t.credential_library_vault_path, t.credential_library_vault_http_method, t.credential_library_vault_http_request_body, + t.credential_library_username, t.credential_library_key_type_and_bits, + t.credential_store_id, t.credential_store_type, t.credential_store_name, t.credential_store_description, t.credential_store_vault_namespace, t.credential_store_vault_address, + t.target_id, t.target_type, t.target_name, t.target_description, t.target_default_port_number, t.target_session_max_seconds, t.target_session_connection_limit, + t.project_id, t.project_name, t.project_description, + t.organization_id, t.organization_name, t.organization_description + into src + from whx_credential_dimension_source as t + where t.credential_library_id = p_library_id + and t.session_id = p_session_id + and t.target_id = t_id + and t.credential_purpose = p_credential_purpose; + + if src is distinct from target then + update wh_credential_dimension + set current_row_indicator = 'Expired', + row_expiration_time = current_timestamp + where credential_library_id = p_library_id + and target_id = t_id + and credential_purpose = p_credential_purpose + and current_row_indicator = 'Current'; + + insert into wh_credential_dimension ( + credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time + ) + select credential_purpose, + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + 'Current', current_timestamp, 'infinity'::timestamptz + from whx_credential_dimension_source + where credential_library_id = p_library_id + and session_id = p_session_id + and target_id = t_id + and credential_purpose = p_credential_purpose + returning * into new_row; + + return new_row.key; + end if; + + return target.key; + end + $$ language plpgsql; +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 9397376fd..b180af6ff 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -315,7 +315,9 @@ begin; (project_id, public_id, name) values ('p____bwidget', 't_________wb', 'Big Widget Target'), - ('p____swidget', 't_________ws', 'Small Widget Target'); + ('p____swidget', 't_________ws', 'Small Widget Target'), + ('p____swidget', 't________ws2', 'Small Widget Target 2'), + ('p____swidget', 't________ws3', 'Small Widget Target 3'); insert into target_host_set (project_id, target_id, host_set_id) @@ -327,7 +329,9 @@ begin; ('p____bwidget', 't_________wb', 's___1wb-plghs'), ('p____bwidget', 't_________wb', 's___2wb-plghs'), ('p____swidget', 't_________ws', 's___1ws-plghs'), - ('p____swidget', 't_________ws', 's___2ws-plghs'); + ('p____swidget', 't_________ws', 's___2ws-plghs'), + ('p____swidget', 't________ws2', 's___1ws-sths'), + ('p____swidget', 't________ws3', 's___1ws-sths'); end; $$ language plpgsql; @@ -336,9 +340,10 @@ begin; as $$ begin insert into credential_vault_store - (project_id, public_id, name, description, vault_address, namespace) + (project_id, public_id, name, description, vault_address, namespace) values - ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'); + ('p____bwidget', 'vs_______wvs', 'widget vault store', 'None', 'https://vault.widget', 'default'), + ('p____swidget', 'vs______swvs', 'small widget vault store', 'None', 'https://small.vault.widget', 'default'); insert into credential_vault_token (store_id, key_id, status, token_hmac, token, last_renewal_time, expiration_time) @@ -401,13 +406,23 @@ begin; values ('vl______wvl12', 'my_username', 'my_private_key', 'my_passphrase'); + insert into credential_vault_ssh_cert_library + (store_id, public_id, name, description, vault_path, username, key_type, key_bits) + values + ('vs______swvs', 'vscl____wvl1', 'widget ssh admin cert', 'None', '/ssh/issue/admin', 'admin', 'rsa', 4096), + ('vs______swvs', 'vscl____wvl2', 'widget ssh ecdsa', 'None', '/ssh/sign/user', 'user', 'ecdsa', 521), + ('vs______swvs', 'vscl____wvl3', 'widget ssh ed25519', 'None', '/ssh/sign/user', 'user', 'ed25519', 0); + insert into target_credential_library (project_id, target_id, credential_library_id, credential_purpose) values ('p____bwidget', 't_________wb', 'vl______wvl1', 'brokered'), ('p____bwidget', 't_________wb', 'vl______wvl2', 'brokered'), ('p____bwidget', 't_________wb', 'vl______wvl3', 'brokered'), - ('p____bwidget', 't_________wb', 'vl______wvl3', 'injected_application'); + ('p____bwidget', 't_________wb', 'vl______wvl3', 'injected_application'), + ('p____bwidget', 't_________ws', 'vscl____wvl1', 'injected_application'), + ('p____bwidget', 't________ws2', 'vscl____wvl2', 'injected_application'), + ('p____bwidget', 't________ws3', 'vscl____wvl3', 'injected_application'); insert into credential_static_store (project_id, public_id, name, description) @@ -432,9 +447,9 @@ create function _wtt_load_widgets_sessions() returns void as $$ begin insert into session - ( project_id , target_id , host_set_id , host_id , user_id , auth_token_id , certificate , endpoint , public_id) + (project_id, target_id, host_set_id, host_id, user_id, auth_token_id, certificate, endpoint, public_id) values - ('p____bwidget' , 't_________wb' , 's___1wb-sths' , 'h_____wb__01' , 'u_____warren' , 'tok___warren' , 'abc'::bytea , 'ep1' , 's1____warren'); + ('p____swidget', 't_________wb', 's___1wb-sths', 'h_____wb__01', 'u_____warren', 'tok___warren', 'abc'::bytea, 'ep1', 's1____warren'); end; $$ language plpgsql; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql new file mode 100644 index 000000000..f26ca74f0 --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_ssh_cert_library.sql @@ -0,0 +1,148 @@ +begin; + select plan(16); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- validate default values + prepare insert_valid as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc1', '/ssh/sign/foo', 'bar', 'ed25519', 0), + ('vs_______wvs', 'vl______vsc2', '/ssh/sign/foo', 'bar', 'ecdsa', 256), + ('vs_______wvs', 'vl______vsc3', '/ssh/sign/foo', 'bar', 'ecdsa', 384), + ('vs_______wvs', 'vl______vsc4', '/ssh/sign/foo', 'bar', 'ecdsa', 521), + ('vs_______wvs', 'vl______vsc5', '/ssh/sign/foo', 'bar', 'rsa', 2048), + ('vs_______wvs', 'vl______vsc6', '/ssh/sign/foo', 'bar', 'rsa', 3072), + ('vs_______wvs', 'vl______vsc7', '/ssh/sign/foo', 'bar', 'rsa', 4096); + + prepare select_vault_ssh_cert_libraries as + select public_id::text, store_id::text, name::text, description::text, vault_path, username, key_type, key_bits, ttl, key_id, critical_options, extensions, credential_type, project_id::text + from credential_vault_ssh_cert_library + where public_id like 'vl______vsc%' + order by public_id; + + prepare select_libraries as + select public_id::text, store_id::text, credential_type, project_id::text + from credential_library + where public_id in ('vl______vsc1', 'vl______vsc3') + order by public_id; + + select lives_ok('insert_valid'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 384, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + + prepare insert_invalid_key_type as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'unknown', 256); + select throws_ok('insert_invalid_key_type', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_key_bits as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'rsa', 99); + select throws_ok('insert_invalid_key_bits', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_rsa_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'rsa', 384); + select throws_ok('insert_invalid_rsa_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_ecdsa_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'ecdsa', 2048); + select throws_ok('insert_invalid_ecdsa_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_ed25519_key_type_bits_combo as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'ed25519', 2048); + select throws_ok('insert_invalid_ed25519_key_type_bits_combo', 'insert or update on table "credential_vault_ssh_cert_library" violates foreign key constraint "credential_vault_ssh_cert_valid_key_type_key_bits_fkey"'); + + prepare insert_invalid_vault_path as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/not-sign/foo', 'bar', 'ed25519', 0); + select throws_ok('insert_invalid_vault_path', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "vault_path_must_be_sign_or_issue"'); + + prepare insert_invalid_vault_path_empty as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '', 'bar', 'ed25519', 0); + select throws_ok('insert_invalid_vault_path', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "vault_path_must_be_sign_or_issue"'); + + prepare insert_invalid_username as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', '', 'ed25519', 0); + select throws_ok('insert_invalid_username', 'new row for relation "credential_vault_ssh_cert_library" violates check constraint "username_must_not_be_empty"'); + + prepare insert_invalid_credential_type as + insert into credential_vault_ssh_cert_library + (store_id, public_id, vault_path, username, credential_type, key_type, key_bits) + values + ('vs_______wvs', 'vl______vsc8', '/ssh/sign/foo', 'bar', 'username_password', 'ed25519', 0); + select lives_ok('insert_invalid_credential_type'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 384, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc8', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', 'ssh_certificate', 'p____bwidget'), + ('vl______vsc3', 'vs_______wvs', 'ssh_certificate', 'p____bwidget')$$ + ); + + prepare delete_ssh_cred_library as + delete from credential_vault_ssh_cert_library where public_id = 'vl______vsc3'; + + select lives_ok('delete_ssh_cred_library'); + select results_eq( + 'select_vault_ssh_cert_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc2', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 256, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc4', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ecdsa', 521, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc5', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 2048, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc6', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 3072, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc7', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'rsa', 4096, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget'), + ('vl______vsc8', 'vs_______wvs', null, null, '/ssh/sign/foo', 'bar', 'ed25519', 0, null, null, null::bytea, null::bytea, 'ssh_certificate', 'p____bwidget')$$ + ); + + select results_eq( + 'select_libraries', + $$VALUES + ('vl______vsc1', 'vs_______wvs', 'ssh_certificate', 'p____bwidget')$$ + ); + +rollback; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql index 7a9326b70..bc9b1644e 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/dimension.sql @@ -3,20 +3,22 @@ begin; insert into wh_credential_dimension ( credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'brokered', - 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', current_timestamp, 'infinity'::timestamptz + 'vl______wvl1', 'vault generic credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz ); update wh_credential_dimension @@ -30,20 +32,22 @@ begin; insert into wh_credential_dimension ( credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'brokered', - 'vl______wvl1', 'vault credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', current_timestamp, 'infinity'::timestamptz + 'vl______wvl1', 'vault generic credential library', 'gidget vault library', 'None', '/secrets', 'GET', '\x4e6f6e65', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'default', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', current_timestamp, 'infinity'::timestamptz ); select is(count(*), 2::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql new file mode 100644 index 000000000..edf4f20cf --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ecdsa.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library with ecdsa key type +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't________ws2', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's2____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s2____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s2____walter', 'vscl____wvl2', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl2', 'vault ssh certificate credential library', 'widget ssh ecdsa', 'None', '/ssh/sign/user', 'Not Applicable', 'Not Applicable', + 'user', 'ecdsa-521', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't________ws2', 'tcp target', 'Small Widget Target 2', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql new file mode 100644 index 000000000..b62fd3049 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_ed25519.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't________ws3', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's3____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s3____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s3____walter', 'vscl____wvl3', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl3', 'vault ssh certificate credential library', 'widget ssh ed25519', 'None', '/ssh/sign/user', 'Not Applicable', 'Not Applicable', + 'user', 'ed25519', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't________ws3', 'tcp target', 'Small Widget Target 3', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql new file mode 100644 index 000000000..c4506cbb5 --- /dev/null +++ b/internal/db/sqltest/tests/wh/credential_dimension/ssh_certificate_cred_library_rsa.sql @@ -0,0 +1,56 @@ +-- ssh_certificate_cred_library tests the wh_credential_dimension when +-- sessions are created with targets that use a vault ssh cert credential library with rsa key type +begin; + select plan(3); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + -- insert first session, should result in a new credentials dimension + insert into session + ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) + values + ('p____swidget', 't_________ws', 'u_____walter', 'tok___walter', 'abc'::bytea, 'ep1', 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1ws-sths', 'h_____ws__01'); + insert into session_credential_dynamic + ( session_id, library_id, credential_id, credential_purpose) + values + ('s1____walter', 'vscl____wvl1', null, 'injected_application'); + select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; + + prepare select_credential_dimensions as + select + credential_purpose::text, + credential_library_id::text, credential_library_type::text, credential_library_name::text, credential_library_description::text, credential_library_vault_path::text, credential_library_vault_http_method::text, credential_library_vault_http_request_body::text, + credential_library_username::text, credential_library_key_type_and_bits::text, + credential_store_id::text, credential_store_type::text, credential_store_name::text, credential_store_description::text, credential_store_vault_namespace::text, credential_store_vault_address::text, + target_id::text, target_type::text, target_name::text, target_description::text, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id::text, project_name::text, project_description::text, + organization_id::text, organization_name::text, organization_description::text, + current_row_indicator::text + from wh_credential_dimension + where organization_id = 'o_____widget'; + + select results_eq( + 'select_credential_dimensions', + $$VALUES + ( + 'injected_application', + 'vscl____wvl1', 'vault ssh certificate credential library', 'widget ssh admin cert', 'None', '/ssh/issue/admin', 'Not Applicable', 'Not Applicable', + 'admin', 'rsa-4096', + 'vs______swvs', 'vault credential store', 'small widget vault store', 'None', 'default', 'https://small.vault.widget', + 't_________ws', 'tcp target', 'Small Widget Target', 'None', 0, 28800, -1, + 'p____swidget', 'Small Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current' + )$$ + ); + + select * from finish(); +rollback; + diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql index 91e728474..8b5f39515 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/source.sql @@ -18,36 +18,39 @@ begin; select is(s.*, row( 's1____walter', - 'brokered', -- credential_purpose, - 'vl______wvl1', -- credential_library_id, - 'vault credential library', -- credential_library_type, - 'widget vault library', -- credential_library_name, - 'None', -- credential_library_description, - '/secrets', -- credential_library_vault_path, - 'GET', -- credential_library_vault_http_method, - 'None', -- credential_library_vault_http_request_body, + 'brokered', -- credential_purpose, + 'vl______wvl1', -- credential_library_id, + 'vault generic credential library', -- credential_library_type, + 'widget vault library', -- credential_library_name, + 'None', -- credential_library_description, + '/secrets', -- credential_library_vault_path, + 'GET', -- credential_library_vault_http_method, + 'Not Applicable', -- credential_library_vault_http_request_body, - 'vs_______wvs', -- credential_store_id, - 'vault credential store', -- credential_store_type, - 'widget vault store', -- credential_store_name, - 'None', -- credential_store_description, - 'default', -- credential_store_vault_namespace, - 'https://vault.widget', -- credential_store_vault_address, + 'Not Applicable', -- credential_library_username, + 'Not Applicable', -- credential_library_key_type_and_bits, - 't_________wb', -- target_id, - 'tcp target', -- target_type, - 'Big Widget Target', -- target_name, - 'None', -- target_description, - 0, -- target_default_port_number, - 28800, -- target_session_max_seconds, - -1, -- target_session_connection_limit, + 'vs_______wvs', -- credential_store_id, + 'vault credential store', -- credential_store_type, + 'widget vault store', -- credential_store_name, + 'None', -- credential_store_description, + 'default', -- credential_store_vault_namespace, + 'https://vault.widget', -- credential_store_vault_address, - 'p____bwidget', -- project_id, - 'Big Widget Factory', -- project_name, - 'None', -- project_description, - 'o_____widget', -- organization_id, - 'Widget Inc', -- organization_name, - 'None' -- organization_description + 't_________wb', -- target_id, + 'tcp target', -- target_type, + 'Big Widget Target', -- target_name, + 'None', -- target_description, + 0, -- target_default_port_number, + 28800, -- target_session_max_seconds, + -1, -- target_session_connection_limit, + + 'p____bwidget', -- project_id, + 'Big Widget Factory', -- project_name, + 'None', -- project_description, + 'o_____widget', -- organization_id, + 'Widget Inc', -- organization_name, + 'None' -- organization_description )::whx_credential_dimension_source) from whx_credential_dimension_source as s where s.target_id = 't_________wb'; diff --git a/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql index 67ba4bd45..26e066681 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension_views/target.sql @@ -8,33 +8,36 @@ begin; ( key, credential_purpose, - credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, - credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, - target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, - project_id, project_name, project_description, - organization_id, organization_name, organization_description, - current_row_indicator, row_effective_time, row_expiration_time + credential_library_id, credential_library_type, credential_library_name, credential_library_description, credential_library_vault_path, credential_library_vault_http_method, credential_library_vault_http_request_body, + credential_library_username, credential_library_key_type_and_bits, + credential_store_id, credential_store_type, credential_store_name, credential_store_description, credential_store_vault_namespace, credential_store_vault_address, + target_id, target_type, target_name, target_description, target_default_port_number, target_session_max_seconds, target_session_connection_limit, + project_id, project_name, project_description, + organization_id, organization_name, organization_description, + current_row_indicator, row_effective_time, row_expiration_time ) values ( 'wcd________1', 'brokered', - 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None', - 'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz + 'vl_______wvl', 'vault generic credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None', + 'Current', '2021-07-21T12:01'::timestamptz, 'infinity'::timestamptz ); select is(t.*, row( 'wcd________1', 'brokered', - 'vl_______wvl', 'vault credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', - 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', - 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, - 'p____bwidget', 'Big Widget Factory', 'None', - 'o_____widget', 'Widget Inc', 'None' + 'vl_______wvl', 'vault generic credential library', 'widget vault library', 'None', '/secrets', 'GET', 'None', + 'Not Applicable', 'Not Applicable', + 'vs_______wvs', 'vault credential store', 'widget vault store', 'None', 'blue', 'https://vault.widget', + 't_________wb', 'tcp target', 'Big Widget Target', 'None', 0, 28800, 1, + 'p____bwidget', 'Big Widget Factory', 'None', + 'o_____widget', 'Widget Inc', 'None' )::whx_credential_dimension_target) from whx_credential_dimension_target as t where t.target_id = 't_________wb'; diff --git a/internal/gen/controller/servers/services/credential.pb.go b/internal/gen/controller/servers/services/credential.pb.go index 6edb422ab..2d310c0a4 100644 --- a/internal/gen/controller/servers/services/credential.pb.go +++ b/internal/gen/controller/servers/services/credential.pb.go @@ -29,6 +29,7 @@ type Credential struct { // // *Credential_UsernamePassword // *Credential_SshPrivateKey + // *Credential_SshCertificate Credential isCredential_Credential `protobuf_oneof:"credential"` } @@ -85,6 +86,13 @@ func (x *Credential) GetSshPrivateKey() *SshPrivateKey { return nil } +func (x *Credential) GetSshCertificate() *SshCertificate { + if x, ok := x.GetCredential().(*Credential_SshCertificate); ok { + return x.SshCertificate + } + return nil +} + type isCredential_Credential interface { isCredential_Credential() } @@ -97,10 +105,16 @@ type Credential_SshPrivateKey struct { SshPrivateKey *SshPrivateKey `protobuf:"bytes,3,opt,name=ssh_private_key,json=sshPrivateKey,proto3,oneof"` } +type Credential_SshCertificate struct { + SshCertificate *SshCertificate `protobuf:"bytes,4,opt,name=ssh_certificate,json=sshCertificate,proto3,oneof"` +} + func (*Credential_UsernamePassword) isCredential_Credential() {} func (*Credential_SshPrivateKey) isCredential_Credential() {} +func (*Credential_SshCertificate) isCredential_Credential() {} + // UsernamePassword is a credential containing a username and a password. type UsernamePassword struct { state protoimpl.MessageState @@ -227,6 +241,74 @@ func (x *SshPrivateKey) GetPrivateKeyPassphrase() string { return "" } +// SshCertificate is a credential containing a username, private key, and +// client certificate. +type SshCertificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The username of the credential + Username string `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty"` // @gotags: `class:"public"` + // The private key of the credential + PrivateKey string `protobuf:"bytes,20,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` // @gotags: `class:"secret"` + // The client certificate signed by a CA to establish trust of the private key. + Certificate string `protobuf:"bytes,30,opt,name=certificate,proto3" json:"certificate,omitempty"` // @gotags: `class:"public"` +} + +func (x *SshCertificate) Reset() { + *x = SshCertificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_servers_services_v1_credential_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SshCertificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshCertificate) ProtoMessage() {} + +func (x *SshCertificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_servers_services_v1_credential_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SshCertificate.ProtoReflect.Descriptor instead. +func (*SshCertificate) Descriptor() ([]byte, []int) { + return file_controller_servers_services_v1_credential_proto_rawDescGZIP(), []int{3} +} + +func (x *SshCertificate) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *SshCertificate) GetPrivateKey() string { + if x != nil { + return x.PrivateKey + } + return "" +} + +func (x *SshCertificate) GetCertificate() string { + if x != nil { + return x.Certificate + } + return "" +} + var File_controller_servers_services_v1_credential_proto protoreflect.FileDescriptor var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ @@ -235,7 +317,7 @@ var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x22, 0xe9, 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x31, 0x22, 0xc4, 0x02, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x5f, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, @@ -247,28 +329,41 @@ var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{ 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x73, 0x68, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x0d, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4a, 0x0a, - 0x10, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x0d, 0x53, 0x73, - 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x73, 0x73, + 0x68, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x73, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x73, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4a, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x0d, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, + 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x22, 0x6f, 0x0a, 0x0e, 0x53, 0x73, 0x68, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, - 0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x42, 0x51, - 0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x42, 0x51, 0x5a, 0x4f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -283,20 +378,22 @@ func file_controller_servers_services_v1_credential_proto_rawDescGZIP() []byte { return file_controller_servers_services_v1_credential_proto_rawDescData } -var file_controller_servers_services_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_servers_services_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_servers_services_v1_credential_proto_goTypes = []interface{}{ (*Credential)(nil), // 0: controller.servers.services.v1.Credential (*UsernamePassword)(nil), // 1: controller.servers.services.v1.UsernamePassword (*SshPrivateKey)(nil), // 2: controller.servers.services.v1.SshPrivateKey + (*SshCertificate)(nil), // 3: controller.servers.services.v1.SshCertificate } var file_controller_servers_services_v1_credential_proto_depIdxs = []int32{ 1, // 0: controller.servers.services.v1.Credential.username_password:type_name -> controller.servers.services.v1.UsernamePassword 2, // 1: controller.servers.services.v1.Credential.ssh_private_key:type_name -> controller.servers.services.v1.SshPrivateKey - 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 + 3, // 2: controller.servers.services.v1.Credential.ssh_certificate:type_name -> controller.servers.services.v1.SshCertificate + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_controller_servers_services_v1_credential_proto_init() } @@ -341,10 +438,23 @@ func file_controller_servers_services_v1_credential_proto_init() { return nil } } + file_controller_servers_services_v1_credential_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SshCertificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_servers_services_v1_credential_proto_msgTypes[0].OneofWrappers = []interface{}{ (*Credential_UsernamePassword)(nil), (*Credential_SshPrivateKey)(nil), + (*Credential_SshCertificate)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -352,7 +462,7 @@ func file_controller_servers_services_v1_credential_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_servers_services_v1_credential_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto b/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto index e67589c53..fe6fb8826 100644 --- a/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto +++ b/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto @@ -17,10 +17,7 @@ message CredentialLibrary { string id = 10; // @gotags: `class:"public"` // The ID of the Credential Store of which this Credential Library is a part. - string credential_store_id = 20 [ - json_name = "credential_store_id", - (controller.custom_options.v1.subtype_source_id) = true - ]; // @gotags: `class:"public"` + string credential_store_id = 20 [json_name = "credential_store_id"]; // @gotags: `class:"public"` // Output only. Scope information for this Credential Library. resources.scopes.v1.ScopeInfo scope = 30; @@ -67,6 +64,16 @@ message CredentialLibrary { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "vault" ]; + VaultSSHCertificateCredentialLibraryAttributes vault_ssh_certificate_credential_library_attributes = 102 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "vault-ssh-certificate" + ]; + VaultCredentialLibraryAttributes vault_generic_credential_library_attributes = 103 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "vault-generic" + ]; } // Output only. The available actions on this resource for this user. @@ -116,3 +123,78 @@ message VaultCredentialLibraryAttributes { } ]; // @gotags: `class:"secret"` } + +// The attributes of a vault SSH Certificate Credential Library. +message VaultSSHCertificateCredentialLibraryAttributes { + // The path in Vault to request credentials from. + google.protobuf.StringValue path = 10 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.path" + that: "VaultPath" + } + ]; // @gotags: `class:"public"` + + // The username to use when making an SSH connection. + google.protobuf.StringValue username = 20 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.username" + that: "Username" + } + ]; // @gotags: `class:"sensitive"` + + // The key type to use when generating an SSH private key. + google.protobuf.StringValue key_type = 30 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_type" + that: "KeyType" + } + ]; // @gotags: `class:"public"` + + // The number of bits to use to generate an SSH private key. + google.protobuf.UInt32Value key_bits = 40 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_bits" + that: "KeyBits" + } + ]; // @gotags: `class:"public"` + + // The requested time to live for the certificate. + google.protobuf.StringValue ttl = 50 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.ttl" + that: "Ttl" + } + ]; // @gotags: `class:"public"` + + // The key id that the created certificate should have. + google.protobuf.StringValue key_id = 60 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.key_id" + that: "KeyId" + } + ]; // @gotags: `class:"public"` + + // The critical options that the certificate should be signed for. + map critical_options = 70 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.critical_options" + that: "CriticalOptions" + } + ]; // @gotags: `class:"public"` + + // The extensions that the certificate should be signed for. + map extensions = 80 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.extensions" + that: "Extensions" + } + ]; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/servers/services/v1/credential.proto b/internal/proto/controller/servers/services/v1/credential.proto index 8943defff..7ca4a4358 100644 --- a/internal/proto/controller/servers/services/v1/credential.proto +++ b/internal/proto/controller/servers/services/v1/credential.proto @@ -11,6 +11,7 @@ message Credential { oneof credential { UsernamePassword username_password = 2; SshPrivateKey ssh_private_key = 3; + SshCertificate ssh_certificate = 4; } } @@ -35,3 +36,16 @@ message SshPrivateKey { // The optional passphrase of the private_key string private_key_passphrase = 30; // @gotags: `class:"secret"` } + +// SshCertificate is a credential containing a username, private key, and +// client certificate. +message SshCertificate { + // The username of the credential + string username = 10; // @gotags: `class:"public"` + + // The private key of the credential + string private_key = 20; // @gotags: `class:"secret"` + + // The client certificate signed by a CA to establish trust of the private key. + string certificate = 30; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto index 1022bf44b..965ffb53d 100644 --- a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto @@ -265,6 +265,107 @@ message CredentialLibrary { string credential_type = 11; } +message SSHCertificateCredentialLibrary { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within project_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // store_id of the owning vault credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string store_id = 6; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // vault_path is the path in Vault to request credentials from. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string vault_path = 8 [(custom_options.v1.mask_mapping) = { + this: "VaultPath" + that: "attributes.path" + }]; + + // username is the username to use when making an SSH connection. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string username = 9 [(custom_options.v1.mask_mapping) = { + this: "Username" + that: "attributes.username" + }]; + + // key_type specifies the key type to use when generating an SSH private key. + // Values must be "rsa", "ed25519", or "ecdsa". + // @inject_tag: `gorm:"not_null"` + string key_type = 10 [(custom_options.v1.mask_mapping) = { + this: "KeyType" + that: "attributes.key_type" + }]; + + // key_bits specifies the number of bits to use to generate an SSH private key. + // Not used if key_type is ed25519. + // @inject_tag: `gorm:"not_null"` + uint32 key_bits = 11 [(custom_options.v1.mask_mapping) = { + this: "KeyBits" + that: "attributes.key_bits" + }]; + + // ttl specifies the requested time to live for the certificate. + // @inject_tag: `gorm:"default:null"` + string ttl = 12 [(custom_options.v1.mask_mapping) = { + this: "Ttl" + that: "attributes.ttl" + }]; + + // key_id specifies the key id that the created certificate should have. + // @inject_tag: `gorm:"default:null"` + string key_id = 13 [(custom_options.v1.mask_mapping) = { + this: "KeyId" + that: "attributes.key_id" + }]; + + // critical_options specifies a map of the critical options that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + string critical_options = 14 [(custom_options.v1.mask_mapping) = { + this: "CriticalOptions" + that: "attributes.critical_options" + }]; + + // extensions specifies a map of the extensions that the certificate should be signed for. + // @inject_tag: `gorm:"default:null"` + string extensions = 15 [(custom_options.v1.mask_mapping) = { + this: "Extensions" + that: "attributes.extensions" + }]; + + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + string credential_type = 16; +} + message Credential { // public_id is a surrogate key suitable for use in a public API. // @inject_tag: `gorm:"primary_key"` diff --git a/internal/tests/api/credentiallibraries/credentiallibrary_test.go b/internal/tests/api/credentiallibraries/credentiallibrary_test.go index 2016c92be..60a5684df 100644 --- a/internal/tests/api/credentiallibraries/credentiallibrary_test.go +++ b/internal/tests/api/credentiallibraries/credentiallibrary_test.go @@ -44,7 +44,7 @@ func TestList(t *testing.T) { expected = append(expected, &credentiallibraries.CredentialLibrary{Name: fmt.Sprint(i), Attributes: map[string]any{"vault_path": "something"}}) } - cl, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName(expected[0].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) + cl, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName(expected[0].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) expected[0] = cl.Item @@ -53,7 +53,7 @@ func TestList(t *testing.T) { assert.ElementsMatch(comparableSetSlice(expected[:1]), comparableSetSlice(ul.Items)) for i := 1; i < 10; i++ { - cl, err = lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName(expected[i].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) + cl, err = lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName(expected[i].Name), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) expected[i] = cl.Item } @@ -115,8 +115,9 @@ func TestCrud(t *testing.T) { lClient := credentiallibraries.NewClient(client) - r, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo"), + r, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo"), credentiallibraries.WithVaultCredentialLibraryPath("something")) + require.Nil(err) checkResource(t, "create", r.Item, err, "foo", 1) r, err = lClient.Read(tc.Context(), r.Item.Id) @@ -157,7 +158,7 @@ func TestErrors(t *testing.T) { lClient := credentiallibraries.NewClient(client) - l, err := lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo"), + l, err := lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo"), credentiallibraries.WithVaultCredentialLibraryPath("something")) require.NoError(err) assert.NotNil(l) @@ -178,7 +179,7 @@ func TestErrors(t *testing.T) { assert.NotNil(apiErr) assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) - l, err = lClient.Create(tc.Context(), cs.Item.Id, credentiallibraries.WithName("foo")) + l, err = lClient.Create(tc.Context(), cs.Item.Type, cs.Item.Id, credentiallibraries.WithName("foo")) require.Error(err) apiErr = api.AsServerError(err) assert.NotNil(apiErr) diff --git a/internal/tests/api/targets/target_test.go b/internal/tests/api/targets/target_test.go index 90685d7a3..dd4ee9a26 100644 --- a/internal/tests/api/targets/target_test.go +++ b/internal/tests/api/targets/target_test.go @@ -86,11 +86,11 @@ func TestCredentialSourcesASD(t *testing.T) { require.NotNil(csVault) lClient := credentiallibraries.NewClient(client) - lib1, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) + lib1, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) require.NoError(err) require.NotNil(lib1) - lib2, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) + lib2, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) require.NoError(err) require.NotNil(lib2) @@ -204,11 +204,11 @@ func TestDeprecatedCredentialSourcesASD(t *testing.T) { require.NotNil(csVault) lClient := credentiallibraries.NewClient(client) - lib1, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) + lib1, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something1")) require.NoError(err) require.NotNil(lib1) - lib2, err := lClient.Create(tc.Context(), csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) + lib2, err := lClient.Create(tc.Context(), csVault.Item.Type, csVault.Item.Id, credentiallibraries.WithVaultCredentialLibraryPath("something2")) require.NoError(err) require.NotNil(lib2) diff --git a/internal/tests/cli/Makefile b/internal/tests/cli/Makefile index f153e4e37..b9531bd03 100644 --- a/internal/tests/cli/Makefile +++ b/internal/tests/cli/Makefile @@ -1,5 +1,36 @@ all: test +REGISTRY_NAME ?= docker.io/hashicorp +TEST_VAULT_IMAGE_NAME=vault +TEST_VAULT_IMAGE ?= $(REGISTRY_NAME)/$(TEST_VAULT_IMAGE_NAME):1.12.2 +TEST_VAULT_PORT ?= 8200 +TEST_VAULT_CONTAINER_NAME ?= boundary-cli-tests-vault + +VAULT_ADDR ?= "http://127.0.0.1:$(TEST_VAULT_PORT)" +VAULT_TOKEN ?= boundarytok + +.PHONY: test-vault-up +test-vault-up: + @echo "Using image: $(TEST_VAULT_IMAGE)" + @echo " vault addr: $(VAULT_ADDR)" + @echo "vault token: $(VAULT_TOKEN)" + @docker run \ + -d \ + --name $(TEST_VAULT_CONTAINER_NAME) \ + -e VAULT_DEV_ROOT_TOKEN_ID=$(VAULT_TOKEN) \ + -p $(TEST_VAULT_PORT):8200 \ + --cap-add IPC_LOCK \ + $(TEST_VAULT_IMAGE) + @echo "Container name: $(TEST_VAULT_CONTAINER_NAME)" + @echo "Test vault available at: $(VAULT_ADDR)" + @echo "For vault logs run:" + @echo " docker logs $(TEST_VAULT_CONTAINER_NAME)" + +.PHONY: test-vault-down +test-vault-down: + docker stop $(TEST_VAULT_CONTAINER_NAME) || true + docker rm -v $(TEST_VAULT_CONTAINER_NAME) || true + .PHONY: test test: ./test.sh diff --git a/internal/tests/cli/boundary/_credential_libraries.bash b/internal/tests/cli/boundary/_credential_libraries.bash new file mode 100644 index 000000000..606399e6b --- /dev/null +++ b/internal/tests/cli/boundary/_credential_libraries.bash @@ -0,0 +1,36 @@ +load _authorized_actions + +function create_vault_ssh_certificate_library() { + boundary credential-libraries \ + create vault-ssh-certificate $@ +} + +function create_vault_generic_library() { + boundary credential-libraries \ + create vault-generic $@ +} + +function create_vault_library() { + boundary credential-libraries \ + create vault $@ +} + +function read_credential_library() { + boundary credential-libraries read -id $1 -format json +} + +function delete_credential_library() { + boundary credential-libraries delete -id $1 +} + +function list_credential_libraries() { + local csid=$1 + boundary credential-libraries list -credential-store-id $1 -format json +} + +function credential_library_id() { + local name=$1 + local csid=$2 + + strip $(list_credential_libraries $csid | jq -c ".items[] | select(.name | contains(\"$name\")) | .[\"id\"]") +} diff --git a/internal/tests/cli/boundary/_credential_stores.bash b/internal/tests/cli/boundary/_credential_stores.bash index 54db7cc93..26a8fb907 100644 --- a/internal/tests/cli/boundary/_credential_stores.bash +++ b/internal/tests/cli/boundary/_credential_stores.bash @@ -10,6 +10,10 @@ function create_static_credential_store() { -scope-id $sid } +function create_vault_credential_store() { + boundary credential-stores create vault $@ +} + function read_credential_store() { boundary credential-stores read -id $1 -format json } diff --git a/internal/tests/cli/boundary/_vault.bash b/internal/tests/cli/boundary/_vault.bash new file mode 100644 index 000000000..4b7955ae2 --- /dev/null +++ b/internal/tests/cli/boundary/_vault.bash @@ -0,0 +1,47 @@ + +function vault_write_boundary_policy() { + ( + cat < controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp - 5, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct + 5, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 6, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue + 6, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue + 7, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp + 7, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp + 8, // 5: controller.api.resources.credentiallibraries.v1.CredentialLibrary.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes - 5, // 7: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct - 3, // 8: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue - 3, // 9: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue - 3, // 10: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue - 11, // [11:11] is the sub-list for method output_type - 11, // [11:11] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 2, // 7: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_ssh_certificate_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes + 1, // 8: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_generic_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes + 8, // 9: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct + 6, // 10: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 6, // 11: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue + 6, // 12: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue + 6, // 13: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 6, // 14: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.username:type_name -> google.protobuf.StringValue + 6, // 15: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_type:type_name -> google.protobuf.StringValue + 9, // 16: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_bits:type_name -> google.protobuf.UInt32Value + 6, // 17: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ttl:type_name -> google.protobuf.StringValue + 6, // 18: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_id:type_name -> google.protobuf.StringValue + 3, // 19: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.critical_options:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + 4, // 20: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.extensions:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + 21, // [21:21] is the sub-list for method output_type + 21, // [21:21] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_controller_api_resources_credentiallibraries_v1_credential_library_proto_init() } @@ -474,10 +729,24 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro return nil } } + file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VaultSSHCertificateCredentialLibraryAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[0].OneofWrappers = []interface{}{ (*CredentialLibrary_Attributes)(nil), (*CredentialLibrary_VaultCredentialLibraryAttributes)(nil), + (*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes)(nil), + (*CredentialLibrary_VaultGenericCredentialLibraryAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -485,7 +754,7 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/testing/internal/e2e/tests/static_with_vault/credential_store_test.go b/testing/internal/e2e/tests/static_with_vault/credential_store_test.go index 339c7eb68..c29749193 100644 --- a/testing/internal/e2e/tests/static_with_vault/credential_store_test.go +++ b/testing/internal/e2e/tests/static_with_vault/credential_store_test.go @@ -256,7 +256,7 @@ func TestApiVaultCredentialStore(t *testing.T) { // Create a credential library for the private key clClient := credentiallibraries.NewClient(client) - newCredentialLibraryResult, err := clClient.Create(ctx, newCredentialStoreId, + newCredentialLibraryResult, err := clClient.Create(ctx, "vault", newCredentialStoreId, credentiallibraries.WithVaultCredentialLibraryPath(c.VaultSecretPath+"/data/"+privateKeySecretName), credentiallibraries.WithCredentialType("ssh_private_key"), ) @@ -265,7 +265,7 @@ func TestApiVaultCredentialStore(t *testing.T) { t.Logf("Created Credential Library: %s", newPrivateKeyCredentialLibraryId) // Create a credential library for the password - newCredentialLibraryResult, err = clClient.Create(ctx, newCredentialStoreId, + newCredentialLibraryResult, err = clClient.Create(ctx, "vault", newCredentialStoreId, credentiallibraries.WithVaultCredentialLibraryPath(c.VaultSecretPath+"/data/"+passwordSecretName), credentiallibraries.WithCredentialType("username_password"), )