diff --git a/.github/workflows/schema-diff.yml b/.github/workflows/schema-diff.yml index 750746afe..a3952af06 100644 --- a/.github/workflows/schema-diff.yml +++ b/.github/workflows/schema-diff.yml @@ -74,26 +74,47 @@ jobs: To understand how these diffs are generated and some limitations see the [documentation](https://github.com/hashicorp/boundary/blob/main/scripts/schema-diff.sh) of the script. - #### Functions - $(getdiff .schema-diff/funcs.diff) +
+ Functions - #### Tables - $(getdiff .schema-diff/tables.diff) + $(getdiff .schema-diff/funcs.diff) +
- #### Views - $(getdiff .schema-diff/views.diff) +
+ Tables - #### Triggers - $(getdiff .schema-diff/triggers.diff) + $(getdiff .schema-diff/tables.diff) +
- #### Indexes - $(getdiff .schema-diff/indexes.diff) +
+ Views - #### Constraints - $(getdiff .schema-diff/constraints.diff) + $(getdiff .schema-diff/views.diff) +
- #### Foreign Key Constraints - $(getdiff .schema-diff/fk_constraints.diff) +
+ Triggers + + $(getdiff .schema-diff/triggers.diff) +
+ +
+ Indexes + + $(getdiff .schema-diff/indexes.diff) +
+ +
+ Constraints + + $(getdiff .schema-diff/constraints.diff) +
+ +
+ Foreign Key Constraints + + $(getdiff .schema-diff/fk_constraints.diff) +
EOF # Parse it through jq to build a valid json object. diff --git a/api/credentiallibraries/option.gen.go b/api/credentiallibraries/option.gen.go index ffbadffd7..9e9ff2584 100644 --- a/api/credentiallibraries/option.gen.go +++ b/api/credentiallibraries/option.gen.go @@ -383,6 +383,18 @@ func WithVaultCredentialLibraryPath(inPath string) Option { } } +func WithVaultLdapCredentialLibraryPath(inPath string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = any(map[string]any{}) + } + val := raw.(map[string]any) + val["path"] = inPath + o.postMap["attributes"] = val + } +} + func WithVaultSSHCertificateCredentialLibraryPath(inPath string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/api/credentiallibraries/vault_ldap_credential_library_attributes.gen.go b/api/credentiallibraries/vault_ldap_credential_library_attributes.gen.go new file mode 100644 index 000000000..ce3b26dc2 --- /dev/null +++ b/api/credentiallibraries/vault_ldap_credential_library_attributes.gen.go @@ -0,0 +1,40 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package credentiallibraries + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type VaultLdapCredentialLibraryAttributes struct { + Path string `json:"path,omitempty"` +} + +func AttributesMapToVaultLdapCredentialLibraryAttributes(in map[string]any) (*VaultLdapCredentialLibraryAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out VaultLdapCredentialLibraryAttributes + 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) GetVaultLdapCredentialLibraryAttributes() (*VaultLdapCredentialLibraryAttributes, error) { + if pt.Type != "vault-ldap" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential-library is of type %s", "vault-ldap", pt.Type) + } + return AttributesMapToVaultLdapCredentialLibraryAttributes(pt.Attributes) +} diff --git a/enos/enos-scenario-e2e-docker-base-with-vault.hcl b/enos/enos-scenario-e2e-docker-base-with-vault.hcl index 6c800c847..5e4b75f9a 100644 --- a/enos/enos-scenario-e2e-docker-base-with-vault.hcl +++ b/enos/enos-scenario-e2e-docker-base-with-vault.hcl @@ -113,6 +113,17 @@ scenario "e2e_docker_base_with_vault" { } } + step "create_ldap_server" { + module = module.docker_ldap + depends_on = [ + step.create_docker_network + ] + variables { + image_name = "${var.docker_mirror}/osixia/openldap:latest" + network_name = [local.network_cluster] + } + } + step "run_e2e_test" { module = module.test_e2e_docker depends_on = [ @@ -141,6 +152,13 @@ scenario "e2e_docker_base_with_vault" { vault_root_token = step.create_vault.token vault_port = step.create_vault.port max_page_size = step.create_boundary.max_page_size + ldap_address = step.create_ldap_server.address + ldap_domain_dn = step.create_ldap_server.domain_dn + ldap_admin_dn = step.create_ldap_server.admin_dn + ldap_admin_password = step.create_ldap_server.admin_password + ldap_user_name = step.create_ldap_server.user_name + ldap_user_password = step.create_ldap_server.user_password + ldap_group_name = step.create_ldap_server.group_name } } } diff --git a/globals/prefixes.go b/globals/prefixes.go index 5286d664f..801b75f5a 100644 --- a/globals/prefixes.go +++ b/globals/prefixes.go @@ -71,6 +71,8 @@ const ( VaultSshCertificateCredentialLibraryPrefix = "clvsclt" // DynamicCredentialPrefix is the prefix for Vault dynamic credentials VaultDynamicCredentialPrefix = "cdvlt" + // VaultLdapCredentialLibraryPrefix is the prefix for Vault Ldap credential libraries + VaultLdapCredentialLibraryPrefix = "clvllt" // UsernamePasswordCredentialPrefix is the prefix for username/password // creds @@ -243,6 +245,10 @@ var prefixToResourceType = map[string]ResourceInfo{ Type: resource.CredentialLibrary, Subtype: UnknownSubtype, }, + VaultLdapCredentialLibraryPrefix: { + Type: resource.CredentialLibrary, + Subtype: UnknownSubtype, + }, VaultDynamicCredentialPrefix: { Type: resource.Credential, Subtype: UnknownSubtype, diff --git a/go.mod b/go.mod index 5049649dd..a7a066c06 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/favadi/protoc-go-inject-tag v1.4.0 github.com/glebarez/sqlite v1.11.0 + github.com/go-ldap/ldap/v3 v3.4.11 github.com/golang-migrate/migrate/v4 v4.19.0 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 github.com/golang/protobuf v1.5.4 @@ -139,10 +140,9 @@ require ( github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect - github.com/go-ldap/ldap/v3 v3.4.8 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3590c2060..085cad4d0 100644 --- a/go.sum +++ b/go.sum @@ -128,16 +128,15 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= -github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= -github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -184,8 +183,6 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= @@ -494,8 +491,6 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -504,10 +499,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= @@ -577,9 +569,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -603,7 +593,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -612,10 +601,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -656,7 +642,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= @@ -667,7 +652,6 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 5b32f31dd..f204c9af1 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -641,6 +641,22 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &credentiallibraries.VaultLdapCredentialLibraryAttributes{}, + outFile: "credentiallibraries/vault_ldap_credential_library_attributes.gen.go", + subtypeName: "VaultLdapCredentialLibrary", + subtype: "vault-ldap", + fieldOverrides: []fieldInfo{ + { + Name: "Path", + SkipDefault: true, + }, + }, + parentTypeName: "CredentialLibrary", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentiallibraries.CredentialLibrary{}, outFile: "credentiallibraries/credential_library.gen.go", diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index f2f5f050c..bbd7cc37c 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -495,6 +495,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", } }), + "credential-libraries create vault-ldap": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentiallibrariescmd.VaultLdapCommand{ + Command: base.NewCommand(ui, opts...), + Func: "create", + } + }), "credential-libraries update": wrapper.Wrap(func() wrapper.WrappableCommand { return &credentiallibrariescmd.Command{ Command: base.NewCommand(ui, opts...), @@ -519,6 +525,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", } }), + "credential-libraries update vault-ldap": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentiallibrariescmd.VaultLdapCommand{ + Command: base.NewCommand(ui, opts...), + Func: "update", + } + }), "credential-stores": func() (cli.Command, error) { return &credentialstorescmd.Command{ diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index c68d50e2f..287ecf4c1 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -151,6 +151,8 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo keySubstMap = genericKeySubstMap case "vault-ssh-certificate": keySubstMap = sshCertKeySubstMap + case "vault-ldap": + keySubstMap = ldapKeySubstMap } maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) @@ -218,3 +220,7 @@ var sshCertKeySubstMap = map[string]string{ "critical_options": "Critical Options", "extensions": "Extensions", } + +var ldapKeySubstMap = map[string]string{ + "path": "Path", +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go new file mode 100644 index 000000000..e2391ae24 --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go @@ -0,0 +1,278 @@ +// Code generated by "make cli"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +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 initVaultLdapFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultLdapActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultLdapMap[k] = append(flagsVaultLdapMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultLdapCommand)(nil) + _ cli.CommandAutocomplete = (*VaultLdapCommand)(nil) +) + +type VaultLdapCommand struct { + *base.Command + + Func string + + plural string + + extraVaultLdapCmdVars +} + +func (c *VaultLdapCommand) AutocompleteArgs() complete.Predictor { + initVaultLdapFlags() + return complete.PredictAnything +} + +func (c *VaultLdapCommand) AutocompleteFlags() complete.Flags { + initVaultLdapFlags() + return c.Flags().Completions() +} + +func (c *VaultLdapCommand) Synopsis() string { + if extra := extraVaultLdapSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential-library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-ldap-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultLdapCommand) Help() string { + initVaultLdapFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultLdapHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultLdapMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultLdapCommand) Flags() *base.FlagSets { + if len(flagsVaultLdapMap[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-ldap-type credential library", flagsVaultLdapMap, c.Func) + + extraVaultLdapFlagsFunc(c, set, f) + + return set +} + +func (c *VaultLdapCommand) Run(args []string) int { + initVaultLdapFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-ldap-type credential library" + switch c.Func { + case "list": + c.plural = "vault-ldap-type credential libraries" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultLdapMap[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(flagsVaultLdapMap[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 := extraVaultLdapFlagsHandlingFunc(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-ldap", 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 = executeExtraVaultLdapActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultLdapActionOutput(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 *VaultLdapCommand) 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 ( + extraVaultLdapActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultLdapSynopsisFunc = func(*VaultLdapCommand) string { return "" } + extraVaultLdapFlagsFunc = func(*VaultLdapCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultLdapFlagsHandlingFunc = func(*VaultLdapCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultLdapActions = func(_ *VaultLdapCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultLdapActionOutput = func(*VaultLdapCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go new file mode 100644 index 000000000..be370e26d --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentiallibrariescmd + +import ( + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func init() { + extraVaultLdapActionsFlagsMapFunc = extraVaultLdapActionsFlagsMapFuncImpl + extraVaultLdapFlagsFunc = extraVaultLdapFlagsFuncImpl + extraVaultLdapFlagsHandlingFunc = extraVaultLdapFlagHandlingFuncImpl +} + +type extraVaultLdapCmdVars struct { + flagPath string +} + +func extraVaultLdapActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + }, + "update": { + pathFlagName, + }, + } + return flags +} + +func extraVaultLdapFlagsFuncImpl(c *VaultLdapCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault Ldap Credential Library Options") + + for _, name := range flagsVaultLdapMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + } + } +} + +func extraVaultLdapFlagHandlingFuncImpl(c *VaultLdapCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultLdapCredentialLibraryPath(c.flagPath)) + } + return true +} + +func (c *VaultLdapCommand) extraVaultLdapHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault-ldap -credential-store-id [options] [args]", + "", + " Create a vault-ldap-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-ldap -credential-store-id csvlt_1234567890 -vault-path "/ldap/static-cred/einstein"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault-ldap [options] [args]", + "", + " Update a vault-ldap-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-ldap -id clvllt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index b6174b96c..b5c1b68a0 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -357,6 +357,22 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"update"}, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-ldap", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, }, "credentials": { { diff --git a/internal/credential/service_list_libraries_ext_test.go b/internal/credential/service_list_libraries_ext_test.go index 82dfb2f22..034dcd9df 100644 --- a/internal/credential/service_list_libraries_ext_test.go +++ b/internal/credential/service_list_libraries_ext_test.go @@ -48,8 +48,10 @@ func TestLibraryService_List(t *testing.T) { vault.TestCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), globals.UnspecifiedCredentialType, 1)[0], vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 1)[0], vault.TestCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), globals.UnspecifiedCredentialType, 1)[0], + vault.TestLdapCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 1)[0], vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 1)[0], vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 1)[0], + vault.TestLdapCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 1)[0], } // since we sort by create time descending, we need to reverse the slice @@ -66,7 +68,9 @@ func TestLibraryService_List(t *testing.T) { cmpopts.IgnoreUnexported( vault.CredentialLibrary{}, vault.SSHCertificateCredentialLibrary{}, + vault.LdapCredentialLibrary{}, store.SSHCertificateCredentialLibrary{}, + store.LdapCredentialLibrary{}, store.CredentialLibrary{}, timestamp.Timestamp{}, timestamppb.Timestamp{}, @@ -385,7 +389,7 @@ func TestLibraryService_List(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 5) + require.Equal(t, resp.EstimatedItemCount, 7) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], libs[0], cmpOpts...)) @@ -394,7 +398,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 5) + require.Equal(t, resp2.EstimatedItemCount, 7) require.Empty(t, resp2.DeletedIds) require.Len(t, resp2.Items, 1) require.Empty(t, cmp.Diff(resp2.Items[0], libs[1], cmpOpts...)) @@ -403,7 +407,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 5) + require.Equal(t, resp3.EstimatedItemCount, 7) require.Empty(t, resp3.DeletedIds) require.Len(t, resp3.Items, 1) require.Empty(t, cmp.Diff(resp3.Items[0], libs[2], cmpOpts...)) @@ -412,7 +416,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp4.CompleteListing) - require.Equal(t, resp4.EstimatedItemCount, 5) + require.Equal(t, resp4.EstimatedItemCount, 7) require.Empty(t, resp4.DeletedIds) require.Len(t, resp4.Items, 1) require.Empty(t, cmp.Diff(resp4.Items[0], libs[3], cmpOpts...)) @@ -420,21 +424,39 @@ func TestLibraryService_List(t *testing.T) { resp5, err := credential.ListLibrariesPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, credStore.GetPublicId()) require.NoError(t, err) require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp5.CompleteListing) - require.Equal(t, resp5.EstimatedItemCount, 5) + require.False(t, resp5.CompleteListing) + require.Equal(t, resp5.EstimatedItemCount, 7) require.Empty(t, resp5.DeletedIds) require.Len(t, resp5.Items, 1) require.Empty(t, cmp.Diff(resp5.Items[0], libs[4], cmpOpts...)) - // Finished initial pagination phase, request refresh - // Expect no results. - resp6, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, credStore.GetPublicId()) + resp6, err := credential.ListLibrariesPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, credStore.GetPublicId()) require.NoError(t, err) require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp6.CompleteListing) - require.Equal(t, resp6.EstimatedItemCount, 5) + require.False(t, resp6.CompleteListing) + require.Equal(t, resp6.EstimatedItemCount, 7) require.Empty(t, resp6.DeletedIds) - require.Empty(t, resp6.Items) + require.Len(t, resp6.Items, 1) + require.Empty(t, cmp.Diff(resp6.Items[0], libs[5], cmpOpts...)) + + resp7, err := credential.ListLibrariesPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, credStore.GetPublicId()) + require.NoError(t, err) + require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp7.CompleteListing) + require.Equal(t, resp7.EstimatedItemCount, 7) + require.Empty(t, resp7.DeletedIds) + require.Len(t, resp7.Items, 1) + require.Empty(t, cmp.Diff(resp7.Items[0], libs[6], cmpOpts...)) + + // Finished initial pagination phase, request refresh + // Expect no results. + resp8, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, credStore.GetPublicId()) + require.NoError(t, err) + require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp8.CompleteListing) + require.Equal(t, resp8.EstimatedItemCount, 7) + require.Empty(t, resp8.DeletedIds) + require.Empty(t, resp8.Items) // Create some new libraries newLib1 := vault.TestCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), globals.UnspecifiedCredentialType, 1)[0] @@ -453,33 +475,33 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) // Refresh again, should get newLib2 - resp7, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, credStore.GetPublicId()) - require.NoError(t, err) - require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash")) - require.False(t, resp7.CompleteListing) - require.Equal(t, resp7.EstimatedItemCount, 7) - require.Empty(t, resp7.DeletedIds) - require.Len(t, resp7.Items, 1) - require.Empty(t, cmp.Diff(resp7.Items[0], newLib2, cmpOpts...)) - - // Refresh again, should get newLib1 - resp8, err := credential.ListLibrariesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, credStore.GetPublicId()) - require.NoError(t, err) - require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp8.CompleteListing) - require.Equal(t, resp8.EstimatedItemCount, 7) - require.Empty(t, resp8.DeletedIds) - require.Len(t, resp8.Items, 1) - require.Empty(t, cmp.Diff(resp8.Items[0], newLib1, cmpOpts...)) - - // Refresh again, should get no results - resp9, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, credStore.GetPublicId()) + resp9, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, credStore.GetPublicId()) require.NoError(t, err) require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash")) - require.True(t, resp9.CompleteListing) - require.Equal(t, resp9.EstimatedItemCount, 7) + require.False(t, resp9.CompleteListing) + require.Equal(t, resp9.EstimatedItemCount, 9) require.Empty(t, resp9.DeletedIds) - require.Empty(t, resp9.Items) + require.Len(t, resp9.Items, 1) + require.Empty(t, cmp.Diff(resp9.Items[0], newLib2, cmpOpts...)) + + // Refresh again, should get newLib1 + resp10, err := credential.ListLibrariesRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, credStore.GetPublicId()) + require.NoError(t, err) + require.Equal(t, resp10.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp10.CompleteListing) + require.Equal(t, resp10.EstimatedItemCount, 9) + require.Empty(t, resp10.DeletedIds) + require.Len(t, resp10.Items, 1) + require.Empty(t, cmp.Diff(resp10.Items[0], newLib1, cmpOpts...)) + + // Refresh again, should get no results + resp11, err := credential.ListLibrariesRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, credStore.GetPublicId()) + require.NoError(t, err) + require.Equal(t, resp11.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp11.CompleteListing) + require.Equal(t, resp11.EstimatedItemCount, 9) + require.Empty(t, resp11.DeletedIds) + require.Empty(t, resp11.Items) }) t.Run("simple pagination with aggressive filtering", func(t *testing.T) { @@ -492,7 +514,7 @@ func TestLibraryService_List(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 5) + require.Equal(t, resp.EstimatedItemCount, 7) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], libs[1], cmpOpts...)) @@ -502,7 +524,7 @@ func TestLibraryService_List(t *testing.T) { require.NotNil(t, resp2.ListToken) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 5) + require.Equal(t, resp2.EstimatedItemCount, 7) require.Empty(t, resp2.DeletedIds) require.Len(t, resp2.Items, 1) require.Empty(t, cmp.Diff(resp2.Items[0], libs[len(libs)-1], cmpOpts...)) @@ -512,7 +534,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 5) + require.Equal(t, resp3.EstimatedItemCount, 7) require.Empty(t, resp3.DeletedIds) require.Empty(t, resp3.Items) @@ -545,7 +567,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp4.CompleteListing) - require.Equal(t, resp4.EstimatedItemCount, 9) + require.Equal(t, resp4.EstimatedItemCount, 11) require.Empty(t, resp4.DeletedIds) require.Len(t, resp4.Items, 1) require.Empty(t, cmp.Diff(resp4.Items[0], newLibs2[0], cmpOpts...)) @@ -555,7 +577,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp5.CompleteListing) - require.Equal(t, resp5.EstimatedItemCount, 9) + require.Equal(t, resp5.EstimatedItemCount, 11) require.Empty(t, resp5.DeletedIds) require.Len(t, resp5.Items, 1) require.Empty(t, cmp.Diff(resp5.Items[0], newLibs1[0], cmpOpts...)) @@ -566,7 +588,7 @@ func TestLibraryService_List(t *testing.T) { return true, nil } deletedCredentialLibraryId := libs[0].GetPublicId() - _, err := repo.DeleteSSHCertificateCredentialLibrary(ctx, prj.GetPublicId(), deletedCredentialLibraryId) + _, err := repo.DeleteLdapCredentialLibrary(ctx, prj.GetPublicId(), deletedCredentialLibraryId) require.NoError(t, err) libs = libs[1:] @@ -579,19 +601,19 @@ func TestLibraryService_List(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 4) + require.Equal(t, resp.EstimatedItemCount, 6) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], libs[0], cmpOpts...)) // request remaining results - resp2, err := credential.ListLibrariesPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, credStore.GetPublicId()) + resp2, err := credential.ListLibrariesPage(ctx, []byte("some hash"), 5, filterFunc, resp.ListToken, repo, credStore.GetPublicId()) require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 4) + require.Equal(t, resp2.EstimatedItemCount, 6) require.Empty(t, resp2.DeletedIds) - require.Len(t, resp2.Items, 3) + require.Len(t, resp2.Items, 5) require.Empty(t, cmp.Diff(resp2.Items, libs[1:], cmpOpts...)) deletedCredentialLibraryId = libs[0].GetPublicId() @@ -608,7 +630,7 @@ func TestLibraryService_List(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 3) + require.Equal(t, resp3.EstimatedItemCount, 5) require.Contains(t, resp3.DeletedIds, deletedCredentialLibraryId) require.Empty(t, resp3.Items) }) diff --git a/internal/credential/vault/credential_library.go b/internal/credential/vault/credential_library.go index 8345dd1de..0294f1ceb 100644 --- a/internal/credential/vault/credential_library.go +++ b/internal/credential/vault/credential_library.go @@ -98,7 +98,7 @@ func (l *CredentialLibrary) TableName() string { if l.tableName != "" { return l.tableName } - return "credential_vault_library" + return "credential_vault_generic_library" } // SetTableName sets the table name. @@ -206,6 +206,20 @@ func (l *listCredentialLibraryResult) toLibrary(ctx context.Context) (credential AdditionalValidPrincipals: l.AdditionalValidPrincipals, }, }, nil + case "ldap": + return &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + PublicId: l.PublicId, + StoreId: l.StoreId, + Name: l.Name, + Description: l.Description, + CreateTime: l.CreateTime, + UpdateTime: l.UpdateTime, + Version: uint32(l.Version), + VaultPath: l.VaultPath, + CredentialType: l.CredentialType, + }, + }, nil default: return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unexpected vault credential library type %s returned", l.Type)) } @@ -218,5 +232,5 @@ type deletedCredentialLibrary struct { // TableName returns the tablename to override the default gorm table name func (s *deletedCredentialLibrary) TableName() string { - return "credential_vault_library_deleted" + return "credential_vault_generic_library_deleted" } diff --git a/internal/credential/vault/docker.go b/internal/credential/vault/docker.go index fe8686291..7d7f9a108 100644 --- a/internal/credential/vault/docker.go +++ b/internal/credential/vault/docker.go @@ -6,8 +6,9 @@ package vault import "testing" var ( - newVaultServer func(t testing.TB, opt ...TestOption) *TestVaultServer = skipNewServer - mountDatabase func(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestDatabase = skipMountDatabase + newVaultServer func(t testing.TB, opt ...TestOption) *TestVaultServer = skipNewServer + mountLdapServer func(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestLdapServer = skipMountLdapServer + mountDatabase func(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestDatabase = skipMountDatabase ) func skipNewServer(t testing.TB, opt ...TestOption) *TestVaultServer { @@ -19,3 +20,8 @@ func skipMountDatabase(t testing.TB, v *TestVaultServer, opt ...TestOption) *Tes t.Skip("docker not available") return nil } + +func skipMountLdapServer(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestLdapServer { + t.Skip("docker not available") + return nil +} diff --git a/internal/credential/vault/ldap_credential_library.go b/internal/credential/vault/ldap_credential_library.go new file mode 100644 index 000000000..3e2251b3d --- /dev/null +++ b/internal/credential/vault/ldap_credential_library.go @@ -0,0 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package vault + +import ( + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/resource" + "google.golang.org/protobuf/proto" +) + +var _ credential.Library = (*LdapCredentialLibrary)(nil) + +// LdapCredentialLibrary is a credential library that issues ldap credentials +// using the vault ldap secret engine. This credential library always issues +// username/password/domain credentials. +type LdapCredentialLibrary struct { + *store.LdapCredentialLibrary + tableName string `gorm:"-"` +} + +// NewLdapCredentialLibrary creates a new in memory LdapCredentialLibrary for a +// Vault backend at vaultPath assigned to storeId. This credential library +// always issues username/password/domain credentials. WithName and +// WithDescription are the only valid options. All other options are ignored. +func NewLdapCredentialLibrary(storeId string, vaultPath string, opt ...Option) (*LdapCredentialLibrary, error) { + const op = "vault.NewLdapCredentialLibrary" + opts := getOpts(opt...) + + l := &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + VaultPath: vaultPath, + CredentialType: string(globals.UsernamePasswordDomainCredentialType), + }, + } + + return l, nil +} + +func allocLdapCredentialLibrary() *LdapCredentialLibrary { + return &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{}, + } +} + +func (l *LdapCredentialLibrary) clone() *LdapCredentialLibrary { + cp := proto.Clone(l.LdapCredentialLibrary) + return &LdapCredentialLibrary{ + LdapCredentialLibrary: cp.(*store.LdapCredentialLibrary), + } +} + +func (l *LdapCredentialLibrary) setId(i string) { + l.PublicId = i +} + +// TableName returns the table name. +func (l *LdapCredentialLibrary) TableName() string { + if l.tableName != "" { + return l.tableName + } + return "credential_vault_ldap_library" +} + +// SetTableName sets the table name. +func (l *LdapCredentialLibrary) SetTableName(n string) { + l.tableName = n +} + +// GetResourceType returns the resource type of the CredentialLibrary +func (l *LdapCredentialLibrary) GetResourceType() resource.Type { + return resource.CredentialLibrary +} + +func (l *LdapCredentialLibrary) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{l.PublicId}, + "resource-type": []string{"credential-vault-ldap-library"}, + "op-type": []string{op.String()}, + } + if l.StoreId != "" { + metadata["store-id"] = []string{l.StoreId} + } + return metadata +} + +// CredentialType returns the type of credential the library retrieves. +func (l *LdapCredentialLibrary) CredentialType() globals.CredentialType { + return globals.CredentialType(l.LdapCredentialLibrary.CredentialType) +} + +type deletedLdapCredentialLibrary struct { + PublicId string `gorm:"primary_key"` + DeleteTime *timestamp.Timestamp +} + +// TableName returns the tablename to override the default gorm table name +func (s *deletedLdapCredentialLibrary) TableName() string { + return "credential_vault_ldap_library_deleted" +} diff --git a/internal/credential/vault/mapping_overriders.go b/internal/credential/vault/mapping_overriders.go index 089754102..eef7559e9 100644 --- a/internal/credential/vault/mapping_overriders.go +++ b/internal/credential/vault/mapping_overriders.go @@ -96,7 +96,7 @@ func (o *UsernamePasswordOverride) TableName() string { if o.tableName != "" { return o.tableName } - return "credential_vault_library_username_password_mapping_override" + return "credential_vault_generic_library_username_password_mapping_ovrd" } // SetTableName sets the table name. @@ -163,7 +163,7 @@ func (o *UsernamePasswordDomainOverride) TableName() string { if o.tableName != "" { return o.tableName } - return "credential_vault_library_username_password_domain_mapping_ovrd" + return "credential_vault_generic_library_usern_pass_domain_mapping_ovrd" } // SetTableName sets the table name. @@ -230,7 +230,7 @@ func (o *SshPrivateKeyOverride) TableName() string { if o.tableName != "" { return o.tableName } - return "credential_vault_library_ssh_private_key_mapping_override" + return "credential_vault_generic_library_ssh_private_key_mapping_ovrd" } // SetTableName sets the table name. diff --git a/internal/credential/vault/private_library.go b/internal/credential/vault/private_library.go index 7e1288c7e..5a00b1a48 100644 --- a/internal/credential/vault/private_library.go +++ b/internal/credential/vault/private_library.go @@ -20,6 +20,7 @@ import ( "strings" "time" + ldapv3 "github.com/go-ldap/ldap/v3" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey" @@ -231,8 +232,10 @@ type issuingCredentialLibrary interface { retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error) } -// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and -// retrieve credentials. +// genericIssuingCredentialLibrary is a subtype of +// privateCredentialLibraryAllTypes specifically for Vault generic credential +// libraries. It contains all the values needed to connect to Vault and retrieve +// credentials. type genericIssuingCredentialLibrary struct { PublicId string StoreId string @@ -446,11 +449,6 @@ func (pl *genericIssuingCredentialLibrary) retrieveCredential(ctx context.Contex return convert(ctx, dCred) } -// TableName returns the table name for gorm. -func (pl *genericIssuingCredentialLibrary) TableName() string { - return "credential_vault_library_issue_credentials" -} - func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]issuingCredentialLibrary, error) { const op = "vault.(Repository).getIssueCredLibraries" @@ -514,8 +512,9 @@ func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []crede return decryptedLibs, nil } -// privateCredentialLibraryAllTypes is a clone of genericIssuingCredentialLibrary. Contains -// all the values needed to connect to Vault and retrieve credentials. +// privateCredentialLibraryAllTypes is a type that interfaces with the database. +// It contains all the values needed to connect to Vault and retrieve +// credentials. type privateCredentialLibraryAllTypes struct { PublicId string `gorm:"primary_key"` StoreId string @@ -686,6 +685,42 @@ func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() is Extensions: pl.Extensions, AdditionalValidPrincipals: pl.AdditionalValidPrincipals, } + case "ldap": + return &ldapIssuingCredentialLibrary{ + 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, + AdditionalValidPrincipals: pl.AdditionalValidPrincipals, + } default: return &genericIssuingCredentialLibrary{ PublicId: pl.PublicId, @@ -768,6 +803,10 @@ func (m *requestMap) get(libraryId string) []credential.Purpose { return m.ids[libraryId] } +// sshCertIssuingCredentialLibrary is a subtype of +// privateCredentialLibraryAllTypes specifically for Vault SSH Certificate +// credential libraries. It contains all the values needed to connect to Vault +// and retrieve credentials. type sshCertIssuingCredentialLibrary struct { PublicId string StoreId string @@ -1108,3 +1147,232 @@ func (lib *sshCertIssuingCredentialLibrary) retrieveCredential(ctx context.Conte certificate: []byte(cert), }, nil } + +type ldapIssuingCredentialLibrary 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 + AdditionalValidPrincipals string +} + +// GetPublicId returns the credential library's id. +func (lib *ldapIssuingCredentialLibrary) GetPublicId() string { return lib.PublicId } + +// GetStoreId returns the credential library's credential store id. +func (lib *ldapIssuingCredentialLibrary) GetStoreId() string { return lib.StoreId } + +// GetName returns the credential library's name. +func (lib *ldapIssuingCredentialLibrary) GetName() string { return lib.Name } + +// GetDescription returns the credential library's description. +func (lib *ldapIssuingCredentialLibrary) GetDescription() string { return lib.Description } + +// GetVersion returns the credential library's version. +func (lib *ldapIssuingCredentialLibrary) GetVersion() uint32 { return lib.Version } + +// GetPurpose returns the credential library's purpose. +func (lib *ldapIssuingCredentialLibrary) GetPurpose() credential.Purpose { return lib.Purpose } + +// GetCreateTime returns the credential library's create time. +func (lib *ldapIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return lib.CreateTime } + +// GetUpdateTime returns the credential library's update time. +func (lib *ldapIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return lib.UpdateTime } + +// CredentialType returns the credential library's issuing credential type. +func (lib *ldapIssuingCredentialLibrary) CredentialType() globals.CredentialType { + switch ct := lib.CredType; ct { + case "": + return globals.UnspecifiedCredentialType + default: + return globals.CredentialType(ct) + } +} + +// GetResourceType returns the credential library's resource type. +func (lib *ldapIssuingCredentialLibrary) GetResourceType() resource.Type { + return resource.CredentialLibrary +} + +// client produces a Vault API client using this credential library's data. +func (lib *ldapIssuingCredentialLibrary) 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.Wrap(ctx, err, op, errors.WithMsg("unable to create vault client")) + } + return client, nil +} + +var vaultLdapPathRegexp = regexp.MustCompile(`^.+\/(static-cred|creds).+$`) + +// retrieveCredential retrieves a dynamic LDAP credential from Vault for a +// specific session and connection. +func (lib *ldapIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, _ ...credential.Option) (dynamicCred, error) { + credId, err := newCredentialId(ctx) + 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) + } + var secret *vault.Secret + + secret, err = client.get(ctx, lib.VaultPath) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if secret == nil { + return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op)) + } + + leaseDuration := time.Duration(secret.LeaseDuration) * time.Second + cred, err := newCredential(ctx, lib.GetPublicId(), secret.LeaseID, lib.TokenHmac, leaseDuration) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + cred.PublicId = credId + cred.IsRenewable = secret.Renewable + + upd := &usrPassDomainCred{ + baseCred: &baseCred{ + Credential: cred, + lib: lib, + secretData: secret.Data, + }, + } + + username, ok := secret.Data["username"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a username or response was not in the expected format") + } + upd.username = username + + pwData, ok := secret.Data["password"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a password or response was not in the expected format") + } + upd.password = credential.Password(pwData) + + matches := vaultLdapPathRegexp.FindStringSubmatch(lib.VaultPath) + if len(matches) < 2 { // [0] is the vault path, [1] is static-cred or creds, if it exists. + return nil, errors.New(ctx, errors.InvalidParameter, op, "vault path was not in an expected format. expected path containing \"static-cred\" or \"creds\"") + } + switch matches[1] { + case "static-cred": + dn, ok := secret.Data["dn"].(string) + if !ok { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a dn or response was not in the expected format") + } + domain, err := extractDomainFromDn(dn) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + upd.domain = domain + case "creds": + var dns []string + dis, ok := secret.Data["distinguished_names"].([]any) + if ok { + for _, value := range dis { + if v, ok := value.(string); ok { + dns = append(dns, v) + } else { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret returned distinguished_names with non-string member") + } + } + } else { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain distinguished_names or response was not in the expected format") + } + if len(dns) == 0 { + return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret returned empty distinguished_names") + } + + // In the Vault LDAP secrets engine, no deduplication occurs and order + // is maintained from the LDIF statements, so the assumption here is + // that the first DN should correspond to the user's creation in the + // creation LDIF, which should correspond to the correct domain. + // https://developer.hashicorp.com/vault/docs/secrets/ldap#dynamic-credentials + domain, err := extractDomainFromDn(dns[0]) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + upd.domain = domain + } + + return upd, nil +} + +// extractDomainFromDn composes a domain name from all the "dc" attributes in an +// LDAP DN. +func extractDomainFromDn(dnStr string) (string, error) { + if dnStr == "" { + return "", fmt.Errorf("empty DN") + } + + dn, err := ldapv3.ParseDN(dnStr) + if err != nil { + return "", fmt.Errorf("failed to parse DN: %w", err) + } + + var dcs []string + for _, rdn := range dn.RDNs { + for _, attr := range rdn.Attributes { + // "0.9.2342.19200300.100.1.25" is the OID for the "dc" attribute + // and it is a valid representation in a DN string. + // (https://docs.ldap.com/specs/rfc4519.txt) + if strings.EqualFold(attr.Type, "dc") || strings.EqualFold(attr.Type, "0.9.2342.19200300.100.1.25") { + dcs = append(dcs, attr.Value) + } + } + } + if len(dcs) == 0 { + return "", fmt.Errorf("no domain component (dc) attribute found in dn") + } + + return strings.Join(dcs, "."), nil +} diff --git a/internal/credential/vault/private_library_test.go b/internal/credential/vault/private_library_test.go index b151df753..a57d4e2ab 100644 --- a/internal/credential/vault/private_library_test.go +++ b/internal/credential/vault/private_library_test.go @@ -1879,6 +1879,136 @@ func TestBaseToSshPriKey(t *testing.T) { } } +func TestRepository_ldapCertIssuingCredentialLibrary_retrieveCredential(t *testing.T) { + // Create Vault server + v := NewTestVaultServer(t, WithTestVaultTLS(TestNoTLS), WithVaultVersion("1.20.0"), WithDockerNetwork(true)) + require.NotNil(t, v) + + vc := v.client(t).cl + mounts, err := vc.Sys().ListMounts() + require.NoError(t, err) + require.NotEmpty(t, mounts) + + // Create OpenLDAP server and mount Vault LDAP secrets engine. + _ = MountLdapServer(t, v) + + // Create and setup Boundary DB + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + sche := scheduler.TestScheduler(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + repo, err := NewRepository(t.Context(), rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + sec, token := v.CreateToken(t, WithPolicies([]string{"default", "boundary-controller", "ldap"}), WithTokenPeriod(time.Hour)) + require.NotNil(t, sec) + require.NotEmpty(t, token) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId(), v.Addr, token, sec.Auth.Accessor) + + tests := []struct { + name string + vaultPath string + expStaticUsername string + expDomain string + expDynamicCred bool + expErr bool + expErrMsg string + }{ + { + name: "doesntExistStaticCred", + vaultPath: "ldap/static-cred/doesntexist", + expErr: true, + expErrMsg: "unknown role:", + }, + { + name: "doesntExistDynamicCred", + vaultPath: "ldap/creds/doesntexist", + expErr: true, + expErrMsg: "vault secret is empty", + }, + { + name: "staticCred", + vaultPath: "ldap/static-cred/einstein", + expStaticUsername: "einstein", + expDomain: "example.org", + }, + { + name: "staticCredHierarchical", + vaultPath: "ldap/static-cred/myorg/myproject/newton", + expStaticUsername: "newton", + expDomain: "example.org", + }, + { + name: "dynamicCred", + vaultPath: "ldap/creds/scientists", + expDynamicCred: true, + expDomain: "example.org", + }, + { + name: "dynamicCredHierarchical", + vaultPath: "ldap/creds/myorg/myproject/scientists", + expDynamicCred: true, + expDomain: "example.org", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lib, err := NewLdapCredentialLibrary(cs.GetPublicId(), tt.vaultPath) + require.NoError(t, err) + require.NotNil(t, lib) + lib.PublicId, err = newLdapCredentialLibraryId(t.Context()) + require.NoError(t, err) + + _, err = rw.DoTx(t.Context(), db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + return iw.Create(t.Context(), lib) + }, + ) + require.NoError(t, err) + + libs, err := repo.getIssueCredLibraries(t.Context(), []credential.Request{{ + SourceId: lib.GetPublicId(), + }}) + require.NoError(t, err) + require.Len(t, libs, 1) + + cred, err := libs[0].retrieveCredential(t.Context(), "TestLdapRetrieveCredential") + if tt.expErr { + require.ErrorContains(t, err, tt.expErrMsg) + return + } + require.NoError(t, err) + + require.IsType(t, &usrPassDomainCred{}, cred) + credUpd := cred.(*usrPassDomainCred) + require.NotEmpty(t, credUpd.baseCred) + require.NotEmpty(t, credUpd.getCredential()) + require.EqualValues(t, libs[0], credUpd.lib) + require.NotEmpty(t, credUpd.secretData) + require.Contains(t, credUpd.secretData, "username") + require.Contains(t, credUpd.secretData, "password") + + require.EqualValues(t, credUpd.secretData["password"], credUpd.Password()) + require.EqualValues(t, tt.expDomain, credUpd.Domain()) + if !tt.expDynamicCred { + require.EqualValues(t, credUpd.secretData["username"], credUpd.Username()) + require.EqualValues(t, tt.expStaticUsername, credUpd.Username()) + require.False(t, credUpd.GetIsRenewable()) + } else { + require.EqualValues(t, credUpd.secretData["username"], credUpd.Username()) + require.Contains(t, credUpd.Username(), "b_token-TestRepository-ldapCertIssuingCredentialLibrary-retrieveCredential_") + require.True(t, credUpd.GetIsRenewable()) + } + }) + } +} + func TestRepository_sshCertIssuingCredentialLibrary_retrieveCredential(t *testing.T) { t.Parallel() @@ -2093,3 +2223,83 @@ func TestRepository_sshCertIssuingCredentialLibrary_retrieveCredential(t *testin }) } } + +func TestExtractDomainFromDn(t *testing.T) { + tests := []struct { + name string + inDn string + expDomain string + expErr bool + expErrMsgContains string + }{ + { + name: "emptyString", + inDn: "", + expErr: true, + expErrMsgContains: "empty DN", + }, + { + name: "notADn", + inDn: "a string that isn't really a DN", + expErr: true, + expErrMsgContains: "failed to parse DN:", + }, + { + name: "validDn", + inDn: "dc=domain,dc=tld", + expDomain: "domain.tld", + }, + { + name: "validDnSpaces", + inDn: "dc = domain , dc = tld", + expDomain: "domain.tld", + }, + { + name: "validDnCaps", + inDn: "DC=DOMAIN,DC=TLD", + expDomain: "DOMAIN.TLD", + }, + { + name: "validDnOid", + inDn: "0.9.2342.19200300.100.1.25=Domain,0.9.2342.19200300.100.1.25=Tld", + expDomain: "Domain.Tld", + }, + { + name: "validDnOidMixed", + inDn: "dc=sub1,0.9.2342.19200300.100.1.25=Domain,dc=Tld", + expDomain: "sub1.Domain.Tld", + }, + { + name: "validDnWithCn", + inDn: "CN=My User,CN=admin,DC=mycorp,DC=domain,DC=TLD", + expDomain: "mycorp.domain.TLD", + }, + { + name: "validDnWithEscapeChars", + inDn: `CN=mycn,OU=HashiCorp\, an IBM Company,DC=domain,DC=TLD`, + expDomain: "domain.TLD", + }, + { + name: "validDnWithEscapeChars2", + inDn: `CN=Before\0DAfter,OU=HashiCorp,DC=domain,DC=TLD`, + expDomain: "domain.TLD", + }, + { + name: "validDnWithSubdomains", + inDn: `dc=foo,dc=bAR,dc=BAZ,dc=domain,dc=Tld`, + expDomain: "foo.bAR.BAZ.domain.Tld", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + domain, err := extractDomainFromDn(tt.inDn) + if tt.expErr { + require.ErrorContains(t, err, tt.expErrMsgContains) + require.Empty(t, domain) + return + } + require.NoError(t, err) + require.Equal(t, tt.expDomain, domain) + }) + } +} diff --git a/internal/credential/vault/public_ids.go b/internal/credential/vault/public_ids.go index cdea3f7ae..1a4b7f12d 100644 --- a/internal/credential/vault/public_ids.go +++ b/internal/credential/vault/public_ids.go @@ -18,6 +18,7 @@ func init() { globals.RegisterPrefixToResourceInfo(globals.VaultDynamicCredentialPrefix, resource.Credential, credential.Domain, Subtype) globals.RegisterPrefixToResourceInfo(globals.VaultCredentialLibraryPrefix, resource.CredentialLibrary, credential.Domain, GenericLibrarySubtype) globals.RegisterPrefixToResourceInfo(globals.VaultSshCertificateCredentialLibraryPrefix, resource.CredentialLibrary, credential.Domain, SSHCertificateLibrarySubtype) + globals.RegisterPrefixToResourceInfo(globals.VaultLdapCredentialLibraryPrefix, resource.CredentialLibrary, credential.Domain, LdapCredentialLibrarySubtype) } // PublicId prefixes for the resources in the vault package. @@ -25,6 +26,7 @@ const ( Subtype = globals.Subtype("vault") GenericLibrarySubtype = globals.Subtype("vault-generic") SSHCertificateLibrarySubtype = globals.Subtype("vault-ssh-certificate") + LdapCredentialLibrarySubtype = globals.Subtype("vault-ldap") ) func newCredentialStoreId(ctx context.Context) (string, error) { @@ -58,3 +60,11 @@ func newSSHCertificateCredentialLibraryId(ctx context.Context) (string, error) { } return id, nil } + +func newLdapCredentialLibraryId(ctx context.Context) (string, error) { + id, err := db.NewPublicId(ctx, globals.VaultLdapCredentialLibraryPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, "vault.newLdapCredentialLibraryPrefix") + } + return id, nil +} diff --git a/internal/credential/vault/query.go b/internal/credential/vault/query.go index b65a1c40c..9fd09eef4 100644 --- a/internal/credential/vault/query.go +++ b/internal/credential/vault/query.go @@ -194,8 +194,9 @@ delete from credential_vault_credential select sum(reltuples::bigint) as estimate from pg_class where oid in ( - 'credential_vault_library'::regclass, - 'credential_vault_ssh_cert_library'::regclass + 'credential_vault_generic_library'::regclass, + 'credential_vault_ssh_cert_library'::regclass, + 'credential_vault_ldap_library'::regclass ) ` @@ -209,7 +210,7 @@ with libraries as ( ), generic_libs as ( select * - from credential_vault_library + from credential_vault_generic_library where public_id in (select public_id from libraries) ), ssh_cert_libs as ( @@ -217,6 +218,11 @@ ssh_cert_libs as ( from credential_vault_ssh_cert_library where public_id in (select public_id from libraries) ), +ldap_libs as ( + select * + from credential_vault_ldap_library + where public_id in (select public_id from libraries) +), final as ( select public_id, store_id, @@ -263,6 +269,29 @@ final as ( additional_valid_principals, 'ssh' as type from ssh_cert_libs + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + vault_path, + credential_type, + null as http_method, -- Add to make union uniform + null as http_request_body, -- Add to make union uniform + null as username, -- Add to make union uniform + null as key_type, -- Add to make union uniform + null as key_bits, -- Add to make union uniform + null as ttl, -- Add to make union uniform + null as key_id, -- Add to make union uniform + null as critical_options, -- Add to make union uniform + null as extensions, -- Add to make union uniform + null as additional_valid_principals, -- Add to make union uniform + 'ldap' as type + from ldap_libs ) select * from final @@ -280,7 +309,7 @@ with libraries as ( ), generic_libs as ( select * - from credential_vault_library + from credential_vault_generic_library where public_id in (select public_id from libraries) ), ssh_cert_libs as ( @@ -288,6 +317,11 @@ ssh_cert_libs as ( from credential_vault_ssh_cert_library where public_id in (select public_id from libraries) ), +ldap_libs as ( + select * + from credential_vault_ldap_library + where public_id in (select public_id from libraries) +), final as ( select public_id, store_id, @@ -334,6 +368,29 @@ final as ( additional_valid_principals, 'ssh' as type from ssh_cert_libs + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + vault_path, + credential_type, + null as http_method, -- Add to make union uniform + null as http_request_body, -- Add to make union uniform + null as username, -- Add to make union uniform + null as key_type, -- Add to make union uniform + null as key_bits, -- Add to make union uniform + null as ttl, -- Add to make union uniform + null as key_id, -- Add to make union uniform + null as critical_options, -- Add to make union uniform + null as extensions, -- Add to make union uniform + null as additional_valid_principals, -- Add to make union uniform + 'ldap' as type + from ldap_libs ) select * from final @@ -351,7 +408,7 @@ with libraries as ( ), generic_libs as ( select * - from credential_vault_library + from credential_vault_generic_library where public_id in (select public_id from libraries) ), ssh_cert_libs as ( @@ -359,6 +416,11 @@ ssh_cert_libs as ( from credential_vault_ssh_cert_library where public_id in (select public_id from libraries) ), +ldap_libs as ( + select * + from credential_vault_ldap_library + where public_id in (select public_id from libraries) +), final as ( select public_id, store_id, @@ -405,6 +467,29 @@ final as ( additional_valid_principals, 'ssh' as type from ssh_cert_libs + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + vault_path, + credential_type, + null as http_method, -- Add to make union uniform + null as http_request_body, -- Add to make union uniform + null as username, -- Add to make union uniform + null as key_type, -- Add to make union uniform + null as key_bits, -- Add to make union uniform + null as ttl, -- Add to make union uniform + null as key_id, -- Add to make union uniform + null as critical_options, -- Add to make union uniform + null as extensions, -- Add to make union uniform + null as additional_valid_principals, -- Add to make union uniform + 'ldap' as type + from ldap_libs ) select * from final @@ -423,7 +508,7 @@ with libraries as ( ), generic_libs as ( select * - from credential_vault_library + from credential_vault_generic_library where public_id in (select public_id from libraries) ), ssh_cert_libs as ( @@ -431,6 +516,11 @@ ssh_cert_libs as ( from credential_vault_ssh_cert_library where public_id in (select public_id from libraries) ), +ldap_libs as ( + select * + from credential_vault_ldap_library + where public_id in (select public_id from libraries) +), final as ( select public_id, store_id, @@ -477,6 +567,29 @@ final as ( additional_valid_principals, 'ssh' as type from ssh_cert_libs + union + select public_id, + store_id, + project_id, + name, + description, + create_time, + update_time, + version, + vault_path, + credential_type, + null as http_method, -- Add to make union uniform + null as http_request_body, -- Add to make union uniform + null as username, -- Add to make union uniform + null as key_type, -- Add to make union uniform + null as key_bits, -- Add to make union uniform + null as ttl, -- Add to make union uniform + null as key_id, -- Add to make union uniform + null as critical_options, -- Add to make union uniform + null as extensions, -- Add to make union uniform + null as additional_valid_principals, -- Add to make union uniform + 'ldap' as type + from ldap_libs ) select * from final diff --git a/internal/credential/vault/repository_credential_library.go b/internal/credential/vault/repository_credential_library.go index 9227c5b13..707d64ccf 100644 --- a/internal/credential/vault/repository_credential_library.go +++ b/internal/credential/vault/repository_credential_library.go @@ -421,7 +421,7 @@ func (pl *listLookupLibrary) toCredentialLibrary() *CredentialLibrary { } // TableName returns the table name for gorm. -func (*listLookupLibrary) TableName() string { return "credential_vault_library_list_lookup" } +func (*listLookupLibrary) TableName() string { return "credential_vault_generic_library_list_lookup" } // GetPublicId returns the public id. func (pl *listLookupLibrary) GetPublicId() string { return pl.PublicId } diff --git a/internal/credential/vault/repository_ldap_credential_library.go b/internal/credential/vault/repository_ldap_credential_library.go new file mode 100644 index 000000000..e3dd80622 --- /dev/null +++ b/internal/credential/vault/repository_ldap_credential_library.go @@ -0,0 +1,224 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package vault + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/go-dbw" +) + +// CreateLdapCredentialLibrary creates a new Vault LDAP credential library. It +// inserts the input domain object into the repository and returns the inserted +// domain object. No options are supported. +// +// The input domain object is not mutated, must contain a valid credential store +// id and must not contain a public id. +func (r *Repository) CreateLdapCredentialLibrary(ctx context.Context, projectId string, l *LdapCredentialLibrary, _ ...Option) (*LdapCredentialLibrary, error) { + const op = "vault.(Repository).CreateLdapCredentialLibrary" + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + if l == nil || l.LdapCredentialLibrary == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil input domain object") + } + if l.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + 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") + } + l = l.clone() + + if l.GetCredentialType() == "" { + l.LdapCredentialLibrary.CredentialType = string(globals.UsernamePasswordDomainCredentialType) + } + if l.GetCredentialType() != string(globals.UsernamePasswordDomainCredentialType) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid credential type") + } + + id, err := newLdapCredentialLibraryId(ctx) + 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 insertedLdapCredLib *LdapCredentialLibrary + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + insertedLdapCredLib = l.clone() + err = w.Create(ctx, insertedLdapCredLib, + db.WithOplog(oplogWrapper, insertedLdapCredLib.oplog(oplog.OpType_OP_TYPE_CREATE)), + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("in credential store: %s: name %s already exists", l.StoreId, l.Name)) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("credential store: %s", l.StoreId)) + } + + return insertedLdapCredLib, nil +} + +// UpdateLdapCredentialLibrary updates an existing Vault LDAP credential library +// in the repository. It returns a domain object containing the updated state +// and the number of updated records. No options are supported. +// +// The input domain object is not mutated and must contain a valid public id. +func (r *Repository) UpdateLdapCredentialLibrary(ctx context.Context, projectId string, l *LdapCredentialLibrary, version uint32, fieldMaskPaths []string, _ ...Option) (*LdapCredentialLibrary, int, error) { + const op = "vault.(Repository).UpdateLdapCredentialLibrary" + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if l == nil || l.LdapCredentialLibrary == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "nil domain object") + } + if l.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(vaultPathField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, fmt.Sprintf("%q field mask path is unsupported", f)) + } + } + + dbMask, nullFields := dbw.BuildUpdatePaths( + map[string]any{ + nameField: l.Name, + descriptionField: l.Description, + vaultPathField: l.VaultPath, + }, fieldMaskPaths, nil, + ) + 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.WithMsg("unable to get oplog wrapper")) + } + + var updatedCredLib *LdapCredentialLibrary + var rowsUpdated int + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(r db.Reader, w db.Writer) error { + updated, err := w.Update(ctx, l.clone(), dbMask, nullFields, + db.WithOplog(oplogWrapper, l.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version), + ) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update credential library")) + } + if updated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 credential library would have been updated") + } + rowsUpdated = updated + + updatedCredLib = allocLdapCredentialLibrary() + updatedCredLib.PublicId = l.GetPublicId() + err = r.LookupByPublicId(ctx, updatedCredLib) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve updated credential library")) + } + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("name %s already exists: %s", l.Name, l.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to update credential library %s", l.PublicId)) + } + + return updatedCredLib, rowsUpdated, nil +} + +// LookupLdapCredentialLibrary looks up an existing Vault LDAP credential +// library in the repository. It returns the domain object for the given public +// id, if it exists. Returns nil, nil if not found. No options are supported. +func (r *Repository) LookupLdapCredentialLibrary(ctx context.Context, publicId string, _ ...Option) (*LdapCredentialLibrary, error) { + const op = "vault.(Repository).LookupLdapCredentialLibrary" + if publicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + + l := allocLdapCredentialLibrary() + l.PublicId = publicId + err := r.reader.LookupByPublicId(ctx, l) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("lookup failed for id %s", publicId)) + } + + return l, nil +} + +// DeleteLdapCredentialLibrary deletes the Vault LDAP credential library with +// the given public id from the repository. Returns the number of records +// deleted. No options are supported. +func (r *Repository) DeleteLdapCredentialLibrary(ctx context.Context, projectId string, publicId string, _ ...Option) (int, error) { + const op = "vault.(Repository).DeleteLdapCredentialLibrary" + 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 := allocLdapCredentialLibrary() + 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) { + rowsDeleted, err = w.Delete(ctx, l, 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 credential library would have been deleted") + } + return err + }, + ) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("delete failed for id %s", l.PublicId)) + } + + return rowsDeleted, nil +} diff --git a/internal/credential/vault/repository_ldap_credential_library_test.go b/internal/credential/vault/repository_ldap_credential_library_test.go new file mode 100644 index 000000000..56b45168f --- /dev/null +++ b/internal/credential/vault/repository_ldap_credential_library_test.go @@ -0,0 +1,709 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package vault + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/credential/vault/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestCreateLdapCredentialLibrary(t *testing.T) { + t.Parallel() + + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] + + tests := []struct { + name string + inProjectId string + inDomainObj *LdapCredentialLibrary + expOut *LdapCredentialLibrary + expErr bool + expErrMsg string + }{ + { + name: "noProjectId", + inProjectId: "", + inDomainObj: nil, + expErr: true, + expErrMsg: "no project id", + }, + { + name: "nilDomainObject", + inProjectId: proj.GetPublicId(), + inDomainObj: nil, + expErr: true, + expErrMsg: "nil input domain object", + }, + { + name: "nilStoreObject", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{LdapCredentialLibrary: nil}, + expErr: true, + expErrMsg: "nil input domain object", + }, + { + name: "emptyObject", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{LdapCredentialLibrary: &store.LdapCredentialLibrary{}}, + expErr: true, + expErrMsg: "no store id", + }, + { + name: "withPublicId", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + PublicId: "cvllt_3o2iuhOpZ", + StoreId: cs.GetPublicId(), + Name: "withPublicId", + Description: "withPublicId Subtest", + VaultPath: "ldap/creds/dynamic1", + }, + }, + expErr: true, + expErrMsg: "public id not empty", + }, + { + name: "noStoreId", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: "", + Name: "noStoreId", + Description: "noStoreId Subtest", + VaultPath: "ldap/creds/dynamic1", + }, + }, + expErr: true, + expErrMsg: "no store id", + }, + { + name: "noVaultPath", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "noVaultPath", + Description: "noVaultPath Subtest", + VaultPath: "", + }, + }, + expErr: true, + expErrMsg: "no vault path", + }, + { + name: "invalidCredentialType", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidCredentialType", + Description: "invalidCredentialType Subtest", + VaultPath: "ldap/creds/dynamic1", + CredentialType: "cred_type_that_doesnt_exist", + }, + }, + expErr: true, + expErrMsg: "invalid credential type", + }, + { + name: "invalidVaultPath1", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidVaultPath1", + Description: "invalidVaultPath1 Subtest", + VaultPath: "ldap/bad/some/test/path", + }, + }, + expErr: true, + expErrMsg: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "invalidVaultPath2", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidVaultPath2", + Description: "invalidVaultPath2 Subtest", + VaultPath: "ldap/static-credbad/some/test/path", + }, + }, + expErr: true, + expErrMsg: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "invalidVaultPath3", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidVaultPath3", + Description: "invalidVaultPath3 Subtest", + VaultPath: "ldap/badstatic-cred/some/test/path", + }, + }, + expErr: true, + expErrMsg: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "invalidVaultPath4", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidVaultPath4", + Description: "invalidVaultPath4 Subtest", + VaultPath: "ldap/credsbad/some/test/path", + }, + }, + expErr: true, + expErrMsg: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "invalidVaultPath5", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "invalidVaultPath5", + Description: "invalidVaultPath5 Subtest", + VaultPath: "ldap/badcreds/some/test/path", + }, + }, + expErr: true, + expErrMsg: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "validStaticCred1", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "validStaticCred1", + Description: "validStaticCred1 Subtest", + VaultPath: "ldap/static-cred/some/test/path", + }, + }, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validStaticCred1", + Description: "validStaticCred1 Subtest", + VaultPath: "ldap/static-cred/some/test/path", + }, + }, + }, + { + name: "validStaticCred2", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "validStaticCred2", + Description: "validStaticCred2 Subtest", + VaultPath: "/ldap/static-cred/path", + }, + }, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validStaticCred2", + Description: "validStaticCred2 Subtest", + VaultPath: "/ldap/static-cred/path", + }, + }, + }, + { + name: "validDynamicCreds1", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "validDynamicCreds1", + Description: "validDynamicCreds1 Subtest", + VaultPath: "ldap/creds/some/test/path", + }, + }, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validDynamicCreds1", + Description: "validDynamicCreds1 Subtest", + VaultPath: "ldap/creds/some/test/path", + }, + }, + }, + { + name: "validDynamicCreds2", + inProjectId: proj.GetPublicId(), + inDomainObj: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "validDynamicCreds2", + Description: "validDynamicCreds2 Subtest", + VaultPath: "/ldap/creds/path", + }, + }, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validDynamicCreds2", + Description: "validDynamicCreds2 Subtest", + VaultPath: "/ldap/creds/path", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + repo, err := NewRepository(t.Context(), rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + ldapCredLib, err := repo.CreateLdapCredentialLibrary(t.Context(), tt.inProjectId, tt.inDomainObj) + if tt.expErr { + require.ErrorContains(t, err, tt.expErrMsg) + require.Nil(t, ldapCredLib) + return + } + require.NoError(t, err) + require.NotNil(t, ldapCredLib) + + require.Contains(t, ldapCredLib.GetPublicId(), globals.VaultLdapCredentialLibraryPrefix) + require.NotEmpty(t, ldapCredLib.GetCreateTime()) + require.NotEmpty(t, ldapCredLib.GetUpdateTime()) + require.EqualValues(t, cs.GetPublicId(), ldapCredLib.GetStoreId()) + require.EqualValues(t, 1, ldapCredLib.GetVersion()) + require.EqualValues(t, string(globals.UsernamePasswordDomainCredentialType), ldapCredLib.GetCredentialType()) + require.EqualValues(t, tt.expOut.GetName(), ldapCredLib.GetName()) + require.EqualValues(t, tt.expOut.GetDescription(), ldapCredLib.GetDescription()) + require.EqualValues(t, tt.expOut.GetVaultPath(), ldapCredLib.GetVaultPath()) + }) + } +} + +func TestUpdateLdapCredentialLibrary(t *testing.T) { + t.Parallel() + + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] + + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + repo, err := NewRepository(t.Context(), rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + t.Run("nilDomainObject", func(t *testing.T) { + l, updated, err := repo.UpdateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), nil, 1, []string{}) + require.ErrorContains(t, err, "nil domain object") + require.EqualValues(t, 0, updated) + require.Nil(t, l) + }) + + t.Run("nilStoreObject", func(t *testing.T) { + l, updated, err := repo.UpdateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), + &LdapCredentialLibrary{LdapCredentialLibrary: nil}, 1, []string{}) + require.ErrorContains(t, err, "nil domain object") + require.EqualValues(t, 0, updated) + require.Nil(t, l) + }) + + t.Run("emptyDomainObject", func(t *testing.T) { + l, updated, err := repo.UpdateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), + &LdapCredentialLibrary{LdapCredentialLibrary: &store.LdapCredentialLibrary{}}, + 1, []string{}, + ) + require.ErrorContains(t, err, "missing public id") + require.EqualValues(t, 0, updated) + require.Nil(t, l) + }) + + t.Run("credLibDoesntExist", func(t *testing.T) { + l, updated, err := repo.UpdateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), + &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{PublicId: "doesnt_exist"}, + }, 1, []string{"name", "description"}, + ) + require.ErrorContains(t, err, "record not found, search issue") + require.EqualValues(t, 0, updated) + require.Nil(t, l) + }) + + updateNameFn := func(name string) func(*LdapCredentialLibrary) *LdapCredentialLibrary { + return func(lcl *LdapCredentialLibrary) *LdapCredentialLibrary { + lcl.Name = name + return lcl + } + } + updateDescriptionFn := func(description string) func(*LdapCredentialLibrary) *LdapCredentialLibrary { + return func(lcl *LdapCredentialLibrary) *LdapCredentialLibrary { + lcl.Description = description + return lcl + } + } + updateVaultPathFn := func(vaultPath string) func(*LdapCredentialLibrary) *LdapCredentialLibrary { + return func(lcl *LdapCredentialLibrary) *LdapCredentialLibrary { + lcl.VaultPath = vaultPath + return lcl + } + } + updateStoreIdFn := func() func(*LdapCredentialLibrary) *LdapCredentialLibrary { + return func(lcl *LdapCredentialLibrary) *LdapCredentialLibrary { + lcl.StoreId = "this_is_immutable" + return lcl + } + } + + tests := []struct { + name string + inProjectId string + inVersion uint32 + inFieldMask []string + changeFns []func(*LdapCredentialLibrary) *LdapCredentialLibrary + expUpdate bool + expOut *LdapCredentialLibrary + expErr bool + expErrStr string + }{ + { + name: "noProjectId", + inProjectId: "", + inVersion: 1, + inFieldMask: []string{"somepath"}, + expErr: true, + expErrStr: "no project id", + }, + { + name: "noVersion", + inProjectId: proj.GetPublicId(), + inVersion: 0, + inFieldMask: []string{"somepath"}, + expErr: true, + expErrStr: "missing version", + }, + { + name: "missingFieldMask", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{}, + expErr: true, + expErrStr: "missing field mask", + }, + { + name: "unsupportedFieldInFieldMask", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"name", "storeId"}, + expErr: true, + expErrStr: `"storeId" field mask path is unsupported`, + }, + { + name: "updateToInvalidVaultPath1", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"vaultPath"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateVaultPathFn("ldap/credbad/new"), + }, + expErr: true, + expErrStr: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "updateToInvalidVaultPath2", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"vaultPath"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateVaultPathFn("ldap/static-credbad/new"), + }, + expErr: true, + expErrStr: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "updateAndFieldMaskMismatch", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"name"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateDescriptionFn("updateAndFieldMaskMismatch Updated Description"), + }, + expUpdate: false, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "TestUpdateLdapCredentialLibrary/updateAndFieldMaskMismatch Original Name", + Description: "TestUpdateLdapCredentialLibrary/updateAndFieldMaskMismatch Original Description", + VaultPath: "ldap/creds/path", + }, + }, + }, + { + name: "validUpdateName", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"name"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateNameFn("validUpdateName Subtest Updated Name"), + }, + expUpdate: true, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validUpdateName Subtest Updated Name", + Description: "TestUpdateLdapCredentialLibrary/validUpdateName Original Description", + VaultPath: "ldap/creds/path", + }, + }, + }, + { + name: "validUpdateDescription", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"description"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateDescriptionFn("validUpdateDescription Subtest Updated Description"), + }, + expUpdate: true, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "TestUpdateLdapCredentialLibrary/validUpdateDescription Original Name", + Description: "validUpdateDescription Subtest Updated Description", + VaultPath: "ldap/creds/path", + }, + }, + }, + { + name: "validUpdateVaultPath", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"vaultPath"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateVaultPathFn("ldap/creds/updated/path/new"), + }, + expUpdate: true, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "TestUpdateLdapCredentialLibrary/validUpdateVaultPath Original Name", + Description: "TestUpdateLdapCredentialLibrary/validUpdateVaultPath Original Description", + VaultPath: "ldap/creds/updated/path/new", + }, + }, + }, + { + name: "validUpdateMultipleFields", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"name", "description", "vaultPath"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateNameFn("validUpdateMultipleFields Updated Name"), + updateDescriptionFn("validUpdateMultipleFields Updated Description"), + updateVaultPathFn("ldap/static-cred/updated/path/new"), + }, + expUpdate: true, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validUpdateMultipleFields Updated Name", + Description: "validUpdateMultipleFields Updated Description", + VaultPath: "ldap/static-cred/updated/path/new", + }, + }, + }, + { + name: "validUpdateNameExtraChangesNotInFieldMask", + inProjectId: proj.GetPublicId(), + inVersion: 1, + inFieldMask: []string{"name"}, + changeFns: []func(*LdapCredentialLibrary) *LdapCredentialLibrary{ + updateNameFn("validUpdateNameExtraChanges Updated Name"), + updateDescriptionFn("validUpdateNameExtraChanges Updated Description"), + updateVaultPathFn("ldap/creds/updated/path/new"), + updateStoreIdFn(), + }, + expUpdate: true, + expOut: &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + Name: "validUpdateNameExtraChanges Updated Name", + Description: "TestUpdateLdapCredentialLibrary/validUpdateNameExtraChangesNotInFieldMask Original Description", + VaultPath: "ldap/creds/path", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + createdLib, err := repo.CreateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: t.Name() + " Original Name", + Description: t.Name() + " Original Description", + VaultPath: "ldap/creds/path", + }, + }) + require.NoError(t, err) + require.NotNil(t, createdLib) + + inDomainObj := createdLib.clone() + for _, changeFn := range tt.changeFns { + inDomainObj = changeFn(inDomainObj) + } + + updatedLdapLib, updated, err := repo.UpdateLdapCredentialLibrary(t.Context(), + tt.inProjectId, inDomainObj, tt.inVersion, tt.inFieldMask, + ) + if tt.expErr { + require.ErrorContains(t, err, tt.expErrStr) + require.Nil(t, updatedLdapLib) + require.EqualValues(t, 0, updated) + return + } + require.NoError(t, err) + require.NotNil(t, updatedLdapLib) + require.NotNil(t, updatedLdapLib.LdapCredentialLibrary) + + if tt.expUpdate { + require.Greater(t, updatedLdapLib.GetUpdateTime().AsTime(), createdLib.GetUpdateTime().AsTime()) + require.EqualValues(t, createdLib.GetVersion()+1, updatedLdapLib.GetVersion()) + } else { + require.EqualValues(t, createdLib.GetUpdateTime().AsTime(), updatedLdapLib.GetUpdateTime().AsTime()) + require.EqualValues(t, createdLib.GetVersion(), updatedLdapLib.GetVersion()) + } + require.EqualValues(t, 1, updated) + require.EqualValues(t, createdLib.GetPublicId(), updatedLdapLib.GetPublicId()) + require.EqualValues(t, createdLib.GetCreateTime().AsTime(), updatedLdapLib.GetCreateTime().AsTime()) + require.EqualValues(t, createdLib.GetStoreId(), updatedLdapLib.GetStoreId()) + require.EqualValues(t, createdLib.GetCredentialType(), updatedLdapLib.GetCredentialType()) + require.EqualValues(t, tt.expOut.GetName(), updatedLdapLib.GetName()) + require.EqualValues(t, tt.expOut.GetDescription(), updatedLdapLib.GetDescription()) + require.EqualValues(t, tt.expOut.GetVaultPath(), updatedLdapLib.GetVaultPath()) + }) + } +} + +func TestLookupLdapCredentialLibrary(t *testing.T) { + t.Parallel() + + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] + + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + repo, err := NewRepository(t.Context(), rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + t.Run("noPublicId", func(t *testing.T) { + l, err := repo.LookupLdapCredentialLibrary(t.Context(), "") + require.ErrorContains(t, err, "no public id") + require.Nil(t, l) + }) + + t.Run("credLibNotFound", func(t *testing.T) { + l, err := repo.LookupLdapCredentialLibrary(t.Context(), "alibthat_doesnt_exist") + require.NoError(t, err) + require.Nil(t, l) + }) + + t.Run("success", func(t *testing.T) { + createdLib, err := repo.CreateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "success", + Description: "success Subtest", + VaultPath: "ldap/creds/path", + }, + }) + require.NoError(t, err) + require.NotNil(t, createdLib) + + lookupLib, err := repo.LookupLdapCredentialLibrary(t.Context(), createdLib.GetPublicId()) + require.NoError(t, err) + require.Empty(t, cmp.Diff(createdLib, lookupLib, protocmp.Transform())) + }) +} + +func TestDeleteLdapCredentialLibrary(t *testing.T) { + t.Parallel() + + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] + + kms := kms.TestKms(t, conn, wrapper) + sche := scheduler.TestScheduler(t, conn, wrapper) + rw := db.New(conn) + + repo, err := NewRepository(t.Context(), rw, rw, kms, sche) + require.NoError(t, err) + require.NotNil(t, repo) + + t.Run("noPublicId", func(t *testing.T) { + deleted, err := repo.DeleteLdapCredentialLibrary(t.Context(), proj.GetPublicId(), "") + require.ErrorContains(t, err, "no public id") + require.EqualValues(t, 0, deleted) + }) + + t.Run("noProjectId", func(t *testing.T) { + deleted, err := repo.DeleteLdapCredentialLibrary(t.Context(), "", "cvllt_doesntexist") + require.ErrorContains(t, err, "no project id") + require.EqualValues(t, 0, deleted) + }) + + t.Run("credLibDoesntExist", func(t *testing.T) { + deleted, err := repo.DeleteLdapCredentialLibrary(t.Context(), proj.GetPublicId(), "cvllt_doesntexist") + require.NoError(t, err) + require.EqualValues(t, 0, deleted) + }) + + t.Run("success", func(t *testing.T) { + createdLib, err := repo.CreateLdapCredentialLibrary(t.Context(), proj.GetPublicId(), &LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ + StoreId: cs.GetPublicId(), + Name: "success", + Description: "success Subtest", + VaultPath: "ldap/creds/path", + }, + }) + require.NoError(t, err) + require.NotNil(t, createdLib) + + lookupLib, err := repo.LookupLdapCredentialLibrary(t.Context(), createdLib.GetPublicId()) + require.NoError(t, err) + require.Empty(t, cmp.Diff(createdLib, lookupLib, protocmp.Transform())) + + deleted, err := repo.DeleteLdapCredentialLibrary(t.Context(), proj.GetPublicId(), lookupLib.GetPublicId()) + require.NoError(t, err) + require.EqualValues(t, 1, deleted) + + lookupLib2, err := repo.LookupLdapCredentialLibrary(t.Context(), createdLib.GetPublicId()) + require.NoError(t, err) + require.Nil(t, lookupLib2) + }) +} diff --git a/internal/credential/vault/store/vault.pb.go b/internal/credential/vault/store/vault.pb.go index 7f22e504c..637fa4fa0 100644 --- a/internal/credential/vault/store/vault.pb.go +++ b/internal/credential/vault/store/vault.pb.go @@ -1269,6 +1269,134 @@ func (x *SshPrivateKeyOverride) GetPrivateKeyPassphraseAttribute() string { return "" } +type LdapCredentialLibrary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // 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"` + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + CredentialType string `protobuf:"bytes,9,opt,name=credential_type,json=credentialType,proto3" json:"credential_type,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LdapCredentialLibrary) Reset() { + *x = LdapCredentialLibrary{} + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LdapCredentialLibrary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LdapCredentialLibrary) ProtoMessage() {} + +func (x *LdapCredentialLibrary) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LdapCredentialLibrary.ProtoReflect.Descriptor instead. +func (*LdapCredentialLibrary) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_vault_store_v1_vault_proto_rawDescGZIP(), []int{9} +} + +func (x *LdapCredentialLibrary) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *LdapCredentialLibrary) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *LdapCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *LdapCredentialLibrary) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LdapCredentialLibrary) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *LdapCredentialLibrary) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *LdapCredentialLibrary) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *LdapCredentialLibrary) GetVaultPath() string { + if x != nil { + return x.VaultPath + } + return "" +} + +func (x *LdapCredentialLibrary) GetCredentialType() string { + if x != nil { + return x.CredentialType + } + return "" +} + var File_controller_storage_credential_vault_store_v1_vault_proto protoreflect.FileDescriptor const file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = "" + @@ -1424,7 +1552,23 @@ const file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = "" "library_id\x18\x01 \x01(\tR\tlibraryId\x12-\n" + "\x12username_attribute\x18\x02 \x01(\tR\x11usernameAttribute\x122\n" + "\x15private_key_attribute\x18\x03 \x01(\tR\x13privateKeyAttribute\x12G\n" + - " private_key_passphrase_attribute\x18\x04 \x01(\tR\x1dprivateKeyPassphraseAttributeBEZCgithub.com/hashicorp/boundary/internal/credential/vault/store;storeb\x06proto3" + " private_key_passphrase_attribute\x18\x04 \x01(\tR\x1dprivateKeyPassphraseAttribute\"\xd5\x03\n" + + "\x15LdapCredentialLibrary\x12\x1b\n" + + "\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12K\n" + + "\vcreate_time\x18\x02 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "createTime\x12K\n" + + "\vupdate_time\x18\x03 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" + + "updateTime\x12$\n" + + "\x04name\x18\x04 \x01(\tB\x10\xc2\xdd)\f\n" + + "\x04Name\x12\x04nameR\x04name\x12@\n" + + "\vdescription\x18\x05 \x01(\tB\x1e\xc2\xdd)\x1a\n" + + "\vDescription\x12\vdescriptionR\vdescription\x12\x19\n" + + "\bstore_id\x18\x06 \x01(\tR\astoreId\x12\x18\n" + + "\aversion\x18\a \x01(\rR\aversion\x12?\n" + + "\n" + + "vault_path\x18\b \x01(\tB \xc2\xdd)\x1c\n" + + "\tVaultPath\x12\x0fattributes.pathR\tvaultPath\x12'\n" + + "\x0fcredential_type\x18\t \x01(\tR\x0ecredentialTypeBEZCgithub.com/hashicorp/boundary/internal/credential/vault/store;storeb\x06proto3" var ( file_controller_storage_credential_vault_store_v1_vault_proto_rawDescOnce sync.Once @@ -1438,7 +1582,7 @@ 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, 9) +var file_controller_storage_credential_vault_store_v1_vault_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []any{ (*CredentialStore)(nil), // 0: controller.storage.credential.vault.store.v1.CredentialStore (*Token)(nil), // 1: controller.storage.credential.vault.store.v1.Token @@ -1449,29 +1593,32 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_goTypes = []an (*UsernamePasswordOverride)(nil), // 6: controller.storage.credential.vault.store.v1.UsernamePasswordOverride (*UsernamePasswordDomainOverride)(nil), // 7: controller.storage.credential.vault.store.v1.UsernamePasswordDomainOverride (*SshPrivateKeyOverride)(nil), // 8: controller.storage.credential.vault.store.v1.SshPrivateKeyOverride - (*timestamp.Timestamp)(nil), // 9: controller.storage.timestamp.v1.Timestamp + (*LdapCredentialLibrary)(nil), // 9: controller.storage.credential.vault.store.v1.LdapCredentialLibrary + (*timestamp.Timestamp)(nil), // 10: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_vault_store_v1_vault_proto_depIdxs = []int32{ - 9, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // 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 + 10, // 0: controller.storage.credential.vault.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 1: controller.storage.credential.vault.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 2: controller.storage.credential.vault.store.v1.CredentialStore.delete_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 3: controller.storage.credential.vault.store.v1.Token.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 4: controller.storage.credential.vault.store.v1.Token.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 5: controller.storage.credential.vault.store.v1.Token.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 6: controller.storage.credential.vault.store.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 7: controller.storage.credential.vault.store.v1.CredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 8: controller.storage.credential.vault.store.v1.CredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 9: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 10: controller.storage.credential.vault.store.v1.SSHCertificateCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 11: controller.storage.credential.vault.store.v1.Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 12: controller.storage.credential.vault.store.v1.Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 13: controller.storage.credential.vault.store.v1.Credential.last_renewal_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 14: controller.storage.credential.vault.store.v1.Credential.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 15: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 10, // 16: controller.storage.credential.vault.store.v1.LdapCredentialLibrary.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_controller_storage_credential_vault_store_v1_vault_proto_init() } @@ -1485,7 +1632,7 @@ func file_controller_storage_credential_vault_store_v1_vault_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc), len(file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/vault/supported.go b/internal/credential/vault/supported.go index b65ad6317..fcc8e8fd9 100644 --- a/internal/credential/vault/supported.go +++ b/internal/credential/vault/supported.go @@ -7,6 +7,8 @@ package vault import ( + "bytes" + "context" "crypto/tls" "fmt" "net/http" @@ -32,15 +34,18 @@ const DefaultVaultVersion = "1.15.1" var ( vaultRepository = "hashicorp/vault" postgresRepository = "postgres" + ldapRepository = "osixia/openldap" ) func init() { newVaultServer = gotNewServer + mountLdapServer = gotLdapServer mountDatabase = gotMountDatabase mirror := os.Getenv("DOCKER_MIRROR") if mirror != "" { vaultRepository = strings.Join([]string{mirror, vaultRepository}, "/") + ldapRepository = strings.Join([]string{mirror, ldapRepository}, "/") postgresRepository = strings.Join([]string{mirror, postgresRepository}, "/") } } @@ -217,20 +222,233 @@ func gotNewServer(t testing.TB, opt ...TestOption) *TestVaultServer { return server } +func gotLdapServer(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestLdapServer { + require.NotNil(t, v.pool) + require.NotNil(t, v.network, "Vault server must be created with docker network") + require.NotNil(t, v.vaultContainer, "vault container doesn't exist") + require.Nil(t, v.ldapContainer, "ldap container exists") + + pool, ok := v.pool.(*dockertest.Pool) + require.True(t, ok) + nw, ok := v.network.(*dockertest.Network) + require.True(t, ok) + + opts := getTestOpts(t, opt...) + if opts.ldapRootDomain == "" { + opts.ldapRootDomain = "example.org" + } + if opts.bindPass == "" { + opts.bindPass = "admin" + } + + dcl := []string{} + for d := range strings.SplitSeq(opts.ldapRootDomain, ".") { + dcl = append(dcl, "dc="+d) + } + dc := strings.Join(dcl, ",") + dn := fmt.Sprintf("cn=admin,%s", dc) + + server := &TestLdapServer{ + BindDomain: opts.ldapRootDomain, + BindPass: opts.bindPass, + BindDn: dn, + } + + userLdif := func(username, dc string) string { + template := ` + dn: cn=%[1]s,%[2]s + objectClass: inetOrgPerson + cn: %[1]s + sn: %[1]s + uid: %[1]s + userPassword: password + ` + template = fmt.Sprintf(template, username, dc) + // LDIF is whitespace sensitive, so remove the leading tab characters we + // have from having indented the template. + return strings.ReplaceAll(template, "\t", "") + } + userEinsteinLdif := userLdif("einstein", dc) + userNewtonLdif := userLdif("newton", dc) + + groupScientistsLdifTemplate := ` + dn: cn=scientists,%[1]s + objectClass: groupOfUniqueNames + cn: scientists + uniqueMember: cn=einstein,%[1]s + uniqueMember: cn=netwon,%[1]s + ` + groupLdif := fmt.Sprintf(groupScientistsLdifTemplate, dc) + // LDIF is whitespace sensitive, so remove the leading tab characters we + // have from having indented the template. + groupLdif = strings.ReplaceAll(groupLdif, "\t", "") + + dir, err := base62.Random(12) + require.NoError(t, err) + dir = path.Join("/tmp", dir) + outdir := path.Join(dir, "ldap") + require.NoError(t, os.Mkdir(dir, os.ModePerm)) + require.NoError(t, os.Mkdir(outdir, os.ModePerm)) + require.NoError(t, os.WriteFile(fmt.Sprintf("%s/%s", outdir, "userEinstein.ldif"), []byte(userEinsteinLdif), 0o666)) + require.NoError(t, os.WriteFile(fmt.Sprintf("%s/%s", outdir, "userNewton.ldif"), []byte(userNewtonLdif), 0o666)) + require.NoError(t, os.WriteFile(fmt.Sprintf("%s/%s", outdir, "group.ldif"), []byte(groupLdif), 0o666)) + + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: ldapRepository, + Tag: "latest", + Networks: []*dockertest.Network{nw}, + Mounts: []string{fmt.Sprintf("%s:/tmp/ldap", outdir)}, + Env: []string{ + fmt.Sprintf("LDAP_DOMAIN=%s", server.BindDomain), + fmt.Sprintf("LDAP_ADMIN_PASSWORD=%s", server.BindPass), + }, + }) + require.NoError(t, err) + t.Cleanup(func() { cleanupResource(t, pool, resource) }) + v.ldapContainer = resource + server.Url = fmt.Sprintf("ldap://%s", resource.GetIPInNetwork(nw)) + + // Wait until the container is healthy or timeout. + timeout := 10 * time.Second + ctx, cancel := context.WithTimeout(t.Context(), timeout) + t.Cleanup(cancel) + for loop := true; loop; { + select { + case <-ctx.Done(): + t.Fatalf("LDAP server was not healthy after trying for %s", timeout) + default: + buf := bytes.NewBuffer(nil) + exitCode, _ := resource.Exec([]string{ + "ldapwhoami", "-x", "-H", "ldap://localhost", "-D", server.BindDn, "-w", server.BindPass, + }, dockertest.ExecOptions{ + StdOut: buf, + }) + // ldapwhoami will output the caller's identity in the form of a DN. + // Because we're using the admin user here, we can just look for the + // BindDn in stdout. + if exitCode == 0 || strings.Contains(buf.String(), server.BindDn) { + loop = false + } + } + } + + buf := bytes.NewBuffer(nil) + ebuf := bytes.NewBuffer(nil) + exitCode, err := resource.Exec([]string{"ldapadd", "-x", "-H", "ldap://localhost", "-D", server.BindDn, "-w", server.BindPass, "-f", "/tmp/ldap/userEinstein.ldif"}, dockertest.ExecOptions{ + StdOut: buf, + StdErr: ebuf, + }) + require.NoError(t, err) + require.Empty(t, ebuf) + require.Equal(t, 0, exitCode) + + buf.Reset() + ebuf.Reset() + exitCode, err = resource.Exec([]string{"ldapadd", "-x", "-H", "ldap://localhost", "-D", server.BindDn, "-w", server.BindPass, "-f", "/tmp/ldap/userNewton.ldif"}, dockertest.ExecOptions{ + StdOut: buf, + StdErr: ebuf, + }) + require.NoError(t, err) + require.Empty(t, ebuf) + require.Equal(t, 0, exitCode) + + buf.Reset() + ebuf.Reset() + exitCode, err = resource.Exec([]string{"ldapadd", "-x", "-H", "ldap://localhost", "-D", server.BindDn, "-w", server.BindPass, "-f", "/tmp/ldap/group.ldif"}, dockertest.ExecOptions{ + StdOut: buf, + StdErr: ebuf, + }) + require.NoError(t, err) + require.Empty(t, ebuf) + require.Equal(t, 0, exitCode) + + // Mount and configure Vault LDAP secrets engine. + v.MountLdap(t, WithBindDn(server.BindDn), WithBindPass(server.BindPass), WithLdapUrl(server.Url)) + + vc := v.client(t).cl + mounts, err := vc.Sys().ListMounts() + require.NoError(t, err) + require.Contains(t, mounts, "ldap/") + + // Add static roles that correspond to the LDAP server users. + _, err = vc.Logical().Write("ldap/static-role/einstein", map[string]any{ + "dn": "cn=einstein,dc=example,dc=org", + "username": "einstein", + "rotation_period": "24h", + }) + require.NoError(t, err) + + _, err = vc.Logical().Write("ldap/static-role/myorg/myproject/newton", map[string]any{ + "dn": "cn=newton,dc=example,dc=org", + "username": "newton", + "rotation_period": "24h", + }) + require.NoError(t, err) + + // Setup LDAP dynamic credentials. + ldifCreateTemplate := ` + dn: cn={{.Username}},dc=example,dc=org + changetype: add + objectClass: inetOrgPerson + cn: {{.Username}} + sn: {{.Username}} + uid: {{.Username}} + userPassword:{{.Password}} + + dn: cn=scientists,dc=example,dc=org + changetype: modify + add: uniqueMember + uniqueMember: cn={{.Username}},dc=example,dc=org + -` + ldifCreateTemplate = strings.ReplaceAll(ldifCreateTemplate, "\t", "") + + ldifDeleteTemplate := ` + dn: cn={{.Username}},dc=example,dc=org + changetype: delete + ` + ldifDeleteTemplate = strings.ReplaceAll(ldifDeleteTemplate, "\t", "") + + _, err = vc.Logical().Write("ldap/role/scientists", map[string]any{ + "username_template": "b_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}", + "creation_ldif": ldifCreateTemplate, + "deletion_ldif": ldifDeleteTemplate, + "rollback_ldif": ldifDeleteTemplate, + "default_ttl": "1h", + "max_ttl": "24h", + }) + require.NoError(t, err) + + _, err = vc.Logical().Write("ldap/role/myorg/myproject/scientists", map[string]any{ + "username_template": "b_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}", + "creation_ldif": ldifCreateTemplate, + "deletion_ldif": ldifDeleteTemplate, + "rollback_ldif": ldifDeleteTemplate, + "default_ttl": "1h", + "max_ttl": "24h", + }) + require.NoError(t, err) + + return server +} + func gotMountDatabase(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestDatabase { require := require.New(t) - require.Nil(v.postgresContainer, "postgres container exists") + require.NotNil(v.pool) require.NotNil(v.network, "Vault server must be created with docker network") + require.NotNil(v.vaultContainer, "vault container doesn't exist") + require.Nil(v.postgresContainer, "postgres container exists") pool, ok := v.pool.(*dockertest.Pool) require.True(ok) - network, ok := v.network.(*dockertest.Network) + nw, ok := v.network.(*dockertest.Network) + require.True(ok) + vaultContainer, ok := v.vaultContainer.(*dockertest.Resource) require.True(ok) dockerOptions := &dockertest.RunOptions{ Repository: postgresRepository, Tag: globals.MinimumSupportedPostgresVersion, - Networks: []*dockertest.Network{network}, + Networks: []*dockertest.Network{nw}, Env: []string{"POSTGRES_PASSWORD=password", "POSTGRES_DB=boundarytest"}, } @@ -316,13 +534,10 @@ func gotMountDatabase(t testing.TB, v *TestVaultServer, opt ...TestOption) *Test } v.addPolicy(t, "database", pc) - vaultContainer, ok := v.vaultContainer.(*dockertest.Resource) - require.True(ok) - - t.Log(vaultContainer.GetIPInNetwork(network)) + t.Log(vaultContainer.GetIPInNetwork(nw)) // Configure PostgreSQL secrets engine - connUrl := fmt.Sprintf("postgresql://{{username}}:{{password}}@%s:5432/boundarytest?sslmode=disable", resource.GetIPInNetwork(network)) + connUrl := fmt.Sprintf("postgresql://{{username}}:{{password}}@%s:5432/boundarytest?sslmode=disable", resource.GetIPInNetwork(nw)) t.Log(connUrl) postgresConfPath := path.Join(mountPath, "config/postgresql") diff --git a/internal/credential/vault/testing.go b/internal/credential/vault/testing.go index cb27228a8..e4bff5128 100644 --- a/internal/credential/vault/testing.go +++ b/internal/credential/vault/testing.go @@ -180,6 +180,44 @@ func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping return libs } +// TestLdapCredentialLibraries creates `count` number of Vault LDAP credential +// libraries in the provided db with the provided credential store. If any +// errors are encountered, the test will fail. +func TestLdapCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*LdapCredentialLibrary { + t.Helper() + w := db.New(conn) + + var libs []*LdapCredentialLibrary + for i := range count { + var staticOrDynamic string + switch mrand.Intn(2) { + case 0: + staticOrDynamic = "static-cred" + case 1: + staticOrDynamic = "creds" + } + + lib, err := NewLdapCredentialLibrary(storeId, fmt.Sprintf("ldap/%s/role-%d", staticOrDynamic, i)) + require.NoError(t, err) + require.NotNil(t, lib) + + libId, err := newLdapCredentialLibraryId(t.Context()) + require.NoError(t, err) + require.NotEmpty(t, libId) + + lib.PublicId = libId + _, err = w.DoTx(t.Context(), db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + return w.Create(t.Context(), lib) + }, + ) + require.NoError(t, err) + 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. @@ -532,6 +570,10 @@ type testOptions struct { clientKey *ecdsa.PrivateKey vaultVersion string serverCertHostNames []string + bindDn string + bindPass string + ldapUrl string + ldapRootDomain string } func getDefaultTestOptions(t testing.TB) testOptions { @@ -666,6 +708,40 @@ func WithAllowedExtension(e string) TestOption { } } +// WithBindDn sets the distinguished name to be used by the Vault LDAP secrets +// engine. +func WithBindDn(dn string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.bindDn = dn + } +} + +// WithBindPass sets the password to be used by the Vault LDAP secrets engine. +// It is also used to set the admin user password in the LDAP server. +func WithBindPass(p string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.bindPass = p + } +} + +// WithLdapUrl sets the URL to be used by the Vault LDAP secrets engine. +func WithLdapUrl(u string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.ldapUrl = u + } +} + +// WithLdapRootDomain controls the LDAP server's root domain. +func WithLdapRootDomain(d string) TestOption { + return func(t testing.TB, o *testOptions) { + t.Helper() + o.ldapRootDomain = d + } +} + // TestOrphanToken sets the token orphan option to b. // The orphan option is true by default. func TestOrphanToken(b bool) TestOption { @@ -916,6 +992,81 @@ func (v *TestVaultServer) MountSSH(t testing.TB, opt ...TestOption) *vault.Secre return s } +// MountLdap mounts the Vault LDAP secrets engine on a Vault instance and +// initializes it. +// +// Supported options include: +// - WithTestMountPath: Sets the mount path. Defaults to "ldap". +// - WithBindDn: Sets the BindDN to be used by the Vault LDAP secrets engine. +// Defaults to "cn=admin,dc=example,dc=org". +// - WithBindPass: Sets the password to be used by the Vault LDAP secrets +// engine. Defaults to "admin". +// - WithLdapUrl: Sets the URL of the LDAP server to be used by the Vault LDAP +// secrets engine. Defaults to "ldap://ldap". +// +// MountLdap also adds a Vault policy named 'ldap' to the standard set of +// policies attached to tokens created with v.CreateToken. The policy is defined +// as: +// +// path "/*" { +// capabilities = ["create", "read", "update", "delete", "list"] +// } +func (v *TestVaultServer) MountLdap(t testing.TB, opt ...TestOption) { + require := require.New(t) + opts := getTestOpts(t, opt...) + vc := v.client(t).cl + + 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 + mountInput := &vault.MountInput{ + Type: "ldap", + Description: t.Name(), + Config: vault.MountConfigInput{ + DefaultLeaseTTL: defaultTTL.String(), + MaxLeaseTTL: maxTTL.String(), + }, + } + mountPath := opts.mountPath + if mountPath == "" { + mountPath = "ldap/" + } + require.True(mountPath[len(mountPath)-1] == '/', "mount path must end with a slash") + require.NoError(vc.Sys().Mount(mountPath, mountInput)) + + policyPath := fmt.Sprintf("%s*", mountPath) + pc := pathCapabilities{ + policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability, + } + v.addPolicy(t, "ldap", pc) + + // Create and write LDAP server config + bindDn := opts.bindDn + if bindDn == "" { + bindDn = "cn=admin,dc=example,dc=org" + } + bindPass := opts.bindPass + if bindPass == "" { + bindPass = "admin" + } + url := opts.ldapUrl + if url == "" { + url = "ldap://ldap" + } + ldapConfig := map[string]any{ + "binddn": bindDn, + "bindpass": bindPass, + "url": url, + } + _, err := vc.Logical().Write(path.Join(mountPath, "config"), ldapConfig) + require.NoError(err) +} + // 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. @@ -1038,9 +1189,12 @@ type TestVaultServer struct { serverCertBundle *testCertBundle clientCertBundle *testCertBundle + // Use any here instead of *dockertest.[Pool|Network|Resource] to avoid + // problems with dockertest not building for some netbsd architectures. pool any - vaultContainer any network any + vaultContainer any + ldapContainer any postgresContainer any } @@ -1115,3 +1269,39 @@ func (d *TestDatabase) ValidateCredential(t testing.TB, s *vault.Secret) error { } return nil } + +// TestLdapServer is returned from MountLdapServer and contains information +// about how an LDAP server was setup. It is primarily used to test +// Boundary <> Vault <> LDAP Server interactions. +type TestLdapServer struct { + BindDomain string + BindPass string + BindDn string + Url string +} + +// MountLdapServer creates an LDAP server as a Docker container, within a +// running Vault's Docker network. Additionally, it mounts and configures the +// LDAP secrets engine in the passed in Vault instance. +// +// Supported options: +// - WithBindPass. Controls the LDAP server's admin password. Defaults to +// "admin". +// - WithLdapRootDomain. Controls the LDAP server's root domain. Defaults to +// "example.org". +// +// Some roles are added to the LDAP server as well as configured within Vault: +// - scientists: A group of unique members: cn=scientists,dc=example,dc=org +// - einstein: A user belonging to the scientists group: cn=einstein,dc=example,dc=org +// - newton: Another user beloging to the scientists group: cn=newton,dc=example,dc=org +// +// Note that the domain components (dc) will change accordingly if WithDomain is +// passed in. +// +// Dynamic roles are also setup using LDIF templates. These roles trigger Vault +// to create users on-demand and manage their lifecycle. All dynamic users are +// added to the "scientists" group. +func MountLdapServer(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestLdapServer { + t.Helper() + return mountLdapServer(t, v, opt...) +} diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index 3c785c55e..b6a61c335 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -60,6 +60,7 @@ const ( var ( maskManager handlers.MaskManager sshCertMaskManager handlers.MaskManager + ldapMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -107,6 +108,13 @@ func init() { ); err != nil { panic(err) } + if ldapMaskManager, err = handlers.NewMaskManager( + context.Background(), + handlers.MaskDestination{&store.LdapCredentialLibrary{}}, + handlers.MaskSource{&pb.CredentialLibrary{}, &pb.VaultLdapCredentialLibraryAttributes{}}, + ); err != nil { + panic(err) + } // TODO: refactor to remove IdActionsMap and CollectionActions package variables action.RegisterResource(resource.CredentialLibrary, IdActions, CollectionActions) @@ -366,19 +374,27 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre var currentCredentialType globals.CredentialType var mo vault.MappingOverride switch globals.ResourceInfoFromPrefix(req.GetId()).Subtype { - case vault.SSHCertificateLibrarySubtype: - cur, err := repo.LookupSSHCertificateCredentialLibrary(ctx, req.Id) - if err != nil { - return nil, err - } - currentCredentialType = globals.CredentialType(cur.GetCredentialType()) - default: + case vault.GenericLibrarySubtype: cur, err := repo.LookupCredentialLibrary(ctx, req.Id) if err != nil { return nil, err } currentCredentialType = globals.CredentialType(cur.GetCredentialType()) mo = cur.MappingOverride + case vault.SSHCertificateLibrarySubtype: + cur, err := repo.LookupSSHCertificateCredentialLibrary(ctx, req.Id) + if err != nil { + return nil, err + } + currentCredentialType = globals.CredentialType(cur.GetCredentialType()) + case vault.LdapCredentialLibrarySubtype: + cur, err := repo.LookupLdapCredentialLibrary(ctx, req.GetId()) + if err != nil { + return nil, err + } + currentCredentialType = globals.CredentialType(cur.GetCredentialType()) + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown vault credential library subtype") } if err := validateUpdateRequest(req, currentCredentialType); err != nil { @@ -456,6 +472,15 @@ func (s Service) getFromRepo(ctx context.Context, id string) (credential.Library return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("ssh certificate credential library %q not found", id)) } return cs, err + case vault.LdapCredentialLibrarySubtype: + cs, err := repo.LookupLdapCredentialLibrary(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("ldapcredential library %q not found", id)) + } + return cs, err } return nil, errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential library subtype") } @@ -464,6 +489,23 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred const op = "credentiallibraries.(Service).createInRepo" var out credential.Library switch item.GetType() { + case vault.GenericLibrarySubtype.String(): + cl, err := toStorageVaultLibrary(ctx, 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 case vault.SSHCertificateLibrarySubtype.String(): cl, err := toStorageVaultSSHCertificateLibrary(ctx, item.GetCredentialStoreId(), item) if err != nil { @@ -481,8 +523,8 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred 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(ctx, item.GetCredentialStoreId(), item) + case vault.LdapCredentialLibrarySubtype.String(): + cl, err := toStorageVaultLdapLibrary(ctx, item.GetCredentialStoreId(), item) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -490,14 +532,16 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred if err != nil { return nil, errors.Wrap(ctx, err, op) } - rl, err := repo.CreateCredentialLibrary(ctx, scopeId, cl) + rl, err := repo.CreateLdapCredentialLibrary(ctx, scopeId, cl) if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential library")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create ldap credential library")) } if rl == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential library but no error returned from repository.") + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ldap credential library but no error returned from repository.") } out = rl + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Unknown vault credential library subtype %q", item.GetType()) } return out, nil } @@ -534,6 +578,23 @@ func (s Service) updateInRepo( return nil, errors.Wrap(ctx, err, op) } switch globals.ResourceInfoFromPrefix(id).Subtype { + case vault.GenericLibrarySubtype: + 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(ctx, 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) + } case vault.SSHCertificateLibrarySubtype: dbMasks = append(dbMasks, sshCertMaskManager.Translate(masks)...) if getMapUpdate(criticalOptionsField, masks) { @@ -557,23 +618,25 @@ func (s Service) updateInRepo( 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)...) + case vault.LdapCredentialLibrarySubtype: + dbMasks = append(dbMasks, ldapMaskManager.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(ctx, item.GetCredentialStoreId(), item) + cl, err := toStorageVaultLdapLibrary(ctx, 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) + out, rowsUpdated, err = repo.UpdateLdapCredentialLibrary(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: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Unknown vault credential library subtype %q", globals.ResourceInfoFromPrefix(id).Subtype) } return out, nil } @@ -586,10 +649,14 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, } rows := 0 switch globals.ResourceInfoFromPrefix(id).Subtype { + case vault.GenericLibrarySubtype: + rows, err = repo.DeleteCredentialLibrary(ctx, scopeId, id) case vault.SSHCertificateLibrarySubtype: rows, err = repo.DeleteSSHCertificateCredentialLibrary(ctx, scopeId, id) + case vault.LdapCredentialLibrarySubtype: + rows, err = repo.DeleteLdapCredentialLibrary(ctx, scopeId, id) default: - rows, err = repo.DeleteCredentialLibrary(ctx, scopeId, id) + return false, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Unknown vault credential library subtype %q", globals.ResourceInfoFromPrefix(id).Subtype) } if err != nil { if errors.IsNotFoundError(err) { @@ -640,6 +707,17 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type, isRec return res } parentId = cl.GetStoreId() + case vault.LdapCredentialLibrarySubtype: + cl, err := repo.LookupLdapCredentialLibrary(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 @@ -854,7 +932,21 @@ func toProto(ctx context.Context, in credential.Library, opt ...handlers.Option) VaultSshCertificateCredentialLibraryAttributes: attrs, } } - + case vault.LdapCredentialLibrarySubtype: + vaultIn, ok := in.(*vault.LdapCredentialLibrary) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, "unable to cast to vault ldap credential library") + } + if outputFields.Has(globals.CredentialTypeField) { + out.CredentialType = vaultIn.GetCredentialType() + } + if outputFields.Has(globals.AttributesField) { + out.Attrs = &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String(vaultIn.GetVaultPath()), + }, + } + } } return &out, nil } @@ -992,20 +1084,33 @@ func toStorageVaultSSHCertificateLibrary(ctx context.Context, storeId string, in return cs, err } +func toStorageVaultLdapLibrary(ctx context.Context, storeId string, in *pb.CredentialLibrary) (*vault.LdapCredentialLibrary, error) { + const op = "credentiallibraries.toStorageVaultLdapLibrary" + + 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())) + } + + attrs := in.GetVaultLdapCredentialLibraryAttributes() + cs, err := vault.NewLdapCredentialLibrary(storeId, attrs.GetPath().GetValue(), opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential library")) + } + + return cs, nil +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // - The path passed in is correctly formatted // - All required parameters are set // - There are no conflicting parameters provided func validateGetRequest(req *pbs.GetCredentialLibraryRequest) error { - prefix := "" - switch globals.ResourceInfoFromPrefix(req.GetId()).Subtype { - case vault.SSHCertificateLibrarySubtype: - prefix = globals.VaultSshCertificateCredentialLibraryPrefix - default: - prefix = globals.VaultCredentialLibraryPrefix - } - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, prefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, globals.VaultLdapCredentialLibraryPrefix) } func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { @@ -1048,8 +1153,9 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { } if t != vault.GenericLibrarySubtype.String() && - t != vault.SSHCertificateLibrarySubtype.String() { - badFields[globals.CredentialStoreIdField] = fmt.Sprintf("Type must be a vault subtype %q or %q", vault.GenericLibrarySubtype.String(), vault.SSHCertificateLibrarySubtype.String()) + t != vault.SSHCertificateLibrarySubtype.String() && + t != vault.LdapCredentialLibrarySubtype.String() { + badFields[globals.CredentialStoreIdField] = fmt.Sprintf("Type must be a vault subtype %q, %q or %q", vault.GenericLibrarySubtype.String(), vault.SSHCertificateLibrarySubtype.String(), vault.LdapCredentialLibrarySubtype.String()) } switch req.GetItem().GetType() { @@ -1111,6 +1217,17 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { badFields[keyTypeField] = "If set, value must be 'ed25519', 'ecdsa', or 'rsa'." } validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) + case vault.LdapCredentialLibrarySubtype.String(): + if req.GetItem().GetCredentialType() != "" { + badFields[globals.CredentialTypeField] = "This field is read only and cannot be set." + } + attrs := req.GetItem().GetVaultLdapCredentialLibraryAttributes() + if attrs == nil { + badFields[attributesPathField] = "This is a required field." + } + if attrs.GetPath().GetValue() == "" { + badFields[vaultPathField] = "This is a required field." + } } default: badFields[globals.CredentialStoreIdField] = "This field must be a valid credential store id." @@ -1120,17 +1237,9 @@ func validateCreateRequest(req *pbs.CreateCredentialLibraryRequest) error { } func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCredentialType globals.CredentialType) error { - prefix := "" - st := globals.ResourceInfoFromPrefix(req.GetId()).Subtype - switch st { - case vault.GenericLibrarySubtype: - prefix = globals.VaultCredentialLibraryPrefix - case vault.SSHCertificateLibrarySubtype: - prefix = globals.VaultSshCertificateCredentialLibraryPrefix - } return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} - switch st { + switch globals.ResourceInfoFromPrefix(req.GetId()).Subtype { case vault.GenericLibrarySubtype: if req.GetItem().GetType() != "" && req.GetItem().GetType() != vault.GenericLibrarySubtype.String() { badFields[globals.TypeField] = "Cannot modify resource type." @@ -1175,13 +1284,27 @@ func validateUpdateRequest(req *pbs.UpdateCredentialLibraryRequest, currentCrede } validateKeyBits(badFields, attrs.GetKeyBits().GetValue(), attrs.GetKeyType().GetValue()) } + case vault.LdapCredentialLibrarySubtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != vault.LdapCredentialLibrarySubtype.String() { + 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().GetVaultLdapCredentialLibraryAttributes() + 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." + } + } } return badFields - }, prefix) + }, globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, globals.VaultLdapCredentialLibraryPrefix) } func validateDeleteRequest(req *pbs.DeleteCredentialLibraryRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, globals.VaultLdapCredentialLibraryPrefix) } func validateListRequest(ctx context.Context, req *pbs.ListCredentialLibrariesRequest) error { diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index 295235acd..e4b59200d 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -86,6 +87,25 @@ func sshCredentialLibraryToProto(credLib *vault.SSHCertificateCredentialLibrary, } } +func ldapCredentialLibraryToProto(credLib *vault.LdapCredentialLibrary, project *iam.Scope) *pb.CredentialLibrary { + return &pb.CredentialLibrary{ + Id: credLib.GetPublicId(), + CredentialStoreId: credLib.GetStoreId(), + CredentialType: credLib.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: project.GetPublicId(), Type: scope.Project.String(), ParentScopeId: project.GetParentId()}, + CreatedTime: credLib.GetCreateTime().GetTimestamp(), + UpdatedTime: credLib.GetUpdateTime().GetTimestamp(), + Version: credLib.GetVersion(), + Type: vault.LdapCredentialLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String(credLib.GetVaultPath()), + }, + }, + } +} + func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -114,6 +134,9 @@ func TestList(t *testing.T) { for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { wantLibraries = append(wantLibraries, sshCredentialLibraryToProto(l, prj)) } + for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 10) { + wantLibraries = append(wantLibraries, ldapCredentialLibraryToProto(l, prj)) + } cases := []struct { name string @@ -131,7 +154,7 @@ func TestList(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 20, + EstItemCount: 30, }, anonRes: &pbs.ListCredentialLibrariesResponse{ Items: wantLibraries, @@ -139,7 +162,7 @@ func TestList(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 20, + EstItemCount: 30, }, }, { @@ -264,16 +287,20 @@ func TestList_Attributes(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) - ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 2) - storeGeneric, storeSSHCertificate := ts[0], ts[1] + ts := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 3) + storeGeneric, storeSSHCertificate, storeLdap := ts[0], ts[1], ts[2] var wantLibrariesGeneric []*pb.CredentialLibrary var wantLibrariesSSHCertificate []*pb.CredentialLibrary + var wantLibrariesLdap []*pb.CredentialLibrary for _, l := range vault.TestCredentialLibraries(t, conn, wrapper, storeGeneric.GetPublicId(), globals.UnspecifiedCredentialType, 5) { wantLibrariesGeneric = append(wantLibrariesGeneric, vaultCredentialLibraryToProto(l, prj)) } for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, storeSSHCertificate.GetPublicId(), 10) { wantLibrariesSSHCertificate = append(wantLibrariesSSHCertificate, sshCredentialLibraryToProto(l, prj)) } + for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, storeLdap.GetPublicId(), 10) { + wantLibrariesLdap = append(wantLibrariesLdap, ldapCredentialLibraryToProto(l, prj)) + } cases := []struct { name string @@ -318,6 +345,24 @@ func TestList_Attributes(t *testing.T) { RemovedIds: nil, }, // anonymous user does not have access to attributes }, + { + name: "Filter on Attribute Ldap Library", + req: &pbs.ListCredentialLibrariesRequest{CredentialStoreId: storeLdap.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/path"==%q`, wantLibrariesLdap[2].GetVaultLdapCredentialLibraryAttributes().GetPath().Value)}, + res: &pbs.ListCredentialLibrariesResponse{ + Items: wantLibrariesLdap[2:3], + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 1, + }, + anonRes: &pbs.ListCredentialLibrariesResponse{ + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + }, // anonymous user does not have access to attributes + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -586,6 +631,51 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "No Vault LDAP path", + req: &pbs.CreateCredentialLibraryRequest{ + Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.LdapCredentialLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String(""), + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid Vault LDAP credential type", + req: &pbs.CreateCredentialLibraryRequest{ + Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.LdapCredentialLibrarySubtype.String(), + CredentialType: string(globals.SshPrivateKeyCredentialType), + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("ldap/invalid/path"), + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Invalid credential library type", + req: &pbs.CreateCredentialLibraryRequest{ + Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: "bla-invalid", + CredentialType: "bla-invalid", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "Using POST method", req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{ @@ -1103,6 +1193,40 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "Create a valid Vault LDAP Credential library", + req: &pbs.CreateCredentialLibraryRequest{ + Item: &pb.CredentialLibrary{ + CredentialStoreId: store.GetPublicId(), + Type: vault.LdapCredentialLibrarySubtype.String(), + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("ldap/creds/hi"), + }, + }, + }, + }, + idPrefix: globals.VaultLdapCredentialLibraryPrefix + "_", + res: &pbs.CreateCredentialLibraryResponse{ + Uri: fmt.Sprintf("credential-libraries/%s_", globals.VaultLdapCredentialLibraryPrefix), + 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.LdapCredentialLibrarySubtype.String(), + CredentialType: string(globals.UsernamePasswordDomainCredentialType), + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("ldap/creds/hi"), + }, + }, + AuthorizedActions: testAuthorizedActions, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -1217,6 +1341,13 @@ func TestGet(t *testing.T) { sshCertLib, err := repo.CreateSSHCertificateCredentialLibrary(context.Background(), prj.GetPublicId(), lib2) require.NoError(t, err) + libLdap1, err := vault.NewLdapCredentialLibrary(store.GetPublicId(), "ldap/creds/test") + require.NoError(t, err) + require.NotNil(t, libLdap1) + libLdap1, err = repo.CreateLdapCredentialLibrary(t.Context(), prj.GetPublicId(), libLdap1) + require.NoError(t, err) + require.NotNil(t, libLdap1) + cases := []struct { name string id string @@ -1367,6 +1498,28 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-ldap", + id: libLdap1.GetPublicId(), + res: &pbs.GetCredentialLibraryResponse{ + Item: &pb.CredentialLibrary{ + Id: libLdap1.GetPublicId(), + CredentialStoreId: libLdap1.GetStoreId(), + CredentialType: libLdap1.GetCredentialType(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: vault.LdapCredentialLibrarySubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: libLdap1.CreateTime.GetTimestamp(), + UpdatedTime: libLdap1.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String(libLdap1.GetVaultPath()), + }, + }, + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.VaultCredentialLibraryPrefix), @@ -1429,6 +1582,7 @@ func TestDelete(t *testing.T) { store := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1)[0] vl := vault.TestCredentialLibraries(t, conn, wrapper, store.GetPublicId(), globals.UnspecifiedCredentialType, 1)[0] vl2 := vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] + vl3 := vault.TestLdapCredentialLibraries(t, conn, wrapper, store.GetPublicId(), 1)[0] s, err := NewService(ctx, iamRepoFn, repoFn, 1000) require.NoError(t, err) cases := []struct { @@ -1445,6 +1599,10 @@ func TestDelete(t *testing.T) { name: "success-ssh-cert", id: vl2.GetPublicId(), }, + { + name: "success-ldap", + id: vl3.GetPublicId(), + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.VaultCredentialLibraryPrefix), @@ -3244,6 +3402,287 @@ func TestUpdate_SSHCertificateCredentialLibrary(t *testing.T) { } } +func TestUpdate_LdapCredentialLibrary(t *testing.T) { + testCtx := context.Background() + 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(testCtx, rw, rw, kms, sche) + } + + _, prj := iam.TestScopes(t, iamRepo) + ctx := auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()) + + s, err := NewService(testCtx, iamRepoFn, repoFn, 1000) + require.NoError(t, err) + cs := vault.TestCredentialStores(t, conn, wrapper, prj.GetPublicId(), 1) + store := cs[0] + + freshLibrary := func(opt ...vault.Option) (*vault.LdapCredentialLibrary, func()) { + repo, err := repoFn() + require.NoError(t, err) + lib, err := vault.NewLdapCredentialLibrary(store.GetPublicId(), "ldap/creds/foo", opt...) + require.NoError(t, err) + + vl, err := repo.CreateLdapCredentialLibrary(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 + } + + tests := []struct { + name string + req *pbs.UpdateCredentialLibraryRequest + res func(*pb.CredentialLibrary) *pb.CredentialLibrary + wantErr bool + wantErrStatus int + wantErrStr string + }{ + { + name: "updateNonExistentField", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"doesntExist"}}, + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("updateNonExistentField_test"), + }, + }, + wantErr: true, + wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), + wantErrStr: "No valid fields included in the update mask.", + }, + { + name: "updateImmutableField", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"storeId"}}, + Item: &pb.CredentialLibrary{ + CredentialStoreId: "immutable", + }, + }, + wantErr: true, + wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), + wantErrStr: "No valid fields included in the update mask.", + }, + { + name: "updateValidFieldWithInvalidData", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, + Item: &pb.CredentialLibrary{ + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("invalid_path"), + }, + }, + }, + }, + wantErr: true, + wantErrStr: "vault_path_must_have_staticcred_or_creds constraint failed", + }, + { + name: "updateImmutableCredentialTypeField", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"credentialType"}}, + Item: &pb.CredentialLibrary{ + CredentialType: string(globals.SshPrivateKeyCredentialType), + }, + }, + wantErr: true, + wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), + wantErrStr: "Cannot modify credential type.", + }, + { + name: "updateImmutableTypeField", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"type"}}, + Item: &pb.CredentialLibrary{ + Type: string(globals.SshCertificateCredentialType), + }, + }, + wantErr: true, + wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), + wantErrStr: "Cannot modify resource type.", + }, + { + name: "unsetVaultPath", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, + Item: &pb.CredentialLibrary{ + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String(""), + }, + }, + }, + }, + wantErr: true, + wantErrStatus: runtime.HTTPStatusFromCode(codes.InvalidArgument), + wantErrStr: "attributes.path\", desc: \"This is a required field and cannot be set to empty.", + }, + { + name: "updateName", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name"}}, + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("updated_name"), + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.Name = wrapperspb.String("updated_name") + return cl + }, + }, + { + name: "updateDescription", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"description"}}, + Item: &pb.CredentialLibrary{ + Description: wrapperspb.String("updated_description"), + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.Description = wrapperspb.String("updated_description") + return cl + }, + }, + { + name: "updateValidVaultPath", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"attributes.path"}}, + Item: &pb.CredentialLibrary{ + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("ldap/static-cred/updated/path"), + }, + }, + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.GetVaultLdapCredentialLibraryAttributes().Path = wrapperspb.String("ldap/static-cred/updated/path") + return cl + }, + }, + { + name: "updateMultipleOneNonExistent", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "doesntExist"}}, + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("updated_name"), + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.Name = wrapperspb.String("updated_name") + return cl + }, + }, + { + name: "updateMultipleOneImmutable", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime. + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "credentialStoreId"}}, + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("updated_name"), + CredentialStoreId: "immutable", + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.Name = wrapperspb.String("updated_name") + return cl + }, + }, + { + name: "updateMultiple", + req: &pbs.UpdateCredentialLibraryRequest{ + Id: "", // Set at test runtime + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "attributes.path"}}, + Item: &pb.CredentialLibrary{ + Name: wrapperspb.String("updated_name"), + Attrs: &pb.CredentialLibrary_VaultLdapCredentialLibraryAttributes{ + VaultLdapCredentialLibraryAttributes: &pb.VaultLdapCredentialLibraryAttributes{ + Path: wrapperspb.String("ldap/creds/updated/path"), + }, + }, + }, + }, + res: func(cl *pb.CredentialLibrary) *pb.CredentialLibrary { + cl.Name = wrapperspb.String("updated_name") + cl.GetVaultLdapCredentialLibraryAttributes().Path = wrapperspb.String("ldap/creds/updated/path") + return cl + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + st, cleanup := freshLibrary() + 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) + + got, gErr := s.UpdateCredentialLibrary(ctx, tc.req) + if tc.wantErr { + require.ErrorContains(gErr, tc.wantErrStr) + + gApiErr, ok := gErr.(*handlers.ApiError) + if ok { + // We don't always return status. + require.NotNil(gApiErr) + require.EqualValues(tc.wantErrStatus, gApiErr.Status) + } + require.Nil(got) + return + } + require.NoError(gErr) + require.NotNil(got) + + want := &pbs.UpdateCredentialLibraryResponse{Item: tc.res(resToChange.GetItem())} + 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(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) + }) + } +} + func TestListPagination(t *testing.T) { // Set database read timeout to avoid duplicates in response oldReadTimeout := globals.RefreshReadLookbackDuration @@ -3292,6 +3731,9 @@ func TestListPagination(t *testing.T) { for _, l := range vault.TestSSHCertificateCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 5) { allCredentialLibraries = append(allCredentialLibraries, sshCredentialLibraryToProto(l, prj)) } + for _, l := range vault.TestLdapCredentialLibraries(t, conn, wrapper, credStore.GetPublicId(), 5) { + allCredentialLibraries = append(allCredentialLibraries, ldapCredentialLibraryToProto(l, prj)) + } // Reverse as we return items sorted by create_time desceding (newest first) slices.Reverse(allCredentialLibraries) @@ -3345,7 +3787,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3370,7 +3812,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3382,10 +3824,10 @@ func TestListPagination(t *testing.T) { // Request rest of results req.ListToken = got.ListToken - req.PageSize = 10 + req.PageSize = 15 got, err = s.ListCredentialLibraries(ctx, req) require.NoError(err) - require.Len(got.GetItems(), 6) + require.Len(got.GetItems(), 11) // Compare without comparing the list token assert.Empty( cmp.Diff( @@ -3396,7 +3838,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3421,14 +3863,14 @@ func TestListPagination(t *testing.T) { // Update one of the other stores allCredentialLibraries[1].Name = wrapperspb.String("new-name") allCredentialLibraries[1].Version = 2 - updatedLibrary := &vault.SSHCertificateCredentialLibrary{ - SSHCertificateCredentialLibrary: &store.SSHCertificateCredentialLibrary{ + updatedLibrary := &vault.LdapCredentialLibrary{ + LdapCredentialLibrary: &store.LdapCredentialLibrary{ PublicId: allCredentialLibraries[1].GetId(), StoreId: allCredentialLibraries[1].GetCredentialStoreId(), Name: allCredentialLibraries[1].Name.Value, }, } - cred, _, err := vaultRepo.UpdateSSHCertificateCredentialLibrary(ctx, prj.PublicId, updatedLibrary, 1, []string{"name"}) + cred, _, err := vaultRepo.UpdateLdapCredentialLibrary(ctx, prj.PublicId, updatedLibrary, 1, []string{"name"}) require.NoError(err) allCredentialLibraries[1].UpdatedTime = cred.UpdateTime.GetTimestamp() allCredentialLibraries[1].Version = cred.Version @@ -3462,7 +3904,7 @@ func TestListPagination(t *testing.T) { SortDir: "desc", // Should contain the deleted library RemovedIds: []string{deletedCredLib.Id}, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3487,7 +3929,7 @@ func TestListPagination(t *testing.T) { SortBy: "updated_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3515,7 +3957,7 @@ func TestListPagination(t *testing.T) { SortDir: "desc", // Should be empty again RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b @@ -3538,7 +3980,7 @@ func TestListPagination(t *testing.T) { SortBy: "created_time", SortDir: "desc", RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index f7ae11100..8554c494c 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -2056,6 +2056,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ for _, cl := range req.GetBrokeredCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, @@ -2069,6 +2070,7 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, @@ -2094,6 +2096,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ for _, cl := range req.GetBrokeredCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, @@ -2107,6 +2110,7 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, @@ -2136,6 +2140,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc for _, cl := range req.GetBrokeredCredentialSourceIds() { if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, @@ -2149,6 +2154,7 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc if !handlers.ValidId(handlers.Id(cl), globals.VaultCredentialLibraryPrefix, globals.VaultSshCertificateCredentialLibraryPrefix, + globals.VaultLdapCredentialLibraryPrefix, globals.UsernamePasswordCredentialPrefix, globals.UsernamePasswordCredentialPreviousPrefix, globals.UsernamePasswordDomainCredentialPrefix, diff --git a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql index 2d109df2e..7304a5793 100644 --- a/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/10/04_vault_credential.up.sql @@ -264,6 +264,7 @@ begin; ('GET'), ('POST'); + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library ( public_id wt_public_id primary key, store_id wt_public_id not null @@ -379,6 +380,7 @@ begin; ('expired'), ('unknown'); + -- Updated in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_credential ( public_id wt_public_id primary key, library_id wt_public_id diff --git a/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql b/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql new file mode 100644 index 000000000..98967e865 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/01_credential_vault_ldap_library.up.sql @@ -0,0 +1,506 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + create table credential_vault_ldap_library ( + public_id wt_public_id primary key, + project_id wt_public_id not null, + 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_have_staticcred_or_creds + check(vault_path ~ '^.+\/(static-cred|creds)\/(?:[^\/\\\s]+\/)*[^\/\\\s]+$'), + credential_type text not null + constraint credential_type_enm_fkey + references credential_type_enm (name) + on delete restrict + on update cascade, + constraint credential_vault_ldap_library_store_id_public_id_uq + unique(store_id, public_id), + constraint credential_vault_ldap_library_store_id_name_uq + unique(store_id, name), + constraint credential_vault_library_fkey + foreign key (project_id, store_id, public_id, credential_type) + references credential_vault_library (project_id, store_id, public_id, credential_type) + on delete cascade + on update cascade + ); + + comment on table credential_vault_ldap_library is + 'credential_vault_ldap_library is a credential library that issues credentials from a vault ldap secret backend.'; + + create trigger insert_credential_vault_library_subtype before insert on credential_vault_ldap_library + for each row execute procedure insert_credential_vault_library_subtype(); + + create trigger update_credential_vault_library_table_update_time before update on credential_vault_ldap_library + for each row execute procedure update_credential_vault_library_table_update_time(); + + create trigger immutable_columns before update on credential_vault_ldap_library + for each row execute procedure immutable_columns('public_id', 'store_id', 'project_id', 'credential_type', 'create_time'); + + create trigger delete_credential_vault_library_subtype after delete on credential_vault_ldap_library + for each row execute procedure delete_credential_vault_library_subtype(); + + create trigger update_version_column after update on credential_vault_ldap_library + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on credential_vault_ldap_library + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on credential_vault_ldap_library + for each row execute procedure default_create_time(); + + create trigger before_insert_credential_vault_library before insert on credential_vault_ldap_library + for each row execute procedure before_insert_credential_vault_library(); + + create function default_vault_ldap_credential_type() returns trigger + as $$ + begin + if new.credential_type is distinct from 'username_password_domain' then + new.credential_type = 'username_password_domain'; + end if; + return new; + end; + $$ language plpgsql; + comment on function default_vault_ldap_credential_type is + 'default_vault_ldap_credential_type ensures the credential_type is set to username_password_domain'; + + create trigger default_vault_ldap_credential_type before insert on credential_vault_ldap_library + for each row execute procedure default_vault_ldap_credential_type(); + + -- Replaces view from 99/01_credential_vault_library_refactor.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_generic_library_username_password_mapping_ovrd + ), + 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_generic_library_ssh_private_key_mapping_ovrd + ), + password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd + ) + 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, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute) + as password_attribute, + pd.domain_attribute as domain_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 + null as additional_valid_principals + from credential_vault_generic_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 + left join password_domain_override pd + on library.public_id = pd.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 domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type, -- used to switch on + additional_valid_principals as additional_valid_principals + from credential_vault_ssh_cert_library library + join credential_vault_store_client store + on library.store_id = store.public_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, + 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, + pd.username_attribute as username_attribute, + pd.password_attribute as password_attribute, + pd.domain_attribute as domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ldap' as cred_lib_type, -- used to switch on + null as additional_valid_principals + from credential_vault_ldap_library library + join credential_vault_store_client store + on library.store_id = store.public_id + left join password_domain_override pd + on library.public_id = pd.library_id; + + comment on view credential_vault_library_issue_credentials is + 'credential_vault_library_issue_credentials is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. ' + 'This view should only be used when issuing credentials from a Vault credential library. Each row may contain encrypted data. ' + 'This view should not be used to retrieve data which will be returned external to boundary.'; + +insert into oplog_ticket (name, version) +values + ('credential_vault_ldap_library', 1); + + create table credential_vault_ldap_library_hst ( + public_id wt_public_id not null, + project_id wt_public_id not null, + store_id wt_public_id not null, + name wt_name, + description wt_description, + vault_path text not null, + credential_type text not null, + history_id wt_url_safe_id default wt_url_safe_id() primary key + constraint credential_library_history_base_fkey + references credential_library_history_base (history_id) + on delete cascade + on update cascade, + valid_range tstzrange not null default tstzrange(current_timestamp, null), + constraint credential_vault_ldap_library_hst_valid_range_excl + exclude using gist (public_id with =, valid_range with &&) + ); + comment on table credential_vault_ldap_library_hst is + 'credential_vault_ldap_library_hst is a history table where each row contains values from a row ' + 'in the credential_vault_ldap_library table during the time range in the valid_range column'; + + create trigger insert_credential_library_history_subtype before insert on credential_vault_ldap_library_hst + for each row execute function insert_credential_library_history_subtype(); + + create trigger delete_credential_library_history_subtype after delete on credential_vault_ldap_library_hst + for each row execute function delete_credential_library_history_subtype(); + + create trigger hst_on_insert after insert on credential_vault_ldap_library + for each row execute function hst_on_insert(); + + create trigger hst_on_update after update on credential_vault_ldap_library + for each row execute function hst_on_update(); + + create trigger hst_on_delete after delete on credential_vault_ldap_library + for each row execute function hst_on_delete(); + + create view credential_vault_ldap_library_hst_aggregate as + select + rdc.recording_id, + vll.public_id, + vll.name, + vll.description, + vll.vault_path, + vll.credential_type, + vsh.public_id as store_public_id, + vsh.project_id as store_project_id, + vsh.name as store_name, + vsh.description as store_description, + vsh.vault_address as store_vault_address, + vsh.namespace as store_namespace, + vsh.tls_server_name as store_tls_server_name, + vsh.tls_skip_verify as store_tls_skip_verify, + vsh.worker_filter as store_worker_filter, + string_agg(distinct rdc.credential_purpose, '|') as purposes + from credential_vault_ldap_library_hst as vll + left join recording_dynamic_credential as rdc on vll.history_id = rdc.credential_library_hst_id + join credential_vault_store_hst as vsh on rdc.credential_vault_store_hst_id = vsh.history_id + group by vll.history_id, rdc.recording_id, vsh.history_id; + comment on view credential_vault_ldap_library_hst_aggregate is + 'credential_vault_ldap_library_hst_aggregate contains the vault ldap library history data along with its store and purpose data.'; + + create table credential_vault_ldap_library_deleted ( + public_id wt_public_id primary key, + delete_time wt_timestamp not null + ); + comment on table credential_vault_ldap_library_deleted is + 'credential_vault_ldap_library_deleted holds the ID and delete_time ' + 'of every deleted vault ldap credential' + 'It is automatically trimmed of records older than 30 days by a job.'; + + create index credential_vault_ldap_library_deleted_delete_time_idx on credential_vault_ldap_library_deleted (delete_time); + + create trigger insert_deleted_id after delete on credential_vault_ldap_library + for each row execute function insert_deleted_id('credential_vault_ldap_library_deleted'); + + -- Replaces the function in 99/01_credential_vault_library_refactor.up.sql + drop trigger insert_recording_dynamic_credentials on recording_session; + drop function insert_recording_dynamic_credentials(); + create function insert_recording_dynamic_credentials() returns trigger + as $$ + begin + with + session_recording(session_id, recording_id) as ( + select session_id, public_id + from recording_session + where session_id = new.session_id + ), + session_dynamic_creds(library_id, purpose, recording_id) as ( + select library_id, credential_purpose, recording_id + from session_credential_dynamic + join session_recording using (session_id) + ), + library_history(public_id, store_id, library_hst_id, valid_range) as ( + select public_id, store_id, history_id, valid_range + from credential_vault_generic_library_hst + union + select public_id, store_id, history_id, valid_range + from credential_vault_ssh_cert_library_hst + union + select public_id, store_id, history_id, valid_range + from credential_vault_ldap_library_hst + ), + final(recording_id, library_id, store_id, library_hst_id, store_hst_id, cred_purpose) as ( + select sdc.recording_id, lib.public_id, lib.store_id, lib.library_hst_id, store_hst.history_id, sdc.purpose + from library_history as lib + join credential_vault_store_hst as store_hst on lib.store_id = store_hst.public_id + and store_hst.valid_range @> current_timestamp + join session_dynamic_creds as sdc on lib.public_id = sdc.library_id + where lib.public_id in (select library_id from session_dynamic_creds) + and lib.valid_range @> current_timestamp + ) + insert into recording_dynamic_credential + (recording_id, credential_vault_store_hst_id, credential_library_hst_id, credential_purpose) + select recording_id, store_hst_id, library_hst_id, cred_purpose + from final; + return new; + end; + $$ language plpgsql; + comment on function insert_recording_dynamic_credentials is + 'insert_recording_dynamic_credentials is an after insert trigger for the recording_session table.'; + + create trigger insert_recording_dynamic_credentials after insert on recording_session + for each row execute procedure insert_recording_dynamic_credentials(); + + -- Replaces view defined in 99/01_credential_vault_library_refactor.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_generic_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 + ), + vault_ldap_library as ( + select vldapcl.public_id as public_id, + 'vault ldap credential library' as type, + coalesce(vldapcl.name, 'None') as name, + coalesce(vldapcl.description, 'None') as description, + vldapcl.vault_path as vault_path, + 'Not Applicable' as http_method, + 'Not Applicable' as http_request_body, + 'Not Applicable' as username, + 'Not Applicable' as key_type_and_bits + from credential_vault_ldap_library as vldapcl + ), + 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, vldapcl.type) as credential_library_type, + coalesce(vcl.name, vsccl.name, vldapcl.name) as credential_library_name, + coalesce(vcl.description, vsccl.description, vldapcl.description) as credential_library_description, + coalesce(vcl.vault_path, vsccl.vault_path, vldapcl.vault_path) as credential_library_vault_path, + coalesce(vcl.http_method, vsccl.http_method, vldapcl.http_method) as credential_library_vault_http_method, + coalesce(vcl.http_request_body, vsccl.http_request_body, vldapcl.http_request_body) as credential_library_vault_http_request_body, + coalesce(vcl.username, vsccl.username, vldapcl.username) as credential_library_username, + coalesce(vcl.key_type_and_bits, vsccl.key_type_and_bits, vldapcl.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, + case + when tt.type = 'tcp' then 'tcp target' + when tt.type = 'ssh' then 'ssh target' + when tt.type = 'rdp' then 'rdp target' + else 'Unknown' + end 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 vault_ldap_library as vldapcl on cl.public_id = vldapcl.public_id + left join credential_vault_store as vcs on cs.public_id = vcs.public_id + left join target_all_subtypes 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; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql b/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql index 8d1877a99..e4e1f0d8c 100644 --- a/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql +++ b/internal/db/schema/migrations/oss/postgres/22/03_vault_library_map.up.sql @@ -3,6 +3,7 @@ begin; + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_mapping_override ( library_id wt_public_id primary key constraint credential_vault_library_fkey @@ -16,6 +17,7 @@ begin; -- insert_credential_vault_library_mapping_override_subtype() is a before insert trigger -- function for subtypes of credential_vault_library_mapping_override + -- Updated and renamed in 99/01_credential_vault_library_refactor.up.sql create function insert_credential_vault_library_mapping_override_subtype() returns trigger as $$ begin @@ -29,6 +31,7 @@ begin; -- delete_credential_vault_library_mapping_override_subtype() is an after delete trigger -- function for subtypes of credential_vault_library_mapping_override + -- Updated and renamed in 99/01_credential_vault_library_refactor.up.sql create function delete_credential_vault_library_mapping_override_subtype() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql b/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql index 203b2b13c..e63228749 100644 --- a/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql +++ b/internal/db/schema/migrations/oss/postgres/22/04_vault_library_map_user_password.up.sql @@ -3,7 +3,8 @@ begin; --- Updated in 36/01_vault_library_map_username_password.up.sql + -- Updated in 36/01_vault_library_map_username_password.up.sql + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_user_password_mapping_override ( library_id wt_public_id primary key constraint credential_vault_library_fkey diff --git a/internal/db/schema/migrations/oss/postgres/36/01_vault_library_map_username_password.up.sql b/internal/db/schema/migrations/oss/postgres/36/01_vault_library_map_username_password.up.sql index 52ea50f95..b3dfe0145 100644 --- a/internal/db/schema/migrations/oss/postgres/36/01_vault_library_map_username_password.up.sql +++ b/internal/db/schema/migrations/oss/postgres/36/01_vault_library_map_username_password.up.sql @@ -3,7 +3,8 @@ begin; --- Renames table from 22/04_vault_library_map_user_password.up.sql + -- Renames table from 22/04_vault_library_map_user_password.up.sql + -- Renamed again in 99/01_credential_vault_library_refactor.up.sql alter table credential_vault_library_user_password_mapping_override rename to credential_vault_library_username_password_mapping_override; comment on table credential_vault_library_username_password_mapping_override is diff --git a/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql b/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql index 268ad9e81..0be0f10c2 100644 --- a/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql +++ b/internal/db/schema/migrations/oss/postgres/39/02_vault_ssh_private_key_override.up.sql @@ -7,6 +7,7 @@ begin; insert into credential_type_enm (name) values ('ssh_private_key'); + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_ssh_private_key_mapping_override ( library_id wt_public_id primary key constraint credential_vault_library_fkey 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 index 0b80f5050..5d7393916 100644 --- 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 @@ -93,6 +93,7 @@ begin; ('rsa', 3072), ('rsa', 4096); + -- Updated in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_ssh_cert_library ( public_id wt_public_id primary key, store_id wt_public_id not null 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 index 564bd1847..c70acf944 100644 --- 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 @@ -4,6 +4,7 @@ begin; -- Replaces view from 56/02_add_data_key_foreign_key_references.up.sql + -- Replaced in 78/01_ssh_signed_certs_additional_valid_principals drop view credential_vault_library_issue_credentials; create view credential_vault_library_issue_credentials as with diff --git a/internal/db/schema/migrations/oss/postgres/71/15_dynamic_credential_history.up.sql b/internal/db/schema/migrations/oss/postgres/71/15_dynamic_credential_history.up.sql index 820ece1ff..9fb8ffb0e 100644 --- a/internal/db/schema/migrations/oss/postgres/71/15_dynamic_credential_history.up.sql +++ b/internal/db/schema/migrations/oss/postgres/71/15_dynamic_credential_history.up.sql @@ -37,6 +37,7 @@ begin; 'delete_credential_library_history_subtype() is an after delete trigger ' 'function for subtypes of credential_library_history_base.'; + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_hst ( public_id wt_public_id not null, name wt_name, diff --git a/internal/db/schema/migrations/oss/postgres/71/16_recording_dynamic_credential.up.sql b/internal/db/schema/migrations/oss/postgres/71/16_recording_dynamic_credential.up.sql index c9c614e32..1f1828fee 100644 --- a/internal/db/schema/migrations/oss/postgres/71/16_recording_dynamic_credential.up.sql +++ b/internal/db/schema/migrations/oss/postgres/71/16_recording_dynamic_credential.up.sql @@ -33,6 +33,7 @@ begin; 'along with the values of those entities at the time of the recording. ' 'These values are also stored in the BSR file.'; + -- Updated in 99/01_credential_vault_library_refactor.up.sql create function insert_recording_dynamic_credentials() returns trigger as $$ begin diff --git a/internal/db/schema/migrations/oss/postgres/71/17_credential_history_views.up.sql b/internal/db/schema/migrations/oss/postgres/71/17_credential_history_views.up.sql index a5fcd38ab..01f02fedd 100644 --- a/internal/db/schema/migrations/oss/postgres/71/17_credential_history_views.up.sql +++ b/internal/db/schema/migrations/oss/postgres/71/17_credential_history_views.up.sql @@ -66,7 +66,8 @@ group by sspkc.history_id, rsc.recording_id, css.history_id; comment on view credential_static_ssh_private_key_credential_hst_aggregate is 'credential_static_ssh_private_key_credential_hst_aggregate contains the ssh private key credential history data along with its store and purpose data.'; --- Dynamic Credentials +-- Replaced in 99/01_credential_vault_library_refactor.up.sql. Note that this +-- view's name has changed to credential_vault_generic_library_hst_aggregate. create view credential_vault_library_hst_aggregate as select rdc.recording_id, diff --git a/internal/db/schema/migrations/oss/postgres/81/01_deleted_tables_and_triggers.up.sql b/internal/db/schema/migrations/oss/postgres/81/01_deleted_tables_and_triggers.up.sql index 1d65d241d..543f99a92 100644 --- a/internal/db/schema/migrations/oss/postgres/81/01_deleted_tables_and_triggers.up.sql +++ b/internal/db/schema/migrations/oss/postgres/81/01_deleted_tables_and_triggers.up.sql @@ -118,6 +118,7 @@ begin; 'It is automatically trimmed of records older than 30 days by a job.'; -- Credential libraries + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_deleted ( public_id wt_public_id primary key, delete_time wt_timestamp not null diff --git a/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql b/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql index 91720c6d2..429a3cd0d 100644 --- a/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql +++ b/internal/db/schema/migrations/oss/postgres/98/02_username_password_domain_vault.up.sql @@ -3,6 +3,7 @@ begin; + -- Renamed in 99/01_credential_vault_library_refactor.up.sql create table credential_vault_library_username_password_domain_mapping_ovrd ( library_id wt_public_id primary key constraint credential_vault_library_fkey @@ -38,6 +39,7 @@ begin; -- Replaces view from 78/01_ssh_signed_certs_additional_valid_principals.up.sql +-- Replaced in 99/01_credential_vault_library_refactor.up.sql drop view credential_vault_library_issue_credentials; create view credential_vault_library_issue_credentials as with @@ -161,6 +163,8 @@ begin; -- Replaces view created in 49/01_vault_credentials.up.sql +-- Replaced in 99/01_credential_vault_library_refactor.up.sql where this +-- view's name changed to credential_vault_generic_library_list_lookup. drop view credential_vault_library_list_lookup; create view credential_vault_library_list_lookup as with diff --git a/internal/db/schema/migrations/oss/postgres/98/04_rdp_targets.up.sql b/internal/db/schema/migrations/oss/postgres/98/04_rdp_targets.up.sql index e16c47e6c..269f4cb77 100644 --- a/internal/db/schema/migrations/oss/postgres/98/04_rdp_targets.up.sql +++ b/internal/db/schema/migrations/oss/postgres/98/04_rdp_targets.up.sql @@ -355,6 +355,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 71/07_targets.up.sql + -- Replaced in 99/01_credential_vault_library_refactor.up.sql. create view whx_credential_dimension_source as with vault_generic_library as ( select vcl.public_id as public_id, diff --git a/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql b/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql new file mode 100644 index 000000000..21077d271 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/99/01_credential_vault_library_refactor.up.sql @@ -0,0 +1,765 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +-- For context, check the README.md in this folder. +begin; + -- Main table. + -- Renames the table defined in 10/04_vault_credential.up.sql + alter table credential_vault_library + rename to credential_vault_generic_library; + + alter table credential_vault_generic_library + rename constraint credential_vault_library_pkey to credential_vault_generic_library_pkey; + + alter table credential_vault_generic_library + rename constraint credential_vault_library_store_id_name_uq to credential_vault_generic_library_store_id_name_uq; + + alter table credential_vault_generic_library + rename constraint credential_vault_library_store_id_public_id_uq to credential_vault_generic_library_store_id_public_id_uq; + + comment on table credential_vault_generic_library is + 'credential_vault_generic_library is a table where each row is a resource that represents a vault generic credential library. ' + 'It is a credential_vault_library subtype and a child table of credential_vault_store.'; + + drop trigger insert_deleted_id on credential_vault_generic_library; + create trigger insert_deleted_id after delete on credential_vault_generic_library + for each row execute procedure insert_deleted_id('credential_vault_generic_library_deleted'); + + insert into oplog_ticket + (name, version) + values + ('credential_vault_generic_library', 1) + on conflict do nothing; + delete from oplog_ticket where name = 'credential_vault_library'; + + + -- History table. + -- Renames the table defined in 71/15_dynamic_credential_history.up.sql + alter table credential_vault_library_hst + rename to credential_vault_generic_library_hst; + + alter table credential_vault_generic_library_hst + rename constraint credential_vault_library_hst_pkey to credential_vault_generic_library_hst_pkey; + + alter table credential_vault_generic_library_hst + rename constraint credential_vault_library_hst_valid_range_excl to credential_vault_generic_library_hst_valid_range_excl; + + comment on table credential_vault_generic_library_hst is + 'credential_vault_generic_library_hst is a history table where each row contains the values from ' + 'a row in the credential_vault_library table during the time range in the valid_range column.'; + + -- Updates the function defined in 71/16_recording_dynamic_credential.up.sql + -- Updated in 100/01_credential_vault_ldap_library.up.sql + drop trigger insert_recording_dynamic_credentials on recording_session; + drop function insert_recording_dynamic_credentials(); + create function insert_recording_dynamic_credentials() returns trigger + as $$ + begin + with + session_recording(session_id, recording_id) as ( + select session_id, public_id + from recording_session + where session_id = new.session_id + ), + session_dynamic_creds(library_id, purpose, recording_id) as ( + select library_id, credential_purpose, recording_id + from session_credential_dynamic + join session_recording using (session_id) + ), + library_history(public_id, store_id, library_hst_id, valid_range) as ( + select public_id, store_id, history_id, valid_range + from credential_vault_generic_library_hst + union + select public_id, store_id, history_id, valid_range + from credential_vault_ssh_cert_library_hst + ), + final(recording_id, library_id, store_id, library_hst_id, store_hst_id, cred_purpose) as ( + select sdc.recording_id, lib.public_id, lib.store_id, lib.library_hst_id, store_hst.history_id, sdc.purpose + from library_history as lib + join credential_vault_store_hst as store_hst on lib.store_id = store_hst.public_id + and store_hst.valid_range @> current_timestamp + join session_dynamic_creds as sdc on lib.public_id = sdc.library_id + where lib.public_id in (select library_id from session_dynamic_creds) + and lib.valid_range @> current_timestamp + ) + insert into recording_dynamic_credential + (recording_id, credential_vault_store_hst_id, credential_library_hst_id, credential_purpose) + select recording_id, store_hst_id, library_hst_id, cred_purpose + from final; + return new; + end; + $$ language plpgsql; + comment on function insert_recording_dynamic_credentials is + 'insert_recording_dynamic_credentials is an after insert trigger for the recording_session table.'; + + create trigger insert_recording_dynamic_credentials after insert on recording_session + for each row execute procedure insert_recording_dynamic_credentials(); + + + -- Deleted table. + -- Renames table defined in 81/01_deleted_tables_and_triggers.up.sql + alter table credential_vault_library_deleted + rename to credential_vault_generic_library_deleted; + + alter table credential_vault_generic_library_deleted + rename constraint credential_vault_library_deleted_pkey to credential_vault_generic_library_deleted_pkey; + + alter index credential_vault_library_deleted_delete_time_idx + rename to credential_vault_generic_library_deleted_delete_time_idx; + + comment on table credential_vault_generic_library_deleted is + 'credential_vault_generic_library_deleted holds the ID and delete_time of every deleted Vault credential library. ' + 'It is automatically trimmed of records older than 30 days by a job.'; + + + -- Mapping override base table. + -- Note that table/constraint/trigger names get very long in the mapping + -- overrides section, and Postgres limits us to 64-1 chars by default, so we + -- may have to restrict our naming. + -- Renames table defined in 22/03_vault_library_map.up.sql + alter table credential_vault_library_mapping_override + rename to credential_vault_generic_library_mapping_override; + + alter table credential_vault_generic_library_mapping_override + rename constraint credential_vault_library_mapping_override_pkey to credential_vault_generic_library_mapping_override_pkey; + + alter table credential_vault_generic_library_mapping_override + rename constraint credential_vault_library_fkey to credential_vault_generic_library_fkey; + + comment on table credential_vault_generic_library_mapping_override is + 'credential_vault_generic_library_mapping_override is a base table for the vault generic library mapping override type. ' + 'Each row is owned by a single vault generic library and maps 1-to-1 to a row in one of the vault generic library mapping override subtype tables.'; + + -- These triggers are recreated on each of the tables' specific sections below. + -- Renames and updates function defined in 22/03_vault_library_map.up.sql + drop trigger insert_credential_vault_library_mapping_override_subtype on credential_vault_library_username_password_mapping_override; + drop trigger insert_credential_vault_library_mapping_override_subtype on credential_vault_library_username_password_domain_mapping_ovrd; + drop trigger insert_credential_vault_library_mapping_override_subtype on credential_vault_library_ssh_private_key_mapping_override; + drop function insert_credential_vault_library_mapping_override_subtype; + create function insert_credential_vault_generic_library_mapping_override_subtyp() returns trigger + as $$ + begin + insert into credential_vault_generic_library_mapping_override + (library_id) + values + (new.library_id); + return new; + end; + $$ language plpgsql; + comment on function insert_credential_vault_generic_library_mapping_override_subtyp() is + 'insert_credential_vault_generic_library_mapping_override_subtyp() is a ' + 'before insert trigger function that inserts rows from subtype tables into ' + 'credential_vault_generic_library_mapping_override. It must be used on all ' + 'Vault generic credential library mapping subtype tables.'; + + -- These triggers are recreated on each of the tables' specific sections below. + -- Renames and updates function defined in 22/03_vault_library_map.up.sql + drop trigger delete_credential_vault_library_mapping_override_subtype on credential_vault_library_username_password_mapping_override; + drop trigger delete_credential_vault_library_mapping_override_subtype on credential_vault_library_username_password_domain_mapping_ovrd; + drop trigger delete_credential_vault_library_mapping_override_subtype on credential_vault_library_ssh_private_key_mapping_override; + drop function delete_credential_vault_library_mapping_override_subtype; + create function delete_credential_vault_generic_library_mapping_override_subtyp() returns trigger + as $$ + begin + delete from credential_vault_generic_library_mapping_override + where library_id = old.library_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_credential_vault_generic_library_mapping_override_subtyp() is + 'delete_credential_vault_generic_library_mapping_override_subtyp() is a ' + 'before insert trigger function that deletes rows from ' + 'credential_vault_generic_library_mapping_override when they''re deleted ' + 'on the subtype tables. It must be used on all Vault generic credential ' + 'library mapping subtype tables.'; + + + -- Mapping override: SSH PK table. + -- Renames table defined in 39/02_vault_ssh_private_key_override.up.sql + alter table credential_vault_library_ssh_private_key_mapping_override + rename to credential_vault_generic_library_ssh_private_key_mapping_ovrd; + + alter table credential_vault_generic_library_ssh_private_key_mapping_ovrd + rename constraint credential_vault_library_ssh_private_key_mapping_override_pkey to credential_vault_generic_library_ssh_priv_key_mapping_ovrd_pkey; + + alter table credential_vault_generic_library_ssh_private_key_mapping_ovrd + rename constraint credential_vault_library_mapping_override_fkey to credential_vault_generic_library_mapping_override_fkey; + + alter table credential_vault_generic_library_ssh_private_key_mapping_ovrd + rename constraint credential_vault_library_fkey to credential_vault_generic_library_fkey; + + comment on table credential_vault_generic_library_ssh_private_key_mapping_ovrd is + 'credential_vault_generic_library_ssh_private_key_mapping_ovrd is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a ssh private key credential type ' + 'for a vault generic credential library.'; + + create trigger insert_credential_vault_generic_library_mapping_override_subtyp before insert on credential_vault_generic_library_ssh_private_key_mapping_ovrd + for each row execute procedure insert_credential_vault_generic_library_mapping_override_subtyp(); + + create trigger delete_credential_vault_generic_library_mapping_override_subtyp after delete on credential_vault_generic_library_ssh_private_key_mapping_ovrd + for each row execute procedure delete_credential_vault_generic_library_mapping_override_subtyp(); + + + -- Mapping override: Username & Password table. + -- Renames table in 36/01_vault_library_map_username_password.up.sql + alter table credential_vault_library_username_password_mapping_override + rename to credential_vault_generic_library_username_password_mapping_ovrd; + + alter table credential_vault_generic_library_username_password_mapping_ovrd + rename constraint credential_vault_library_user_password_mapping_override_pkey to credential_vault_generic_library_usern_pass_mapping_ovrd_pkey; + + alter table credential_vault_generic_library_username_password_mapping_ovrd + rename constraint credential_vault_library_fkey to credential_vault_generic_library_fkey; + + alter table credential_vault_generic_library_username_password_mapping_ovrd + rename constraint credential_vault_library_mapping_override_fkey to credential_vault_generic_library_mapping_override_fkey; + + comment on table credential_vault_generic_library_username_password_mapping_ovrd is + 'credential_vault_generic_library_username_password_mapping_ovrd is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a username password credential type ' + 'for a vault generic credential library.'; + + create trigger insert_credential_vault_generic_library_mapping_override_subtyp before insert on credential_vault_generic_library_username_password_mapping_ovrd + for each row execute procedure insert_credential_vault_generic_library_mapping_override_subtyp(); + + create trigger delete_credential_vault_generic_library_mapping_override_subtyp after delete on credential_vault_generic_library_username_password_mapping_ovrd + for each row execute procedure delete_credential_vault_generic_library_mapping_override_subtyp(); + + + -- Mapping override: Username, Password & Domain table. + -- Renames table defined in 98/02_username_password_domain_vault.up.sql + alter table credential_vault_library_username_password_domain_mapping_ovrd + rename to credential_vault_generic_library_usern_pass_domain_mapping_ovrd; + + alter table credential_vault_generic_library_usern_pass_domain_mapping_ovrd + rename constraint credential_vault_library_username_password_domain_mapping__pkey to credential_vault_generic_library_upd_mapping_ovrd_pkey; + + alter table credential_vault_generic_library_usern_pass_domain_mapping_ovrd + rename constraint credential_vault_library_fkey to credential_vault_generic_library_fkey; + + alter table credential_vault_generic_library_usern_pass_domain_mapping_ovrd + rename constraint credential_vault_library_mapping_override_fkey to credential_vault_generic_library_mapping_override_fkey; + + comment on table credential_vault_generic_library_usern_pass_domain_mapping_ovrd is + 'credential_vault_generic_library_usern_pass_domain_mapping_ovrd is a table ' + 'where each row represents a mapping that overrides the default mapping ' + 'from a generic vault secret to a user password domain credential type ' + 'for a vault generic credential library.'; + + create trigger insert_credential_vault_generic_library_mapping_override_subtyp before insert on credential_vault_generic_library_usern_pass_domain_mapping_ovrd + for each row execute procedure insert_credential_vault_generic_library_mapping_override_subtyp(); + + create trigger delete_credential_vault_generic_library_mapping_override_subtyp after delete on credential_vault_generic_library_usern_pass_domain_mapping_ovrd + for each row execute procedure delete_credential_vault_generic_library_mapping_override_subtyp(); + + + -- Views. Postgres already automatically updated them to reference the renamed + -- tables, however these should still be explictly in the migration. + -- > whx_credential_dimension_source + -- > credential_vault_library_issue_credentials + -- > credential_vault_library_list_lookup: Renamed to credential_vault_generic_library_list_lookup. + -- > credential_vault_library_hst_aggregate: Renamed to credential_vault_generic_library_hst_aggregate. + + -- Replaces view defined in 98/04_rdp_targets.up.sql + -- Replaced in 100/01_credential_vault_ldap_library.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_generic_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, + case + when tt.type = 'tcp' then 'tcp target' + when tt.type = 'ssh' then 'ssh target' + when tt.type = 'rdp' then 'rdp target' + else 'Unknown' + end 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_all_subtypes 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 defined in 98/02_username_password_domain_vault.up.sql + -- Replaced in 100/01_credential_vault_ldap_library.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_generic_library_username_password_mapping_ovrd + ), + 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_generic_library_ssh_private_key_mapping_ovrd + ), + password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd + ) + 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, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute) + as password_attribute, + pd.domain_attribute as domain_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 + null as additional_valid_principals + from credential_vault_generic_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 + left join password_domain_override pd + on library.public_id = pd.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 domain_attribute, + null as private_key_attribute, + null as private_key_passphrase_attribute, + 'ssh-signed-cert' as cred_lib_type, -- used to switch on + additional_valid_principals as additional_valid_principals + 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.'; + + -- Replaces and renames view defined in 98/02_username_password_domain_vault.up.sql. + alter view credential_vault_library_list_lookup + rename to credential_vault_generic_library_list_lookup; + drop view credential_vault_generic_library_list_lookup; + create view credential_vault_generic_library_list_lookup 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_generic_library_username_password_mapping_ovrd + ), + 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_generic_library_ssh_private_key_mapping_ovrd + ), + password_domain_override (library_id, username_attribute, password_attribute, domain_attribute) as ( + select library_id, + nullif(username_attribute, wt_to_sentinel('no override')), + nullif(password_attribute, wt_to_sentinel('no override')), + nullif(domain_attribute, wt_to_sentinel('no override')) + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd + ) + 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, + coalesce(upasso.username_attribute, sshpk.username_attribute, pd.username_attribute) + as username_attribute, + coalesce(upasso.password_attribute, pd.password_attribute) + as password_attribute, + sshpk.private_key_attribute as private_key_attribute, + sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute, + pd.domain_attribute as domain_attribute + from credential_vault_generic_library library + 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 + left join password_domain_override pd + on library.public_id = pd.library_id; + comment on view credential_vault_generic_library_list_lookup is + 'credential_vault_generic_library_list_lookup is a view where each row contains a Vault generic credential library and any of library''s credential mapping overrides. ' + 'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.'; + + -- Replaces and renames view defined in 71/17_credential_history_views.up.sql. + alter view credential_vault_library_hst_aggregate + rename to credential_vault_generic_library_hst_aggregate; + drop view credential_vault_generic_library_hst_aggregate; + create view credential_vault_generic_library_hst_aggregate as + select + rdc.recording_id, + vl.public_id, + vl.name, + vl.description, + vl.vault_path, + vl.http_method, + vl.http_request_body, + vl.credential_type, + vsh.public_id as store_public_id, + vsh.project_id as store_project_id, + vsh.name as store_name, + vsh.description as store_description, + vsh.vault_address as store_vault_address, + vsh.namespace as store_namespace, + vsh.tls_server_name as store_tls_server_name, + vsh.tls_skip_verify as store_tls_skip_verify, + vsh.worker_filter as store_worker_filter, + string_agg(distinct rdc.credential_purpose, '|') as purposes + from credential_vault_generic_library_hst as vl + left join recording_dynamic_credential as rdc on vl.history_id = rdc.credential_library_hst_id + join credential_vault_store_hst as vsh on rdc.credential_vault_store_hst_id = vsh.history_id + group by vl.history_id, rdc.recording_id, vsh.history_id; + comment on view credential_vault_generic_library_hst_aggregate is + 'credential_vault_generic_library_hst_aggregate contains Vault generic credential library history data along with its store and purpose data.'; + + + -- Renaming is complete. We can now redefine credential_vault_library and + -- rewire all the Vault subtype tables into this redefined table. + + -- credential_vault_store doesn't have a unique constraint on (project_id, + -- public_id) fields. We'll need this to attach a FK to them later. There + -- should be no side-effects from this given that: + -- > public_id is a PK (already globally unique) and, + -- > credential_vault_store is a subtype table of credential_store, which + -- does have a (project_id, public_id) uniqueness constraint. + alter table credential_vault_store + add constraint credential_vault_store_project_id_public_id_uq + unique(project_id, public_id); + + -- This statement defines the basic fields and pulls all the data from + -- credential_library. We can do this as a blanket select * statement because + -- only Vault-subtype credential libraries exist at this point. + -- + -- We want to insert the data before any constraints are set because + -- credential_library has had a few revisions and while we have also updated + -- the data alongside those table revisions, success on an insert with + -- historical data isn't guaranteed. Additionally, we also want to do this + -- before any triggers are put in place. + create table credential_vault_library as + select * from credential_library; + + alter table credential_vault_library + add primary key (public_id), + alter column store_id set not null, + alter column credential_type set not null, + alter column credential_type set default 'unspecified', + add constraint credential_type_enm_fkey + foreign key (credential_type) + references credential_type_enm (name) + on delete restrict + on update cascade, + alter column project_id set not null, + alter column create_time set not null, + alter column update_time set not null, + add constraint credential_library_fkey + foreign key (public_id, store_id, credential_type, project_id) + references credential_library (public_id, store_id, credential_type, project_id) + on delete cascade + on update cascade, + add constraint credential_vault_store_fkey + foreign key (project_id, store_id) + references credential_vault_store (project_id, public_id) + on delete cascade + on update cascade, + add constraint credential_vault_library_project_id_public_id_uq + unique(project_id, public_id), + add constraint credential_vault_library_project_store_public_ids_credtype_uq + unique(project_id, store_id, public_id, credential_type); + comment on table credential_vault_library is + 'credential_vault_library is a base table for Vault credential library ' + 'subtypes and a child table of credential_vault_store. Each row maps 1-to-1 ' + 'to a row in one of the Vault credential library subtype tables.'; + + create trigger immutable_columns before update on credential_vault_library + for each row execute function immutable_columns('public_id', 'store_id', 'project_id', 'credential_type'); + create trigger insert_credential_library_subtype before insert on credential_vault_library + for each row execute procedure insert_credential_library_subtype(); + create trigger delete_credential_library_subtype after delete on credential_vault_library + for each row execute procedure delete_credential_library_subtype(); + create trigger update_credential_library_table_update_time before update on credential_vault_library + for each row execute procedure update_credential_library_table_update_time(); + + create index credential_vault_library_create_time_public_id_idx + on credential_vault_library (create_time desc, public_id desc); + create index credential_vault_library_update_time_public_id_idx + on credential_vault_library (update_time desc, public_id desc); + + create or replace function insert_credential_vault_library_subtype() returns trigger + as $$ + begin + select project_id into new.project_id + from credential_store + where credential_store.public_id = new.store_id; + + insert into credential_vault_library + (public_id, store_id, project_id, credential_type, create_time) + values + (new.public_id, new.store_id, new.project_id, new.credential_type, new.create_time); + return new; + end; + $$ language plpgsql; + comment on function insert_credential_vault_library_subtype() is + 'insert_credential_vault_library_subtype is a before insert trigger ' + 'function that inserts rows from subtype tables into ' + 'credential_vault_library. It must be used on all Vault credential library ' + 'subtype tables.'; + + create function delete_credential_vault_library_subtype() returns trigger + as $$ + begin + delete from credential_vault_library + where public_id = old.public_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_credential_vault_library_subtype() is + 'delete_credential_vault_library_subtype is an after delete trigger ' + 'function that deletes rows from credential_vault_library when they''re ' + 'deleted on the subtype tables. It must be used on all Vault credential ' + 'library subtype tables.'; + + create function update_credential_vault_library_table_update_time() returns trigger + as $$ + begin + update credential_vault_library set update_time = now() where public_id = new.public_id; + return new; + end; + $$ language plpgsql; + comment on function update_credential_vault_library_table_update_time() is + 'update_credential_vault_library_table_update_time is a before update ' + 'trigger function that updates the update_time column on ' + 'credential_vault_library. It must be used on all Vault credential library ' + 'subtype tables.'; + + + -- We still have some entities on the Vault subtype tables that reference + -- credential_library when they should now reference credential_vault_library. + alter table credential_vault_generic_library + drop constraint credential_library_fkey, + add constraint credential_vault_library_fkey + foreign key (project_id, store_id, public_id, credential_type) + references credential_vault_library (project_id, store_id, public_id, credential_type) + on delete cascade + on update cascade; + + -- Updates table defined in 63/01_credential_vault_ssh_cert_library.up.sql + alter table credential_vault_ssh_cert_library + drop constraint credential_library_fkey, + add constraint credential_vault_library_fkey + foreign key (project_id, store_id, public_id, credential_type) + references credential_vault_library (project_id, store_id, public_id, credential_type) + on delete cascade + on update cascade; + + drop trigger insert_credential_library_subtype on credential_vault_generic_library; + create trigger insert_credential_vault_library_subtype before insert on credential_vault_generic_library + for each row execute procedure insert_credential_vault_library_subtype(); + + drop trigger delete_credential_library_subtype on credential_vault_generic_library; + create trigger delete_credential_vault_library_subtype after delete on credential_vault_generic_library + for each row execute procedure delete_credential_vault_library_subtype(); + + drop trigger update_credential_library_table_update_time on credential_vault_generic_library; + create trigger update_credential_vault_library_table_update_time before update on credential_vault_generic_library + for each row execute procedure update_credential_vault_library_table_update_time(); + + drop trigger insert_credential_library_subtype on credential_vault_ssh_cert_library; + create trigger insert_credential_vault_library_subtype before insert on credential_vault_ssh_cert_library + for each row execute procedure insert_credential_vault_library_subtype(); + + drop trigger delete_credential_library_subtype on credential_vault_ssh_cert_library; + create trigger delete_credential_vault_library_subtype after delete on credential_vault_ssh_cert_library + for each row execute procedure delete_credential_vault_library_subtype(); + + drop trigger update_credential_library_table_update_time on credential_vault_ssh_cert_library; + create trigger update_credential_vault_library_table_update_time before update on credential_vault_ssh_cert_library + for each row execute procedure update_credential_vault_library_table_update_time(); + + -- Finally, we can correct the restriction in credential_vault_credential. + -- credential_vault_library is now a superset of + -- credential_vault_generic_library, so this shouldn't have side-effects. + -- Updates the table defined in 10/04_vault_credential.up.sql + alter table credential_vault_credential + drop constraint credential_vault_library_fkey, + add constraint credential_vault_library_fkey + foreign key (library_id) + references credential_vault_library (public_id) + on delete set null + on update cascade; +commit; diff --git a/internal/db/schema/migrations/oss/postgres/99/README.md b/internal/db/schema/migrations/oss/postgres/99/README.md new file mode 100644 index 000000000..6873d04d3 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/99/README.md @@ -0,0 +1,50 @@ +# Vault Generic Credential Library DB Refactor +Historically, the "Vault generic" credential library was named just "Vault" +credential library. Over the course of Boundary's development, it was renamed to +"Vault generic" to better match its abilities and to conceptually separate it +from other Vault-specific credential libraries, however this nomenclature change +was not applied to database (or domain) entities. This migration applies this +nomenclature change at the database level, renaming all related entities to +match the new "Vault generic" name. + +Currently, these database entities are either named or prefixed with +`credential_vault_library`. This migration changes them to be named or prefixed +with `credential_vault_generic_library`. + +Additionally, `credential_vault_library` takes on a new meaning and is defined +as a new base table that holds information about all *Vault-specific* credential +libraries. `credential_vault_library` is a subtype table of +`credential_library`: + +``` +credential_library |> credential_vault_library |> credential_vault_generic_library + | |> credential_vault_ssh_cred_library + | |> any future Vault-specific credential libraries + | +In the future, we could also have: + |> base table for a non-Vault system's credlibs |> credential library subtype tables +``` + +While the history table for Vault generic credential libraries is renamed to +`credential_vault_generic_library_hst`, the overall history design remains +unchanged. + +This pattern is being established due to the need to differentiate between the +systems that Boundary's credential libraries integrate with, for database +integrity purposes: + +`credential_dynamic` is a base table that represents all renewable/revokable +dynamic credentials. `credential_vault_credential` is a subtype table that +represents renewable/revokable dynamic credentials (Vault leases). Given the +specificity of this design, these leases must be obtained by a Vault credential +library. + +To enforce this, the `library_id` field in `credential_vault_credential` has a +foreign key constraint on `credential_vault_library(public_id)`, meaning only +Vault leases obtained by a Vault generic credential library can ever be inserted +into this table and therefore managed by Boundary. + +With the future introduction of new Vault credential libraries that can also +issue Vault leases, this constraint becomes too restrictive and the design needs +to be extended. By redefining `credential_vault_library` as described above, +this refactor provides the backing for this to be done. diff --git a/internal/db/schema/migrations/oss/postgres_99_01_test.go b/internal/db/schema/migrations/oss/postgres_99_01_test.go new file mode 100644 index 000000000..e3e80d22d --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres_99_01_test.go @@ -0,0 +1,1131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oss_test + +import ( + "database/sql" + "fmt" + "math/rand/v2" + "strconv" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/testing/dbtest" + "github.com/hashicorp/go-secure-stdlib/base62" + "github.com/stretchr/testify/require" +) + +// TestVaultCredLibRefactor tests the credential_vault_library refactor. It +// inserts state in the changed tables prior to the refactor migration, executes +// the refactor migration and then asserts that this state is still correct. +// +// For structural tests, see +// sqltest/tests/credential/vault/credential_vault_library_refactor.sql. +// +// For context, see 99/README.md. +func TestVaultCredLibRefactor(t *testing.T) { + t.Parallel() + require := require.New(t) + + c := testVaultCredLibRefactorDbSetupCounts{ + orgs: 2, + projectsPerOrg: 4, + vaultGenericCredLibs: 20, + vaultGenericCredLibsDeleted: 5, + vaultSshCertCredLibs: 20, + vaultSshCertCredLibsDeleted: 5, + } + + dataBeforeMigration, dataAfterMigration := testVaultCredLibRefactorDbSetup(t, c) + require.NotEmpty(dataBeforeMigration) + require.NotEmpty(dataAfterMigration) + + // Validate that `testVaultCredLibRefactorDbSetup` seeded data correctly + // according to the count variables above. + require.Len(dataBeforeMigration.orgs, c.orgs) + require.Len(dataBeforeMigration.projects, c.orgs*c.projectsPerOrg) + require.Len(dataBeforeMigration.credentialStores, c.orgs*c.projectsPerOrg) + require.Len(dataBeforeMigration.credLibBase, (c.orgs*c.projectsPerOrg*c.vaultGenericCredLibs)+(c.orgs*c.projectsPerOrg*c.vaultSshCertCredLibs)) + require.NotEmpty(dataBeforeMigration.credLibHistoryBase) // Some credential libraries have been updated so they'll have more than 1 history entry. + require.Len(dataBeforeMigration.postRefactorVaultCredLibBase, 0) + require.Len(dataBeforeMigration.vaultGenericCredLibs, c.orgs*c.projectsPerOrg*c.vaultGenericCredLibs) + require.Len(dataBeforeMigration.vaultSshCertCredLibs, c.orgs*c.projectsPerOrg*c.vaultSshCertCredLibs) + require.Len(dataBeforeMigration.deletedVaultGenericCredLibs, c.orgs*c.projectsPerOrg*c.vaultGenericCredLibsDeleted) + require.Len(dataBeforeMigration.deletedVaultSshCertCredLibs, c.orgs*c.projectsPerOrg*c.vaultSshCertCredLibsDeleted) + + // Validate migrated data. Given that we asserted the data before migration + // is correctly seeded, we can just assert that post-migration and + // pre-migration elements match. + require.ElementsMatch(dataBeforeMigration.orgs, dataAfterMigration.orgs) + require.ElementsMatch(dataBeforeMigration.projects, dataAfterMigration.projects) + require.ElementsMatch(dataBeforeMigration.credentialStores, dataAfterMigration.credentialStores) + require.ElementsMatch(dataBeforeMigration.credLibBase, dataAfterMigration.credLibBase) + require.ElementsMatch(dataBeforeMigration.credLibHistoryBase, dataAfterMigration.credLibHistoryBase) + require.ElementsMatch(dataBeforeMigration.credLibBase, dataAfterMigration.postRefactorVaultCredLibBase) // We fill new credential_vault_library with data from credential_library. + require.ElementsMatch(dataBeforeMigration.vaultGenericCredLibs, dataAfterMigration.vaultGenericCredLibs) + require.ElementsMatch(dataBeforeMigration.vaultSshCertCredLibs, dataAfterMigration.vaultSshCertCredLibs) + require.ElementsMatch(dataBeforeMigration.deletedVaultGenericCredLibs, dataAfterMigration.deletedVaultGenericCredLibs) + require.ElementsMatch(dataBeforeMigration.deletedVaultSshCertCredLibs, dataAfterMigration.deletedVaultSshCertCredLibs) + + // Finally, check table and view row counts. + require.EqualValues(dataBeforeMigration.rowCounts.credLib, dataAfterMigration.rowCounts.credLib) + require.EqualValues(dataBeforeMigration.rowCounts.credLibHistory, dataAfterMigration.rowCounts.credLibHistory) + require.EqualValues(dataBeforeMigration.rowCounts.credLib, dataAfterMigration.rowCounts.credVaultLibPostRefactor) // We fill new credential_vault_library from credential_library. + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibMappingOvrd, dataAfterMigration.rowCounts.credVaultGenericLibMappingOvrd) + + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLib, dataAfterMigration.rowCounts.credVaultGenericLib) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibHistory, dataAfterMigration.rowCounts.credVaultGenericLibHistory) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibDeleted, dataAfterMigration.rowCounts.credVaultGenericLibDeleted) + + require.EqualValues(dataBeforeMigration.rowCounts.credVaultSshCertLib, dataAfterMigration.rowCounts.credVaultSshCertLib) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultSshCertLibHistory, dataAfterMigration.rowCounts.credVaultSshCertLibHistory) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultSshCertLibDeleted, dataAfterMigration.rowCounts.credVaultSshCertLibDeleted) + + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibMappingOvrdUserPass, dataAfterMigration.rowCounts.credVaultGenericLibMappingOvrdUserPass) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibMappingOvrdUserPassDomain, dataAfterMigration.rowCounts.credVaultGenericLibMappingOvrdUserPassDomain) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibMappingOvrdSshPk, dataAfterMigration.rowCounts.credVaultGenericLibMappingOvrdSshPk) + + require.EqualValues(dataBeforeMigration.rowCounts.credVaultLibIssueCredentials, dataAfterMigration.rowCounts.credVaultLibIssueCredentials) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibListLookup, dataAfterMigration.rowCounts.credVaultGenericLibListLookup) + require.EqualValues(dataBeforeMigration.rowCounts.credVaultGenericLibHistoryAgg, dataAfterMigration.rowCounts.credVaultGenericLibHistoryAgg) + require.EqualValues(dataBeforeMigration.rowCounts.credDimensionSource, dataAfterMigration.rowCounts.credDimensionSource) +} + +// testVaultCredLibRefactorDbSetup sets up a Boundary database running a prior +// migration with credential library data, then migrates to the refactored +// version. Not designed to be called outside of TestVaultCredLibRefactor. +func testVaultCredLibRefactorDbSetup(t testing.TB, counts testVaultCredLibRefactorDbSetupCounts) (beforeMigration, afterMigration dbData) { + require := require.New(t) + + const priorMigration = 98006 + const credlibRefactorMigration = 99001 + + dialect := dbtest.Postgres + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(err) + t.Cleanup(func() { require.NoError(c()) }) + + db, err := common.SqlOpen(dialect, u) + require.NoError(err) + + // Migrate to the prior migration (before the one we want to test). + m, err := schema.NewManager(t.Context(), schema.Dialect(dialect), db, schema.WithEditions( + schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": priorMigration}), + )) + require.NoError(err) + + _, err = m.ApplyMigrations(t.Context()) + require.NoError(err) + + state, err := m.CurrentState(t.Context()) + require.NoError(err) + require.Equal(&schema.State{ + Initialized: true, + Editions: []schema.EditionState{ + { + Name: "oss", + BinarySchemaVersion: priorMigration, + DatabaseSchemaVersion: priorMigration, + DatabaseSchemaState: schema.Equal, + }, + }, + }, state) + + tx, err := db.BeginTx(t.Context(), &sql.TxOptions{}) + require.NoError(err) + require.NotNil(tx) + + // Queries to seed all the required data. + createOrgScopeQuery := ` + insert into iam_scope + (parent_id, type, public_id, name) + values + ('global', 'org', $1, $2); + ` + createProjectScopeQuery := ` + insert into iam_scope + (parent_id, type, public_id, name) + values + ( $1, 'project', $2, $3); + ` + createCredStoreQuery := ` + insert into credential_vault_store + (public_id, project_id, name, vault_address) + values + ( $1, $2, $3, $4); + ` + createVaultGenericCredLibQuery := ` + insert into credential_vault_library + (public_id, store_id, project_id, name, description, credential_type, vault_path, http_method, http_request_body) + values + ( $1, $2, $3, $4, $5, $6, $7, $8, $9); + ` + updateVaultGenericCredLibQuery := ` + update credential_vault_library + set name = $1, + description = $2, + vault_path = $3 + where public_id = $4; + ` + deleteVaultGenericCredLibQuery := ` + delete from credential_vault_library + where public_id = $1; + ` + createUpMappingOvrdQuery := ` + insert into credential_vault_library_username_password_mapping_override + (library_id, username_attribute, password_attribute) + values + ( $1, $2, $3); + ` + createUpdMappingOvrdQuery := ` + insert into credential_vault_library_username_password_domain_mapping_ovrd + (library_id, username_attribute, password_attribute, domain_attribute) + values + ( $1, $2, $3, $4); + ` + createSshPkMappingOvrdQuery := ` + insert into credential_vault_library_ssh_private_key_mapping_override + (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) + values + ( $1, $2, $3, $4); + ` + createVaultSshCertCredLibQuery := ` + insert into credential_vault_ssh_cert_library + (public_id, store_id, project_id, name, description, credential_type, vault_path, username, key_type, key_bits, ttl, key_id, critical_options, extensions, additional_valid_principals) + values + ( $1, $2, $3, $4, $5, 'ssh_certificate', $6, $7, $8, $9, $10, $11, $12, $13, $14); + ` + updateVaultSshCertCredLibQuery := ` + update credential_vault_ssh_cert_library + set name = $1, + description = $2, + vault_path = $3, + username = $4 + where public_id = $5; + ` + deleteVaultSshCertCredLibQuery := ` + delete from credential_vault_ssh_cert_library + where public_id = $1; + ` + + execOk := func(res sql.Result, err error) { + // Exec returned no error. + require.NoError(err) + require.NotNil(res) + + // Exec affected at least 1 row. + ra, err := res.RowsAffected() + require.NoError(err) + require.Greater(ra, int64(0)) + } + + // Create org scopes. + for range counts.orgs { + orgId := fmt.Sprintf("o_%s", base62.MustRandom(10)) + orgName := fmt.Sprintf("Test %q Org Scope", orgId) + execOk(tx.ExecContext(t.Context(), createOrgScopeQuery, orgId, orgName)) + + // For each org scope, create `projectsPerOrgCount` project scopes and a + // Vault credential store. + for range counts.projectsPerOrg { + projectId := fmt.Sprintf("p_%s", base62.MustRandom(10)) + projectName := fmt.Sprintf("Test %q Project Scope", projectId) + execOk(tx.ExecContext(t.Context(), createProjectScopeQuery, orgId, projectId, projectName)) + + credStoreId := fmt.Sprintf("csst_%s", base62.MustRandom(10)) + credStoreName := fmt.Sprintf("Test %q Vault Credential Store", credStoreId) + execOk(tx.ExecContext(t.Context(), createCredStoreQuery, credStoreId, projectId, credStoreName, "http://127.0.0.1:8200")) + + // For each credential store, create `vaultGenericCredLibs` Vault + // generic credential libraries with differing configurations and + // mapping overrides. Add `vaultGenericCredLibsDeleted` to account + // for the ones we have to create to delete. + deletedVaultGenericCredLibs := 0 + for range counts.vaultGenericCredLibs + counts.vaultGenericCredLibsDeleted { + vgCredLibId := fmt.Sprintf("clvlt_%s", base62.MustRandom(10)) + vgCredLibName := fmt.Sprintf("Test %q Vault Generic Credential Library", base62.MustRandom(10)) + + var vgCredType string + switch rand.IntN(4) { + case 0: + vgCredType = "unspecified" + case 1: + vgCredType = "username_password" + case 2: + vgCredType = "username_password_domain" + case 3: + vgCredType = "ssh_private_key" + } + + var vgHttpMethod string + var vgHttpReqBody []byte + switch rand.IntN(2) { + case 0: + vgHttpMethod = "GET" + case 1: + vgHttpMethod = "POST" + vgHttpReqBody = []byte(`{"my_custom_request_body":true, "data": "this_is_some_data"}`) + } + + execOk(tx.ExecContext(t.Context(), createVaultGenericCredLibQuery, + vgCredLibId, credStoreId, projectId, vgCredLibName, vgCredLibName, + vgCredType, "/my/vg/vault/path", vgHttpMethod, vgHttpReqBody, + )) + + var vgMappingOverrideQuery string + vgMappingOverrideArgs := []any{vgCredLibId} + switch vgCredType { + case "unspecified": // No mapping override. + case "username_password": + vgMappingOverrideQuery = createUpMappingOvrdQuery + vgMappingOverrideArgs = append(vgMappingOverrideArgs, "username_ovrd", "password_ovrd") + case "username_password_domain": + vgMappingOverrideQuery = createUpdMappingOvrdQuery + vgMappingOverrideArgs = append(vgMappingOverrideArgs, "username_ovrd", "password_ovrd", "domain_ovrd") + case "ssh_private_key": + vgMappingOverrideQuery = createSshPkMappingOvrdQuery + vgMappingOverrideArgs = append(vgMappingOverrideArgs, "username_ovrd", "ssh_pk_ovrd", "ssh_pk_passphrase_ovrd") + } + if vgMappingOverrideQuery != "" { + execOk(tx.ExecContext(t.Context(), vgMappingOverrideQuery, vgMappingOverrideArgs...)) + } + + // Chance of updating the credential library. + switch rand.IntN(10) { + case 0: + execOk(tx.ExecContext(t.Context(), updateVaultGenericCredLibQuery, "Updated"+vgCredLibName, + "Updated"+vgCredLibName, "/my/updated/vault/path", vgCredLibId)) + } + + if counts.vaultGenericCredLibsDeleted > deletedVaultGenericCredLibs { + execOk(tx.ExecContext(t.Context(), deleteVaultGenericCredLibQuery, vgCredLibId)) + deletedVaultGenericCredLibs++ + } + } + + // For each credential store, create `vaultSshCertCredLibs` Vault + // SSH Certificate credential libraries with different + // configurations. Add `vaultSshCertCredLibsDeleted` to account for + // the ones we have to create to delete. + deletedSshCertCredLibs := 0 + for range counts.vaultSshCertCredLibs + counts.vaultSshCertCredLibsDeleted { + vSshCertCredLibId := fmt.Sprintf("clvsclt_%s", base62.MustRandom(10)) + vSshCertCredLibName := fmt.Sprintf("Test %q Vault SSH Certificate Credential Library", base62.MustRandom(10)) + + var keyType string + var keyBits int + switch rand.IntN(3) { + case 0: + keyType = "ed25519" + keyBits = 0 + case 1: + keyType = "ecdsa" + keyBits = 521 + case 2: + keyType = "rsa" + keyBits = 4096 + } + + var ttl string + switch rand.IntN(2) { + case 0: // No TTL. + case 1: + ttl = strconv.FormatInt(rand.Int64N(65536), 10) + } + + var keyId string + switch rand.IntN(2) { + case 0: // No key id. + case 1: + keyId = "my_key_id" + } + + var criticalOptions []byte + switch rand.IntN(2) { + case 0: // No Critical options. + case 1: + criticalOptions = []byte(`critical_option1=yes, critical_option2=alsoyes, critical_option3=yep`) + } + + var extensions []byte + switch rand.IntN(2) { + case 0: // No extensions. + case 1: + extensions = []byte(`extension1=yes, extension2=alsoyes, extension3=yep`) + } + + var additionalPrincipals string + switch rand.IntN(2) { + case 0: // No additional principals. + case 1: + additionalPrincipals = "principal1, principal2, principal3" + } + + execOk(tx.ExecContext(t.Context(), createVaultSshCertCredLibQuery, + vSshCertCredLibId, credStoreId, projectId, vSshCertCredLibName, + vSshCertCredLibName, "/ssh/sign/cert1", "username", keyType, + keyBits, ttl, keyId, criticalOptions, extensions, additionalPrincipals, + )) + require.NoError(err) + + // Chance of updating credential library. + switch rand.IntN(10) { + case 0: + execOk(tx.ExecContext(t.Context(), updateVaultSshCertCredLibQuery, "Updated"+vSshCertCredLibName, + "Updated"+vSshCertCredLibName, "/ssh/issue/updated_cert1", "updated_username", vSshCertCredLibId)) + } + + if counts.vaultSshCertCredLibsDeleted > deletedSshCertCredLibs { + execOk(tx.ExecContext(t.Context(), deleteVaultSshCertCredLibQuery, vSshCertCredLibId)) + deletedSshCertCredLibs++ + } + } + } + } + err = tx.Commit() + require.NoError(err) + + // Get a snapshot of what the data looks like before the migration. + beforeMigration = testBuildTestVaultCredLibRefactorDbData(t, db, false) + + // Now we're ready for the migration we want to test. + m, err = schema.NewManager(t.Context(), schema.Dialect(dialect), db, schema.WithEditions( + schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": credlibRefactorMigration}), + )) + require.NoError(err) + + _, err = m.ApplyMigrations(t.Context()) + require.NoError(err) + + state, err = m.CurrentState(t.Context()) + require.NoError(err) + require.Equal(&schema.State{ + Initialized: true, + Editions: []schema.EditionState{ + { + Name: "oss", + BinarySchemaVersion: credlibRefactorMigration, + DatabaseSchemaVersion: credlibRefactorMigration, + DatabaseSchemaState: schema.Equal, + }, + }, + }, state) + + // Get a snapshot of the data after the migration. + afterMigration = testBuildTestVaultCredLibRefactorDbData(t, db, true) + + return beforeMigration, afterMigration +} + +// testBuildTestVaultCredLibRefactorDbData will build a 'dbData' object from +// database queries. Accepts a boolean that denotes if any given call is +// post-refactor. 'false' means assume before refactor migration; 'true' means +// assume after refactor migration. This boolean controls table names on various +// queries that retrieve data. +func testBuildTestVaultCredLibRefactorDbData(t testing.TB, db *sql.DB, afterRefactor bool) dbData { + require := require.New(t) + + queryContext := func(tx *sql.Tx, query string, callbackFn func(scanFn func(dest ...any) error)) { + rows, err := tx.QueryContext(t.Context(), query) + require.NoError(err) + require.NotNil(rows) + + for rows.Next() { + callbackFn(rows.Scan) + } + require.NoError(rows.Err()) + } + + data := dbData{ + orgs: make([]orgProj, 0), + projects: make([]orgProj, 0), + credentialStores: make([]vaultCredStore, 0), + credLibBase: make([]credLibBase, 0), + credLibHistoryBase: make([]credLibHistoryBase, 0), + postRefactorVaultCredLibBase: make([]credLibBase, 0), + vaultGenericCredLibs: make([]vaultGenericCredLib, 0), + vaultSshCertCredLibs: make([]vaultSshCertCredLib, 0), + deletedVaultGenericCredLibs: make([]credLibDeleted, 0), + deletedVaultSshCertCredLibs: make([]credLibDeleted, 0), + rowCounts: rowCounts{}, + } + + tx, err := db.BeginTx(t.Context(), &sql.TxOptions{}) + require.NoError(err) + require.NotNil(tx) + defer func() { _ = tx.Rollback() }() + + // Get org and project scopes. + queryContext(tx, "select * from iam_scope", func(scanFn func(dest ...any) error) { + var publicId, name, typ, description, parentId, primaryAuthMethodId *string + var createTime, updateTime *time.Time + var version *int + require.NoError(scanFn(&publicId, &createTime, &updateTime, &name, + &typ, &description, &parentId, &version, &primaryAuthMethodId), + ) + + op := orgProj{ + publicId: testDeref(t, publicId), + name: testDeref(t, name), + description: testDeref(t, description), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + typ: testDeref(t, typ), + parentId: testDeref(t, parentId), + version: testDeref(t, version), + } + switch op.typ { + case "global": + case "org": + data.orgs = append(data.orgs, op) + case "project": + data.projects = append(data.projects, op) + default: + require.Failf("failed to get org/project scopes", "unknown scope type %s", op.typ) + } + }) + + // Get credential stores. + queryContext(tx, "select * from credential_vault_store", func(scanFn func(dest ...any) error) { + var publicId, projectId, name, description, vaultAddr, namespace, + tlsServerName, workerFilter *string + var createTime, updateTime, deleteTime *time.Time + var version *int + var caCert *[]byte + var tlsSkipVerify *bool + + require.NoError(scanFn(&publicId, &projectId, &name, &description, + &createTime, &updateTime, &deleteTime, &version, &vaultAddr, &namespace, + &caCert, &tlsServerName, &tlsSkipVerify, &workerFilter), + ) + + data.credentialStores = append(data.credentialStores, vaultCredStore{ + publicId: testDeref(t, publicId), + projectId: testDeref(t, projectId), + name: testDeref(t, name), + description: testDeref(t, description), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + deleteTime: testDeref(t, deleteTime), + version: testDeref(t, version), + vaultAddr: testDeref(t, vaultAddr), + namespace: testDeref(t, namespace), + caCert: testDeref(t, caCert), + tlsServerName: testDeref(t, tlsServerName), + tlsSkipVerify: testDeref(t, tlsSkipVerify), + workerFilter: testDeref(t, workerFilter), + }) + }) + + // Get credential library base table. + queryContext(tx, "select * from credential_library", func(scanFn func(dest ...any) error) { + var publicId, storeId, credType, projectId *string + var createTime, updateTime *time.Time + require.NoError(scanFn(&publicId, &storeId, &credType, &projectId, &createTime, &updateTime)) + + data.credLibBase = append(data.credLibBase, credLibBase{ + publicId: testDeref(t, publicId), + storeId: testDeref(t, storeId), + credType: testDeref(t, credType), + projectId: testDeref(t, projectId), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + }) + }) + + // Get credential library base table if we're post-refactor. + if afterRefactor { + queryContext(tx, "select * from credential_vault_library", func(scanFn func(dest ...any) error) { + var publicId, storeId, credType, projectId *string + var createTime, updateTime *time.Time + require.NoError(scanFn(&publicId, &storeId, &credType, &projectId, &createTime, &updateTime)) + + data.postRefactorVaultCredLibBase = append(data.postRefactorVaultCredLibBase, credLibBase{ + publicId: testDeref(t, publicId), + storeId: testDeref(t, storeId), + credType: testDeref(t, credType), + projectId: testDeref(t, projectId), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + }) + }) + } + + // Get credential library history base table. + queryContext(tx, "select * from credential_library_history_base", func(scanFn func(dest ...any) error) { + var historyId *string + require.NoError(scanFn(&historyId)) + data.credLibHistoryBase = append(data.credLibHistoryBase, credLibHistoryBase{testDeref(t, historyId)}) + }) + + // Get Vault generic credential libraries. + vaultGenericCredLibTableName := "credential_vault_library" + if afterRefactor { + vaultGenericCredLibTableName = "credential_vault_generic_library" + } + queryContext(tx, fmt.Sprintf("select * from %s", vaultGenericCredLibTableName), func(scanFn func(dest ...any) error) { + var publicId, storeId, name, description, vaultPath, httpMethod, + credType, projectId *string + var createTime, updateTime *time.Time + var version *int + var httpReqBody *[]byte + + require.NoError(scanFn(&publicId, &storeId, &name, &description, + &createTime, &updateTime, &version, &vaultPath, &httpMethod, + &httpReqBody, &credType, &projectId), + ) + + data.vaultGenericCredLibs = append(data.vaultGenericCredLibs, vaultGenericCredLib{ + publicId: testDeref(t, publicId), + storeId: testDeref(t, storeId), + name: testDeref(t, name), + description: testDeref(t, description), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + version: testDeref(t, version), + vaultPath: testDeref(t, vaultPath), + httpMethod: testDeref(t, httpMethod), + httpReqBody: testDeref(t, httpReqBody), + credType: testDeref(t, credType), + projectId: testDeref(t, projectId), + + mappingOverride: vaultGenericCredLibMappingOvrd{}, // Fetch below. + history: make([]vaultGenericCredLibHistory, 0), // Fetch below. + }) + }) + + // Get mapping overrides for Vault generic credential libraries. + upMappingOvrdTableName := "credential_vault_library_username_password_mapping_override" + updMappingOvrdTableName := "credential_vault_library_username_password_domain_mapping_ovrd" + sshPkMappingOvrdTableName := "credential_vault_library_ssh_private_key_mapping_override" + if afterRefactor { + upMappingOvrdTableName = "credential_vault_generic_library_username_password_mapping_ovrd" + updMappingOvrdTableName = "credential_vault_generic_library_usern_pass_domain_mapping_ovrd" + sshPkMappingOvrdTableName = "credential_vault_generic_library_ssh_private_key_mapping_ovrd" + } + for _, cl := range data.vaultGenericCredLibs { + queryFmt := "select * from %s where library_id = '%s'" + getMappingOvrd := false + switch cl.credType { + case "unspecified": + case "username_password": + queryFmt = fmt.Sprintf(queryFmt, upMappingOvrdTableName, cl.publicId) + getMappingOvrd = true + case "username_password_domain": + queryFmt = fmt.Sprintf(queryFmt, updMappingOvrdTableName, cl.publicId) + getMappingOvrd = true + case "ssh_private_key": + queryFmt = fmt.Sprintf(queryFmt, sshPkMappingOvrdTableName, cl.publicId) + getMappingOvrd = true + default: + require.Failf("failed to get vault generic mapping overrides", "unknown credential type %s", cl.credType) + } + if getMappingOvrd { + queryContext(tx, queryFmt, func(scanFn func(dest ...any) error) { + var libraryId, usernameAttr, passwordAttr, domainAttr, pkAttr, + pkPassphraseAttr *string + + switch cl.credType { + case "username_password": + require.NoError(scanFn(&libraryId, &usernameAttr, &passwordAttr)) + case "username_password_domain": + require.NoError(scanFn(&libraryId, &usernameAttr, &passwordAttr, &domainAttr)) + case "ssh_private_key": + require.NoError(scanFn(&libraryId, &usernameAttr, &pkAttr, &pkPassphraseAttr)) + } + + cl.mappingOverride = vaultGenericCredLibMappingOvrd{ + libraryId: testDeref(t, libraryId), + usernameAttr: testDeref(t, usernameAttr), + passwordAttr: testDeref(t, passwordAttr), + domainAttr: testDeref(t, domainAttr), + pkAttr: testDeref(t, pkAttr), + pkPassphraseAttr: testDeref(t, pkPassphraseAttr), + } + }) + } + } + + // Get history for Vault generic credential libraries. + for _, cl := range data.vaultGenericCredLibs { + hstTableName := "credential_vault_library_hst" + if afterRefactor { + hstTableName = "credential_vault_generic_library_hst" + } + + query := fmt.Sprintf("select * from %s where public_id = '%s'", hstTableName, cl.publicId) + queryContext(tx, query, func(scanFn func(dest ...any) error) { + var publicId, name, description, projectId, storeId, vaultPath, + httpMethod, credType, historyId, validRange *string + var httpReqBody *[]byte + + require.NoError(scanFn(&publicId, &name, &description, &projectId, + &storeId, &vaultPath, &httpMethod, &httpReqBody, &credType, + &historyId, &validRange), + ) + + cl.history = append(cl.history, vaultGenericCredLibHistory{ + historyId: testDeref(t, historyId), + validRange: testDeref(t, validRange), + cl: vaultGenericCredLib{ + publicId: testDeref(t, publicId), + name: testDeref(t, name), + description: testDeref(t, description), + projectId: testDeref(t, projectId), + storeId: testDeref(t, storeId), + vaultPath: testDeref(t, vaultPath), + httpMethod: testDeref(t, httpMethod), + httpReqBody: testDeref(t, httpReqBody), + credType: testDeref(t, credType), + }, + }) + }) + } + + // Get Vault SSH certificate credential libraries. + queryContext(tx, "select * from credential_vault_ssh_cert_library", func(scanFn func(dest ...any) error) { + var publicId, storeId, name, description, vaultPath, username, keyType, + ttl, keyId, credType, projectId, additionalPrincipals *string + var createTime, updateTime *time.Time + var version, keyBits *int + var criticalOptions, extensions *[]byte + + require.NoError(scanFn(&publicId, &storeId, &name, &description, + &createTime, &updateTime, &version, &vaultPath, &username, &keyType, + &keyBits, &ttl, &keyId, &criticalOptions, &extensions, &credType, + &projectId, &additionalPrincipals), + ) + + data.vaultSshCertCredLibs = append(data.vaultSshCertCredLibs, vaultSshCertCredLib{ + publicId: testDeref(t, publicId), + storeId: testDeref(t, storeId), + name: testDeref(t, name), + description: testDeref(t, description), + createTime: testDeref(t, createTime), + updateTime: testDeref(t, updateTime), + version: testDeref(t, version), + vaultPath: testDeref(t, vaultPath), + username: testDeref(t, username), + keyType: testDeref(t, keyType), + keyBits: testDeref(t, keyBits), + ttl: testDeref(t, ttl), + keyId: testDeref(t, keyId), + criticalOptions: testDeref(t, criticalOptions), + extensions: testDeref(t, extensions), + credType: testDeref(t, credType), + projectId: testDeref(t, projectId), + additionalValidPrincipals: testDeref(t, additionalPrincipals), + + history: make([]vaultSshCertCredLibHistory, 0), // Fetch below. + }) + }) + + // Get history for Vault SSH certificate credential libraries. + for _, cl := range data.vaultSshCertCredLibs { + query := fmt.Sprintf("select * from credential_vault_ssh_cert_library_hst where public_id = '%s'", cl.publicId) + queryContext(tx, query, func(scanFn func(dest ...any) error) { + var publicId, storeId, name, description, vaultPath, username, keyType, + ttl, credType, projectId, historyId, validRange *string + var keyBits *int + var criticalOptions, extensions *[]byte + + require.NoError(scanFn(&publicId, &name, &description, &projectId, + &storeId, &vaultPath, &username, &keyType, &keyBits, &ttl, + &criticalOptions, &extensions, &credType, &historyId, &validRange), + ) + + cl.history = append(cl.history, vaultSshCertCredLibHistory{ + historyId: *historyId, + validRange: *validRange, + cl: vaultSshCertCredLib{ + publicId: testDeref(t, publicId), + name: testDeref(t, name), + description: testDeref(t, description), + projectId: testDeref(t, projectId), + storeId: testDeref(t, storeId), + vaultPath: testDeref(t, vaultPath), + username: testDeref(t, username), + keyType: testDeref(t, keyType), + keyBits: testDeref(t, keyBits), + ttl: testDeref(t, ttl), + criticalOptions: testDeref(t, criticalOptions), + extensions: testDeref(t, extensions), + credType: testDeref(t, credType), + }, + }) + }) + } + + // Get all deleted vault credential libraries. + vaultGenericCredLibDeletedTableName := "credential_vault_library_deleted" + if afterRefactor { + vaultGenericCredLibDeletedTableName = "credential_vault_generic_library_deleted" + } + vaultSshCertCredLibDeletedTableName := "credential_vault_ssh_cert_library_deleted" + deletedTables := []string{vaultGenericCredLibDeletedTableName, vaultSshCertCredLibDeletedTableName} + for _, dt := range deletedTables { + queryContext(tx, fmt.Sprintf("select * from %s", dt), func(scanFn func(dest ...any) error) { + var publicId *string + var deleteTime *time.Time + + require.NoError(scanFn(&publicId, &deleteTime)) + + cld := credLibDeleted{ + publicId: testDeref(t, publicId), + deleteTime: testDeref(t, deleteTime), + } + switch dt { + case vaultGenericCredLibDeletedTableName: + data.deletedVaultGenericCredLibs = append(data.deletedVaultGenericCredLibs, cld) + case vaultSshCertCredLibDeletedTableName: + data.deletedVaultSshCertCredLibs = append(data.deletedVaultSshCertCredLibs, cld) + } + }) + } + + // Get row counts. + var rowCountQuery string + if !afterRefactor { + rowCountQuery = ` + select + ( select count(*) from credential_library ), + ( select count(*) from credential_library_history_base ), + ( select count(*) from credential_vault_library_mapping_override ), + + ( select count(*) from credential_vault_library ), + ( select count(*) from credential_vault_library_hst ), + ( select count(*) from credential_vault_library_deleted ), + + ( select count(*) from credential_vault_ssh_cert_library ), + ( select count(*) from credential_vault_ssh_cert_library_hst ), + ( select count(*) from credential_vault_ssh_cert_library_deleted ), + + ( select count(*) from credential_vault_library_username_password_mapping_override ), + ( select count(*) from credential_vault_library_username_password_domain_mapping_ovrd ), + ( select count(*) from credential_vault_library_ssh_private_key_mapping_override ), + + ( select count(*) from credential_vault_library_issue_credentials ), + ( select count(*) from credential_vault_library_list_lookup ), + ( select count(*) from credential_vault_library_hst_aggregate ), + ( select count(*) from whx_credential_dimension_source ); + ` + } else { + rowCountQuery = ` + select + ( select count(*) from credential_library ), + ( select count(*) from credential_library_history_base ), + ( select count(*) from credential_vault_generic_library_mapping_override ), + ( select count(*) from credential_vault_library ), + + ( select count(*) from credential_vault_generic_library ), + ( select count(*) from credential_vault_generic_library_hst ), + ( select count(*) from credential_vault_generic_library_deleted ), + + ( select count(*) from credential_vault_ssh_cert_library ), + ( select count(*) from credential_vault_ssh_cert_library_hst ), + ( select count(*) from credential_vault_ssh_cert_library_deleted ), + + ( select count(*) from credential_vault_generic_library_username_password_mapping_ovrd ), + ( select count(*) from credential_vault_generic_library_usern_pass_domain_mapping_ovrd ), + ( select count(*) from credential_vault_generic_library_ssh_private_key_mapping_ovrd ), + + ( select count(*) from credential_vault_library_issue_credentials ), + ( select count(*) from credential_vault_generic_library_list_lookup ), + ( select count(*) from credential_vault_generic_library_hst_aggregate ), + ( select count(*) from whx_credential_dimension_source ); + ` + } + queryContext(tx, rowCountQuery, func(scanFn func(dest ...any) error) { + var cl, clHistBase, clVaultBasePostRefactorOnly, clVaultGenericMappingOvrd, + clVaultGeneric, clVaultGenericHist, clVaultGenericDel, clVaultSshCert, + clVaultSshCertHist, clVaultSshCertDel, clVaultGenericUpMappingOvrd, + clVaultGenericUpdMappingOvrd, clVaultGenericSshPkMappingOverd, + clVaultLibIssueCreds, clVaultGenericListLookup, clVaultGenericHistAgg, credDimensionSource *int64 + + if !afterRefactor { + require.NoError(scanFn(&cl, &clHistBase, &clVaultGenericMappingOvrd, + &clVaultGeneric, &clVaultGenericHist, &clVaultGenericDel, + &clVaultSshCert, &clVaultSshCertHist, &clVaultSshCertDel, + &clVaultGenericUpMappingOvrd, &clVaultGenericUpdMappingOvrd, &clVaultGenericSshPkMappingOverd, + &clVaultLibIssueCreds, &clVaultGenericListLookup, &clVaultGenericHistAgg, &credDimensionSource), + ) + } else { + require.NoError(scanFn(&cl, &clHistBase, &clVaultGenericMappingOvrd, &clVaultBasePostRefactorOnly, + &clVaultGeneric, &clVaultGenericHist, &clVaultGenericDel, + &clVaultSshCert, &clVaultSshCertHist, &clVaultSshCertDel, + &clVaultGenericUpMappingOvrd, &clVaultGenericUpdMappingOvrd, &clVaultGenericSshPkMappingOverd, + &clVaultLibIssueCreds, &clVaultGenericListLookup, &clVaultGenericHistAgg, &credDimensionSource), + ) + } + + data.rowCounts = rowCounts{ + credLib: testDeref(t, cl), + credLibHistory: testDeref(t, clHistBase), + credVaultLibPostRefactor: testDeref(t, clVaultBasePostRefactorOnly), + credVaultGenericLibMappingOvrd: testDeref(t, clVaultGenericMappingOvrd), + credVaultGenericLib: testDeref(t, clVaultGeneric), + credVaultGenericLibHistory: testDeref(t, clVaultGenericHist), + credVaultGenericLibDeleted: testDeref(t, clVaultGenericDel), + credVaultSshCertLib: testDeref(t, clVaultSshCert), + credVaultSshCertLibHistory: testDeref(t, clVaultSshCertHist), + credVaultSshCertLibDeleted: testDeref(t, clVaultSshCertDel), + credVaultGenericLibMappingOvrdUserPass: testDeref(t, clVaultGenericUpMappingOvrd), + credVaultGenericLibMappingOvrdUserPassDomain: testDeref(t, clVaultGenericUpdMappingOvrd), + credVaultGenericLibMappingOvrdSshPk: testDeref(t, clVaultGenericSshPkMappingOverd), + credVaultLibIssueCredentials: testDeref(t, clVaultLibIssueCreds), + credVaultGenericLibListLookup: testDeref(t, clVaultGenericListLookup), + credVaultGenericLibHistoryAgg: testDeref(t, clVaultGenericHistAgg), + credDimensionSource: testDeref(t, credDimensionSource), + } + }) + + return data +} + +// testDeref is a generic function that dereferences a pointer type, +// safeguarding against a nil pointer dereference. If the input pointer is nil, +// it returns the zero-value for the input type. +func testDeref[T any](_ testing.TB, in *T) T { + if in == nil { + return *new(T) + } + return *in +} + +// dbData is the aggregation of all the data that +// testBuildTestVaultCredLibRefactorDbData gets together. Not designed to be +// used outside of TestVaultCredLibRefactor. +type dbData struct { + orgs []orgProj // From iam_scope. + projects []orgProj // From iam_scope. + + credentialStores []vaultCredStore // From credential_vault_store. + + credLibBase []credLibBase // From credential_library. + credLibHistoryBase []credLibHistoryBase // From credential_library_history_base. + postRefactorVaultCredLibBase []credLibBase // From credential_vault_library (after refactor only). + + vaultGenericCredLibs []vaultGenericCredLib // From credential_vault_library (pre-refactor) or credential_vault_generic_library. + vaultSshCertCredLibs []vaultSshCertCredLib // From credential_vault_ssh_cert_library. + + deletedVaultGenericCredLibs []credLibDeleted // From credential_vault_library_deleted (pre-refactor) or credential_vault_generic_library_deleted. + deletedVaultSshCertCredLibs []credLibDeleted // From credential_vault_ssh_cert_library_deleted. + + rowCounts rowCounts +} + +// orgProj holds database data about a org/project scope. Not designed to be +// used outside of testBuildTestVaultCredLibRefactorDbData. +type orgProj struct { + publicId string + name string + description string + createTime time.Time + updateTime time.Time + typ string + parentId string + version int +} + +// credLibBase holds database data about a credential library. +// Not designed to be used outside of testBuildTestVaultCredLibRefactorDbData. +type credLibBase struct { + publicId string + storeId string + credType string + projectId string + createTime time.Time + updateTime time.Time +} + +// credLibHistoryBase holds historical data about a credential library. Not +// designed to be used outside of testBuildTestVaultCredLibRefactorDbData. +type credLibHistoryBase struct { + historyId string +} + +// credLibDeleted holds database data about a deleted credential library. Not +// designed to be used outside of testBuildTestVaultCredLibRefactorDbData. +type credLibDeleted struct { + publicId string + deleteTime time.Time +} + +// vaultCredStore holds database data about a Vault credential store. Not +// designed to be used outside of testBuildTestVaultCredLibRefactorDbData. +type vaultCredStore struct { + publicId string + projectId string + name string + description string + createTime time.Time + updateTime time.Time + deleteTime time.Time + version int + vaultAddr string + namespace string + caCert []byte + tlsServerName string + tlsSkipVerify bool + workerFilter string +} + +// vaultGenericCredLib holds database data about a Vault generic credential +// library. Not designed to be used outside of +// testBuildTestVaultCredLibRefactorDbData. +type vaultGenericCredLib struct { + publicId string + storeId string + name string + description string + createTime time.Time + updateTime time.Time + version int + vaultPath string + httpMethod string + httpReqBody []byte + credType string + projectId string + + mappingOverride vaultGenericCredLibMappingOvrd + history []vaultGenericCredLibHistory +} +type vaultGenericCredLibHistory struct { + cl vaultGenericCredLib + historyId string + validRange string +} + +// vaultSshCertCredLib holds database data about a Vault SSH certificate +// credential library. Not designed to be used outside of +// testBuildTestVaultCredLibRefactorDbData. +type vaultSshCertCredLib struct { + publicId string + storeId string + name string + description string + createTime time.Time + updateTime time.Time + version int + vaultPath string + username string + keyType string + keyBits int + ttl string + keyId string + criticalOptions []byte + extensions []byte + credType string + projectId string + additionalValidPrincipals string + + history []vaultSshCertCredLibHistory +} +type vaultSshCertCredLibHistory struct { + cl vaultSshCertCredLib + historyId string + validRange string +} + +// vaultGenericCredLibMappingOvrd holds database data about a Vault generic +// credential library's mapping override. Not designed to be used outside of +// testBuildTestVaultCredLibRefactorDbData. +type vaultGenericCredLibMappingOvrd struct { + libraryId string + usernameAttr string + passwordAttr string + domainAttr string + pkAttr string + pkPassphraseAttr string +} + +// rowCounts holds row count information for various database tables and views. +// Not designed to be used outside of testBuildTestVaultCredLibRefactorDbData. +type rowCounts struct { + // credential_library + credLib int64 + // credential_library_history_base + credLibHistory int64 + // credential_vault_library (post-refactor meaning only) + credVaultLibPostRefactor int64 + // credential_vault_library_mapping_override (pre-refactor) or + // credential_vault_generic_library_mapping_override + credVaultGenericLibMappingOvrd int64 + + // credential_vault_library (pre-refactor) or + // credential_vault_generic_library + credVaultGenericLib int64 + // credential_vault_library_hst (pre-refactor) or + // credential_vault_generic_library_hst + credVaultGenericLibHistory int64 + // credential_vault_library_deleted (pre-refactor) or + // credential_vault_generic_library_deleted + credVaultGenericLibDeleted int64 + + // credential_vault_ssh_cert_library + credVaultSshCertLib int64 + // credential_vault_ssh_cert_library_hst + credVaultSshCertLibHistory int64 + // credential_vault_ssh_cert_library_deleted + credVaultSshCertLibDeleted int64 + + // credential_vault_library_username_password_mapping_override + // (pre-refactor) or + // credential_vault_generic_library_username_password_mapping_ovrd + credVaultGenericLibMappingOvrdUserPass int64 + // credential_vault_library_username_password_domain_mapping_ovrd + // (pre-refactor) or + // credential_vault_generic_library_usern_pass_domain_mapping_ovrd + credVaultGenericLibMappingOvrdUserPassDomain int64 + // credential_vault_library_ssh_private_key_mapping_override (pre-refactor) + // or credential_vault_generic_library_ssh_private_key_mapping_ovrd + credVaultGenericLibMappingOvrdSshPk int64 + + // credential_vault_library_issue_credentials + credVaultLibIssueCredentials int64 + + // credential_vault_library_list_lookup (pre-refactor), or + // credential_vault_generic_library_list_lookup + credVaultGenericLibListLookup int64 + + // credential_vault_library_hst_aggregate (pre-refactor), or + // credential_vault_generic_library_hst_aggregate + credVaultGenericLibHistoryAgg int64 + + // whx_credential_dimension_source + credDimensionSource int64 +} + +// testVaultCredLibRefactorDbSetupCounts is an input to +// testVaultCredLibRefactorDbSetup that controls the number of +// orgs/projects/credential libraries to create. +type testVaultCredLibRefactorDbSetupCounts struct { + // orgs is the number of org scopes to create. + orgs int + + // projectsPerOrg is the number of project scopes to create per org. + projectsPerOrg int + + // vaultGenericCredLibs is the number of Vault generic credential + // libraries to create per project. + vaultGenericCredLibs int + + // vaultGenericCredLibsDeleted is the number of Vault generic credential + // libraries to delete per project. Note that we create new credential + // libraries for the purposes of deletion so this doesn't affect + // vaultGenericCredLibs. + vaultGenericCredLibsDeleted int + + // vaultSshCertCredLibs is the number of Vault SSH Certificate + // credential libraries to create per project. + vaultSshCertCredLibs int + + // vaultSshCertCredLibsDeleted is the number of Vault SSH certificate + // credential libraries to delete per project. Note that we create new + // credential libraries for the purposes of deletion so this doesn't affect + // vaultSshCertCredLibs. + vaultSshCertCredLibsDeleted int +} diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index f38d9650e..301da03b0 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -479,7 +479,7 @@ begin; ('p____rcolors', 'cvs__rcolors', 'red vault store', 'Some', 'https://red.vault.color', 'red'), ('p____gcolors', 'cvs__gcolors', 'green vault store', 'Maybe', 'https://green.vault.color', 'green'); - insert into credential_vault_library + insert into credential_vault_generic_library (store_id, public_id, name, description, vault_path, http_method) values ('cvs__bcolors', 'cvl_______b1', 'blue vault library', 'None', '/secrets', 'GET'), @@ -493,6 +493,13 @@ begin; ('cvs__rcolors', 'cvl__ssh__r1', 'red vault ssh library', '/ssh/issue/red', 'webdev', 'ecdsa', 521), ('cvs__gcolors', 'cvl__ssh__g1', 'green vault ssh library', '/ssh/issue/green', 'dba', 'rsa', 4096); + insert into credential_vault_ldap_library + (store_id, public_id, name, vault_path, credential_type) + values + ('cvs__bcolors', 'cvl__ldap_b1', 'blue vault ldap library', '/ldap/static-cred/blue', 'username_password_domain'), + ('cvs__rcolors', 'cvl__ldap_r1', 'red vault ldap library', '/ldap/creds/red', 'username_password_domain'), + ('cvs__gcolors', 'cvl__ldap_g1', 'green vault ldap library', '/ldap/static-cred/org-colors-r-us/group-green-color/green','username_password_domain'); + insert into credential_static_store (project_id, public_id, name, description) values diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 01b63839d..58045eb2f 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -388,7 +388,7 @@ begin; values ('vs_______wvs', 'kdkv___widget', 'current', 'hmac-value', 'token-value', now(), now() + interval '1 hour'); - insert into credential_vault_library + insert into credential_vault_generic_library (store_id, public_id, name, description, vault_path, http_method, credential_type) values ('vs_______wvs', 'vl______wvl1', 'widget vault library', 'None', '/secrets', 'GET', 'unspecified'), @@ -413,82 +413,82 @@ begin; ('vs_______wvs', 'vl______wvl20', 'widget vault kv eighteen', 'None', '/secrets/kv/eighteen', 'GET', 'username_password_domain'), ('vs_______wvs', 'vl______wvl21', 'widget vault kv nineteen', 'None', '/secrets/kv/nineteen', 'GET', 'username_password_domain'); - insert into credential_vault_library_username_password_mapping_override + insert into credential_vault_generic_library_username_password_mapping_ovrd (library_id) values ('vl______wvl4'); - insert into credential_vault_library_username_password_mapping_override + insert into credential_vault_generic_library_username_password_mapping_ovrd (library_id, username_attribute) values ('vl______wvl5', 'my_username'); - insert into credential_vault_library_username_password_mapping_override + insert into credential_vault_generic_library_username_password_mapping_ovrd (library_id, password_attribute) values ('vl______wvl6', 'my_password'); - insert into credential_vault_library_username_password_mapping_override + insert into credential_vault_generic_library_username_password_mapping_ovrd (library_id, username_attribute, password_attribute) values ('vl______wvl7', 'my_username', 'my_password'); - insert into credential_vault_library_ssh_private_key_mapping_override + insert into credential_vault_generic_library_ssh_private_key_mapping_ovrd (library_id) values ('vl______wvl9'); - insert into credential_vault_library_ssh_private_key_mapping_override + insert into credential_vault_generic_library_ssh_private_key_mapping_ovrd (library_id, username_attribute) values ('vl______wvl10', 'my_username'); - insert into credential_vault_library_ssh_private_key_mapping_override + insert into credential_vault_generic_library_ssh_private_key_mapping_ovrd (library_id, private_key_attribute) values ('vl______wvl11', 'my_private_key'); - insert into credential_vault_library_ssh_private_key_mapping_override + insert into credential_vault_generic_library_ssh_private_key_mapping_ovrd (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) values ('vl______wvl12', 'my_username', 'my_private_key', 'my_passphrase'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id) values ('vl______wvl13'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, username_attribute, password_attribute, domain_attribute) values ('vl______wvl14', 'my_username', 'my_password', 'my_domain'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, username_attribute, domain_attribute) values ('vl______wvl15', 'my_username', 'my_domain'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, password_attribute, domain_attribute) values ('vl______wvl16', 'my_password', 'my_domain'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, username_attribute, password_attribute) values ('vl______wvl17', 'my_username', 'my_password'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, username_attribute) values ('vl______wvl18', 'my_username'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, password_attribute) values ('vl______wvl19', 'my_password'); - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id, domain_attribute) values ('vl______wvl20', 'my_domain'); diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_ldap_library.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_ldap_library.sql new file mode 100644 index 000000000..c2f36757e --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_ldap_library.sql @@ -0,0 +1,64 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + select plan(4); + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); + + select lives_ok( + $$ + insert into credential_vault_ldap_library + (store_id, public_id, vault_path, credential_type) + values + ('vs_______wvs', 'vl______vlsc1', '/ldap/static-cred/foo', 'username_password_domain'), + ('vs_______wvs', 'vl______vlsc2', '/ldap/static-cred/foo/bar', 'username_password_domain'), + ('vs_______wvs', 'vl______vlsc3', '/ldap/static-cred/foo/bar/baz', 'username_password_domain'), + ('vs_______wvs', 'vl______vlsc4', '/ldap/static-cred/foo/bar/baz/more/paths', 'username_password_domain'), + ('vs_______wvs', 'vl______vldyn5', '/ldap/creds/foo', 'username_password_domain'), + ('vs_______wvs', 'vl______vldyn6', '/ldap/creds/foo/bar/baz/more/paths', 'username_password_domain'), + ('vs_______wvs', 'vl______vldyn7', '/ldap/creds/foo/bar/baz', 'something_that_doesnt_exist'); + $$ + ); + + select results_eq( + $$ + select public_id, project_id, store_id, name, description, vault_path, credential_type + from credential_vault_ldap_library + where public_id like 'vl______vlsc%' + or public_id like 'vl______vldyn%' + order by public_id; + $$, + $$ + values + ('vl______vldyn5'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/creds/foo', 'username_password_domain'), + ('vl______vldyn6'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/creds/foo/bar/baz/more/paths', 'username_password_domain'), + ('vl______vldyn7'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/creds/foo/bar/baz', 'username_password_domain'), + ('vl______vlsc1'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/static-cred/foo', 'username_password_domain'), + ('vl______vlsc2'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/static-cred/foo/bar', 'username_password_domain'), + ('vl______vlsc3'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/static-cred/foo/bar/baz', 'username_password_domain'), + ('vl______vlsc4'::wt_public_id, 'p____bwidget'::wt_public_id, 'vs_______wvs'::wt_public_id, null::wt_name, null::wt_description, '/ldap/static-cred/foo/bar/baz/more/paths', 'username_password_domain'); + $$ + ); + + select row_eq( + $$ + select count(*) + from credential_vault_library + where public_id like 'vl______vlsc%' + or public_id like 'vl______vldyn%'; + $$, + row(7::bigint) + ); + + select row_eq( + $$ + select count(*) + from credential_library + where public_id like 'vl______vlsc%' + or public_id like 'vl______vldyn%'; + $$, + row(7::bigint) + ); + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql index ae78da439..5d04db1f8 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library.sql @@ -11,31 +11,31 @@ begin; select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets', 'credentials'); -- validate the setup data - select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl______wvl1' and credential_type = 'unspecified'; + select is(count(*), 1::bigint) from credential_vault_generic_library where public_id = 'vl______wvl1' and credential_type = 'unspecified'; select is(count(*), 1::bigint) from credential_library where public_id = 'vl______wvl1' and credential_type = 'unspecified'; - select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl______wvl3' and credential_type = 'username_password'; + select is(count(*), 1::bigint) from credential_vault_generic_library where public_id = 'vl______wvl3' and credential_type = 'username_password'; select is(count(*), 1::bigint) from credential_library where public_id = 'vl______wvl3' and credential_type = 'username_password'; -- validate the insert triggers prepare insert_vault_library as - insert into credential_vault_library + insert into credential_vault_generic_library (store_id, public_id, vault_path, http_method, credential_type) values ('vs_______wvs', 'vl_______tt1', '/secrets/kv', 'GET', 'username_password'); select lives_ok('insert_vault_library'); - select is(count(*), 1::bigint) from credential_vault_library where public_id = 'vl_______tt1' and credential_type = 'username_password'; + select is(count(*), 1::bigint) from credential_vault_generic_library where public_id = 'vl_______tt1' and credential_type = 'username_password'; select is(count(*), 1::bigint) from credential_library where public_id = 'vl_______tt1' and credential_type = 'username_password'; -- validate the delete triggers prepare delete_vault_library as delete - from credential_vault_library + from credential_vault_generic_library where public_id = 'vl_______tt1'; select lives_ok('delete_vault_library'); - select is(count(*), 0::bigint) from credential_vault_library where public_id = 'vl_______tt1'; + select is(count(*), 0::bigint) from credential_vault_generic_library where public_id = 'vl_______tt1'; select is(count(*), 0::bigint) from credential_library where public_id = 'vl_______tt1'; select * from finish(); diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql index 5248a335c..7af2f8add 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_issue_credentials.sql @@ -13,19 +13,19 @@ begin; select is(count(*), 1::bigint) from credential_vault_store where public_id = 'vs_______wvs'; select is(count(*), 4::bigint) - from credential_vault_library_ssh_private_key_mapping_override + from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); select is(count(*), 4::bigint) - from credential_vault_library_username_password_mapping_override + from credential_vault_generic_library_username_password_mapping_ovrd where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); select is(count(*), 8::bigint) - from credential_vault_library_username_password_domain_mapping_ovrd + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id in ('vl______wvl13', 'vl______wvl14', 'vl______wvl15', 'vl______wvl16', 'vl______wvl17', 'vl______wvl18', 'vl______wvl19', 'vl______wvl20'); select is(count(*), 8::bigint) - from credential_vault_library_mapping_override + from credential_vault_generic_library_mapping_override where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); -- create test vault tokens diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_refactor.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_refactor.sql new file mode 100644 index 000000000..f015f0efb --- /dev/null +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_refactor.sql @@ -0,0 +1,602 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +-- Tests the refactor around credential_vault_library (and other associated +-- tables/views). It tests that the new structure is setup as expected with all +-- the associated triggers, FKs, constraints, etc. It does not test the refactor +-- where state is present in the database beforehand. For this, see the Go SQL +-- tests in migrations/oss/postgres_99_01_test.go. +-- +-- For extra context, see migration folder 99/README.md. + +begin; + select plan(97); + + -- Redefined credential_vault_library. + select has_table( 'credential_vault_library'); + select has_column( 'credential_vault_library', 'public_id'); + select col_is_pk( 'credential_vault_library', 'public_id'); + select col_not_null('credential_vault_library', 'public_id'); + + select has_column( 'credential_vault_library', 'store_id'); + select col_not_null('credential_vault_library', 'store_id'); + + select has_column( 'credential_vault_library', 'credential_type'); + select col_not_null( 'credential_vault_library', 'credential_type'); + select col_default_is('credential_vault_library', 'credential_type', 'unspecified'); + + select has_column( 'credential_vault_library', 'project_id'); + select col_not_null('credential_vault_library', 'project_id'); + + select has_column( 'credential_vault_library', 'create_time'); + select col_not_null('credential_vault_library', 'create_time'); + + select has_column( 'credential_vault_library', 'update_time'); + select col_not_null('credential_vault_library', 'update_time'); + + select fk_ok('credential_vault_library', ARRAY['public_id', 'store_id', 'credential_type', 'project_id'], 'credential_library', ARRAY['public_id', 'store_id', 'credential_type', 'project_id']); + select fk_ok('credential_vault_library', ARRAY['project_id', 'store_id'], 'credential_vault_store', ARRAY['project_id', 'public_id']); + + select has_trigger('credential_vault_library', 'immutable_columns'); + select has_trigger('credential_vault_library', 'insert_credential_library_subtype'); + select has_trigger('credential_vault_library', 'delete_credential_library_subtype'); + select has_trigger('credential_vault_library', 'update_credential_library_table_update_time'); + + -- Vault generic credential library table. + select has_table('credential_vault_generic_library'); + select has_column( 'credential_vault_generic_library', 'public_id'); + select col_is_pk('credential_vault_generic_library', 'public_id'); + select has_column( 'credential_vault_generic_library', 'store_id'); + select has_column( 'credential_vault_generic_library', 'name'); + select has_column( 'credential_vault_generic_library', 'description'); + select has_column( 'credential_vault_generic_library', 'create_time'); + select has_column( 'credential_vault_generic_library', 'update_time'); + select has_column( 'credential_vault_generic_library', 'version'); + select has_column( 'credential_vault_generic_library', 'vault_path'); + select has_column( 'credential_vault_generic_library', 'http_method'); + select has_column( 'credential_vault_generic_library', 'http_request_body'); + select has_column( 'credential_vault_generic_library', 'credential_type'); + select has_column( 'credential_vault_generic_library', 'project_id'); + + select fk_ok('credential_vault_generic_library', ARRAY['project_id', 'store_id', 'public_id', 'credential_type'], 'credential_vault_library', ARRAY['project_id', 'store_id', 'public_id', 'credential_type']); + + select has_trigger('credential_vault_generic_library', 'immutable_columns'); + select has_trigger('credential_vault_generic_library', 'insert_deleted_id'); + select has_trigger('credential_vault_generic_library', 'insert_credential_vault_library_subtype'); + select has_trigger('credential_vault_generic_library', 'update_credential_vault_library_table_update_time'); + select has_trigger('credential_vault_generic_library', 'delete_credential_vault_library_subtype'); + select has_trigger('credential_vault_generic_library', 'before_insert_credential_vault_library'); + select has_trigger('credential_vault_generic_library', 'hst_on_insert'); + select has_trigger('credential_vault_generic_library', 'hst_on_update'); + select has_trigger('credential_vault_generic_library', 'hst_on_delete'); + + -- Vault generic deleted table. + select hasnt_table('credential_vault_library_deleted'); + select has_table( 'credential_vault_generic_library_deleted'); + + -- Vault generic history table. + select hasnt_table('credential_vault_library_hst'); + select has_table( 'credential_vault_generic_library_hst'); + + -- Mapping overrides: Base table, UP, UPD and SSH PK. + select hasnt_table('credential_vault_library_mapping_override'); + select has_table('credential_vault_generic_library_mapping_override'); + + select hasnt_table('credential_vault_library_username_password_mapping_override'); + select has_table('credential_vault_generic_library_username_password_mapping_ovrd'); + + select hasnt_table('credential_vault_library_username_password_domain_mapping_ovrd'); + select has_table('credential_vault_generic_library_usern_pass_domain_mapping_ovrd'); + + select hasnt_table('credential_vault_library_ssh_private_key_mapping_override'); + select has_table('credential_vault_generic_library_ssh_private_key_mapping_ovrd'); + + -- Views. + select has_view('credential_vault_library_issue_credentials'); + select has_view('whx_credential_dimension_source'); + + select hasnt_view('credential_vault_generic_hst_aggregate'); + select has_view('credential_vault_generic_library_hst_aggregate'); + + select hasnt_view('credential_vault_library_list_lookup'); + select has_view('credential_vault_generic_library_list_lookup'); + + -- Vault dynamic credentials table. + select fk_ok('credential_vault_credential', 'library_id', 'credential_vault_library', 'public_id'); + + -- We can be reasonably sure the structure is as intended. Now we insert data + -- to actually test it. + select lives_ok( + $$ + insert into iam_scope + (parent_id, type, public_id, name) + values + ('global', 'org', 'o_LaIu234Pg6', 'Test Org Scope'); + + insert into iam_scope + (parent_id, type, public_id, name) + values + ('o_LaIu234Pg6', 'project', 'p_31ouN5ldA2', 'Test Project Scope'); + + insert into credential_vault_store + (public_id, project_id, name, vault_address) + values + ('csvlt_5n3A2kJo', 'p_31ouN5ldA2', 'Test Vault Credential Store', 'https://my.vault.instance.local'); + + insert into credential_vault_generic_library + (public_id, store_id, project_id, name, description, vault_path, http_method, http_request_body) + values + ('clvlt_V134iOjA', 'csvlt_5n3A2kJo', 'p_31ouN5ldA2', 'vault_generic1', 'Test vault generic credential library', '/my/vault/path', 'POST', 'my_http_request_body'); + + insert into credential_vault_generic_library + (public_id, store_id, project_id, credential_type, name, description, vault_path, http_method) + values + ('clvlt_j5oK17vG', 'csvlt_5n3A2kJo', 'p_31ouN5ldA2', 'username_password', 'vault_generic2', 'Test vault generic credential library 2', '/my/vault2/path', 'GET'); + + insert into credential_vault_ssh_cert_library + (public_id, store_id, project_id, name, vault_path, username, key_type, key_bits) + values + ('clvsclt_35iWGH12', 'csvlt_5n3A2kJo', 'p_31ouN5ldA2', 'vault_ssh_cert1', '/ssh/issue/mycert', 'myuser', 'ed25519', 0); + + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd + values ('clvlt_V134iOjA', 'custom_username_attr', 'custom_pw_attr', 'custom_domain_attr'); + + insert into credential_vault_generic_library_username_password_mapping_ovrd + values ('clvlt_j5oK17vG', 'custom_username_attr', 'custom_pw_attr'); + $$ + ); + + -- Assert the data was inserted correctly into the subtype tables. + select row_eq( + $$ -- Query: + select public_id, + store_id, + name, + description, + version, + vault_path, + http_method, + http_request_body, + credential_type, + project_id + from credential_vault_generic_library + where public_id = 'clvlt_V134iOjA'; + $$, + row( -- Expect: + 'clvlt_V134iOjA'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'vault_generic1'::wt_name, + 'Test vault generic credential library'::wt_description, + 1::wt_version, + '/my/vault/path'::text, + 'POST'::text, + 'my_http_request_body'::bytea, + 'unspecified'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + + select row_eq( + $$ -- Query: + select public_id, + store_id, + name, + description, + version, + vault_path, + http_method, + http_request_body, + credential_type, + project_id + from credential_vault_generic_library + where public_id = 'clvlt_j5oK17vG'; + $$, + row( -- Expect: + 'clvlt_j5oK17vG'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'vault_generic2'::wt_name, + 'Test vault generic credential library 2'::wt_description, + 1::wt_version, + '/my/vault2/path'::text, + 'GET'::text, + null::bytea, + 'username_password'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + + select row_eq( + $$ -- Query: + select public_id, + store_id, + name, + version, + vault_path, + username, + key_type, + key_bits, + ttl, + key_id, + critical_options, + extensions, + credential_type, + project_id, + additional_valid_principals + from credential_vault_ssh_cert_library + where public_id = 'clvsclt_35iWGH12'; + $$, + row( -- Expect: + 'clvsclt_35iWGH12'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'vault_ssh_cert1'::wt_name, + 1::wt_version, + '/ssh/issue/mycert'::text, + 'myuser'::text, + 'ed25519'::text, + 0::integer, + null::text, + null::text, + null::bytea, + null::bytea, + 'ssh_certificate'::text, + 'p_31ouN5ldA2'::wt_public_id, + null::text + ) + ); + + -- Assert that the data was inserted correctly into the base tables + -- (credential_vault_library and credential_library). + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_vault_library + where public_id = 'clvlt_V134iOjA'; + $$, + row( -- Expect: + 'clvlt_V134iOjA'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'unspecified'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_library + where public_id = 'clvlt_V134iOjA'; + $$, + row( -- Expect: + 'clvlt_V134iOjA'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'unspecified'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_vault_library + where public_id = 'clvlt_j5oK17vG'; + $$, + row( -- Expect: + 'clvlt_j5oK17vG'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'username_password'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_library + where public_id = 'clvlt_j5oK17vG'; + $$, + row( -- Expect: + 'clvlt_j5oK17vG'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'username_password'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_vault_library + where public_id = 'clvsclt_35iWGH12'; + $$, + row( -- Expect: + 'clvsclt_35iWGH12'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'ssh_certificate'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + select row_eq( + $$ -- Query: + select public_id, + store_id, + credential_type, + project_id + from credential_library + where public_id = 'clvsclt_35iWGH12'; + $$, + row( -- Expect: + 'clvsclt_35iWGH12'::wt_public_id, + 'csvlt_5n3A2kJo'::wt_public_id, + 'ssh_certificate'::text, + 'p_31ouN5ldA2'::wt_public_id + ) + ); + + -- Assert that the data was inserted correctly into the history tables. + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_hst + where public_id = 'clvlt_V134iOjA'; + $$, + row(1::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_library_history_base as base + join credential_vault_generic_library_hst as vglh on base.history_id = vglh.history_id + where vglh.public_id = 'clvlt_V134iOjA'; + $$, + row(1::bigint) + ); + + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_hst + where public_id = 'clvlt_j5oK17vG'; + $$, + row(1::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_library_history_base as base + join credential_vault_generic_library_hst as vglh on base.history_id = vglh.history_id + where vglh.public_id = 'clvlt_j5oK17vG'; + $$, + row(1::bigint) + ); + + select row_eq( + $$ + select count(*) + from credential_vault_ssh_cert_library_hst + where public_id = 'clvsclt_35iWGH12'; + $$, + row(1::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_library_history_base as base + join credential_vault_ssh_cert_library_hst as vsclh on base.history_id = vsclh.history_id + where vsclh.public_id = 'clvsclt_35iWGH12'; + $$, + row(1::bigint) + ); + + -- Assert that the data was inserted correctly into the mapping override + -- tables. + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_mapping_override; + $$, + row(2::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd + where library_id = 'clvlt_V134iOjA'; + $$, + row(1::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_username_password_mapping_ovrd + where library_id = 'clvlt_j5oK17vG'; + $$, + row(1::bigint) + ); + + -- Update credential library. Verify that: + -- - The update works. + -- - Version column is incremented. + -- - A new history entry is created both in the base table and subtype table. + select lives_ok( + $$ + update credential_vault_generic_library + set + name = 'vault_generic1_updated', + vault_path = '/my/updated/vault/path' + where public_id = 'clvlt_V134iOjA'; + $$ + ); + + select row_eq( + $$ -- Query: + select public_id, + name, + version, + vault_path + from credential_vault_generic_library + where public_id = 'clvlt_V134iOjA'; + $$, + row( -- Expect: + 'clvlt_V134iOjA'::wt_public_id, + 'vault_generic1_updated'::wt_name, + 2::wt_version, + '/my/updated/vault/path'::text + ) + ); + + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_hst + where public_id = 'clvlt_V134iOjA'; + $$, + row(2::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_library_history_base as base + join credential_vault_generic_library_hst as vglh on base.history_id = vglh.history_id + where vglh.public_id = 'clvlt_V134iOjA'; + $$, + row(2::bigint) + ); + + -- Delete credential library. Verify that: + -- - The delete works. + -- - The deleted credential library id is inserted into the deleted table. + -- - The deleted credential library row is deleted from the base tables. + -- - Mapping overrides that reference this credential library are + -- automatically deleted. + select lives_ok( + $$ + delete from credential_vault_generic_library + where public_id = 'clvlt_j5oK17vG'; + $$ + ); + + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_deleted + where public_id = 'clvlt_j5oK17vG'; + $$, + row(1::bigint) + ); + + select row_eq( + $$ + select count(*) + from credential_vault_library + where public_id = 'clvlt_j5oK17vG'; + $$, + row(0::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_library + where public_id = 'clvlt_j5oK17vG'; + $$, + row(0::bigint) + ); + + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_username_password_mapping_ovrd + where library_id = 'clvlt_j5oK17vG'; + $$, + row(0::bigint) + ); + select row_eq( + $$ + select count(*) + from credential_vault_generic_library_mapping_override + where library_id = 'clvlt_j5oK17vG'; + $$, + row(0::bigint) + ); + + -- Check data in views. + select results_eq( + $$ + select credential_library_id, + credential_library_name + from whx_credential_dimension_source + order by credential_library_name; + $$, + $$ + values + ('cvl_______g1'::wt_public_id, 'green vault library'), + ('cvl__ssh__g1'::wt_public_id, 'green vault ssh library') + $$ + ); + + select results_eq( + $$ + select public_id, + name + from credential_vault_library_issue_credentials + order by name; + $$, + $$ + values + ( 'cvl__ldap_b1'::wt_public_id, 'blue vault ldap library'::wt_name), + ( 'cvl_______b1'::wt_public_id, 'blue vault library'::wt_name), + ( 'cvl__ssh__b1'::wt_public_id, 'blue vault ssh library'::wt_name), + ( 'cvl__ldap_g1'::wt_public_id, 'green vault ldap library'::wt_name), + ( 'cvl_______g1'::wt_public_id, 'green vault library'::wt_name), + ( 'cvl__ssh__g1'::wt_public_id, 'green vault ssh library'::wt_name), + ( 'cvl__ldap_r1'::wt_public_id, 'red vault ldap library'::wt_name), + ( 'cvl_______r1'::wt_public_id, 'red vault library'::wt_name), + ( 'cvl__ssh__r1'::wt_public_id, 'red vault ssh library'::wt_name), + ( 'clvlt_V134iOjA'::wt_public_id, 'vault_generic1_updated'::wt_name), + ('clvsclt_35iWGH12'::wt_public_id, 'vault_ssh_cert1'::wt_name); + $$ + ); + + select results_eq( + $$ + select public_id, + name + from credential_vault_generic_library_list_lookup + order by name; + $$, + $$ + values + ( 'cvl_______b1'::wt_public_id, 'blue vault library'::wt_name), + ( 'cvl_______g1'::wt_public_id, 'green vault library'::wt_name), + ( 'cvl_______r1'::wt_public_id, 'red vault library'::wt_name), + ( 'clvlt_V134iOjA'::wt_public_id, 'vault_generic1_updated'::wt_name); + $$ + ); + + select results_eq( + $$ + select public_id, + name + from credential_vault_generic_library_hst_aggregate + order by name; + $$, + $$ + values + ('cvl_______g1'::wt_public_id, 'green vault library'::wt_name) + $$ + ); + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql index 011c7c153..84b1af3d8 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_ssh_private_key_mapping_override.sql @@ -3,10 +3,10 @@ -- credential_vault_library_ssh_private_key_mapping_override tests: -- the following triggers --- insert_credential_vault_library_mapping_override_subtype --- delete_credential_vault_library_mapping_override_subtype +-- insert_credential_vault_generic_library_mapping_override_subtyp +-- delete_credential_vault_generic_library_mapping_override_subtyp -- and the following view --- credential_vault_library_list_lookup +-- credential_vault_generic_library_list_lookup begin; select plan(11); @@ -14,16 +14,16 @@ begin; -- validate the setup data select is(count(*), 4::bigint) - from credential_vault_library_ssh_private_key_mapping_override + from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); select is(count(*), 4::bigint) - from credential_vault_library_mapping_override + from credential_vault_generic_library_mapping_override where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12'); prepare select_libraries as select public_id::text, credential_type::text, username_attribute::text, private_key_attribute::text, private_key_passphrase_attribute::text - from credential_vault_library_list_lookup + from credential_vault_generic_library_list_lookup where public_id in ('vl______wvl2', 'vl______wvl8', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12') order by public_id; @@ -39,28 +39,28 @@ begin; ); -- validate the insert triggers - select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl8'; prepare insert_cvl_ssh_private_key_mapping_override as - insert into credential_vault_library_ssh_private_key_mapping_override + insert into credential_vault_generic_library_ssh_private_key_mapping_ovrd (library_id, username_attribute, private_key_attribute) values ('vl______wvl8', 'my_username', 'my_private_key'); select lives_ok('insert_cvl_ssh_private_key_mapping_override'); - select is(count(*), 1::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; - select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 1::bigint) from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id = 'vl______wvl8'; + select is(count(*), 1::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl8'; -- validate the delete triggers prepare delete_cvl_ssh_private_key_mapping_override as delete - from credential_vault_library_ssh_private_key_mapping_override + from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id = 'vl______wvl8'; select lives_ok('delete_cvl_ssh_private_key_mapping_override'); - select is(count(*), 0::bigint) from credential_vault_library_ssh_private_key_mapping_override where library_id = 'vl______wvl8'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_generic_library_ssh_private_key_mapping_ovrd where library_id = 'vl______wvl8'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl8'; select * from finish(); rollback; diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_domain_mapping_ovrd.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_domain_mapping_ovrd.sql index 343adf0bc..d2e08c899 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_domain_mapping_ovrd.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_domain_mapping_ovrd.sql @@ -3,10 +3,10 @@ -- credential_vault_library_username_password_domain_mapping_ovrd tests: -- the following triggers --- insert_credential_vault_library_mapping_override_subtype --- delete_credential_vault_library_mapping_override_subtype +-- insert_credential_vault_generic_library_mapping_override_subtyp +-- delete_credential_vault_generic_library_mapping_override_subtyp -- and the following view --- credential_vault_library_list_lookup +-- credential_vault_generic_library_list_lookup begin; @@ -15,16 +15,16 @@ begin; select is(count(*), 8::bigint) - from credential_vault_library_username_password_domain_mapping_ovrd + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id in ('vl______wvl13', 'vl______wvl14', 'vl______wvl15', 'vl______wvl16', 'vl______wvl17', 'vl______wvl18', 'vl______wvl19', 'vl______wvl20'); select is(count(*), 8::bigint) - from credential_vault_library_mapping_override + from credential_vault_generic_library_mapping_override where library_id in ('vl______wvl13', 'vl______wvl14', 'vl______wvl15', 'vl______wvl16', 'vl______wvl17', 'vl______wvl18', 'vl______wvl19', 'vl______wvl20'); prepare select_libraries as select public_id::text, credential_type::text, username_attribute::text, password_attribute::text, domain_attribute::text - from credential_vault_library_list_lookup + from credential_vault_generic_library_list_lookup where public_id in ('vl______wvl2', 'vl______wvl21', 'vl______wvl13', 'vl______wvl14', 'vl______wvl15', 'vl______wvl16', 'vl______wvl17', 'vl______wvl18', 'vl______wvl19', 'vl______wvl20') order by public_id; @@ -45,30 +45,30 @@ begin; -- validate insert triggers - select is(count(*), 0::bigint) from credential_vault_library_username_password_domain_mapping_ovrd where library_id = 'vl______wvl21'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl21'; + select is(count(*), 0::bigint) from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id = 'vl______wvl21'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl21'; prepare insert_cvl_username_password_mapping_override as - insert into credential_vault_library_username_password_domain_mapping_ovrd + insert into credential_vault_generic_library_usern_pass_domain_mapping_ovrd (library_id,username_attribute, password_attribute, domain_attribute) values ('vl______wvl21', 'my_username', 'my_password', 'my_domain'); select lives_ok('insert_cvl_username_password_mapping_override'); - select is(count(*), 1::bigint) from credential_vault_library_username_password_domain_mapping_ovrd where library_id = 'vl______wvl21'; - select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl21'; + select is(count(*), 1::bigint) from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id = 'vl______wvl21'; + select is(count(*), 1::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl21'; -- validate delete triggers prepare delete_cvl_username_password_domain_mapping_ovrd as delete - from credential_vault_library_username_password_domain_mapping_ovrd + from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id = 'vl______wvl21'; select lives_ok('delete_cvl_username_password_domain_mapping_ovrd'); - select is(count(*), 0::bigint) from credential_vault_library_username_password_domain_mapping_ovrd where library_id = 'vl______wvl21'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl21'; + select is(count(*), 0::bigint) from credential_vault_generic_library_usern_pass_domain_mapping_ovrd where library_id = 'vl______wvl21'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl21'; select * from finish(); diff --git a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql index 92ccd468c..6fb3bef8b 100644 --- a/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql +++ b/internal/db/sqltest/tests/credential/vault/credential_vault_library_username_password_mapping_override.sql @@ -3,10 +3,10 @@ -- credential_vault_library_username_password_mapping_override tests: -- the following triggers --- insert_credential_vault_library_mapping_override_subtype --- delete_credential_vault_library_mapping_override_subtype +-- insert_credential_vault_generic_library_mapping_override_subtyp +-- delete_credential_vault_generic_library_mapping_override_subtyp -- and the following view --- credential_vault_library_list_lookup +-- credential_vault_generic_library_list_lookup begin; select plan(11); @@ -14,16 +14,16 @@ begin; -- validate the setup data select is(count(*), 4::bigint) - from credential_vault_library_username_password_mapping_override + from credential_vault_generic_library_username_password_mapping_ovrd where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); select is(count(*), 4::bigint) - from credential_vault_library_mapping_override + from credential_vault_generic_library_mapping_override where library_id in ('vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7'); prepare select_libraries as select public_id::text, credential_type::text, username_attribute::text, password_attribute::text - from credential_vault_library_list_lookup + from credential_vault_generic_library_list_lookup where public_id in ('vl______wvl2', 'vl______wvl3', 'vl______wvl4', 'vl______wvl5', 'vl______wvl6', 'vl______wvl7') order by public_id; @@ -39,28 +39,28 @@ begin; ); -- validate the insert triggers - select is(count(*), 0::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_generic_library_username_password_mapping_ovrd where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl3'; prepare insert_cvl_username_password_mapping_override as - insert into credential_vault_library_username_password_mapping_override + insert into credential_vault_generic_library_username_password_mapping_ovrd (library_id, username_attribute, password_attribute) values ('vl______wvl3', 'my_username', 'my_password'); select lives_ok('insert_cvl_username_password_mapping_override'); - select is(count(*), 1::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3'; - select is(count(*), 1::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 1::bigint) from credential_vault_generic_library_username_password_mapping_ovrd where library_id = 'vl______wvl3'; + select is(count(*), 1::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl3'; -- validate the delete triggers prepare delete_cvl_username_password_mapping_override as delete - from credential_vault_library_username_password_mapping_override + from credential_vault_generic_library_username_password_mapping_ovrd where library_id = 'vl______wvl3'; select lives_ok('delete_cvl_username_password_mapping_override'); - select is(count(*), 0::bigint) from credential_vault_library_username_password_mapping_override where library_id = 'vl______wvl3'; - select is(count(*), 0::bigint) from credential_vault_library_mapping_override where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_generic_library_username_password_mapping_ovrd where library_id = 'vl______wvl3'; + select is(count(*), 0::bigint) from credential_vault_generic_library_mapping_override where library_id = 'vl______wvl3'; select * from finish(); rollback; diff --git a/internal/db/sqltest/tests/history/dynamic_credentials.sql b/internal/db/sqltest/tests/history/dynamic_credentials.sql index 4705ddbf2..f49bd5ad8 100644 --- a/internal/db/sqltest/tests/history/dynamic_credentials.sql +++ b/internal/db/sqltest/tests/history/dynamic_credentials.sql @@ -2,7 +2,7 @@ -- SPDX-License-Identifier: BUSL-1.1 begin; - select plan(13); + select plan(16); -- Verify the trigger functions exist and are declared properly select has_function('insert_credential_library_history_subtype'); @@ -13,18 +13,23 @@ begin; select volatility_is('delete_credential_library_history_subtype', 'volatile'); select isnt_strict('delete_credential_library_history_subtype'); - select has_trigger('credential_vault_library_hst', 'insert_credential_library_history_subtype'); - select has_trigger('credential_vault_library_hst', 'delete_credential_library_history_subtype'); - select fk_ok('credential_vault_library_hst', 'history_id', 'credential_library_history_base' , 'history_id'); + select has_trigger('credential_vault_generic_library_hst', 'insert_credential_library_history_subtype'); + select has_trigger('credential_vault_generic_library_hst', 'delete_credential_library_history_subtype'); + select fk_ok('credential_vault_generic_library_hst', 'history_id', 'credential_library_history_base' , 'history_id'); select has_trigger('credential_vault_ssh_cert_library_hst', 'insert_credential_library_history_subtype'); select has_trigger('credential_vault_ssh_cert_library_hst', 'delete_credential_library_history_subtype'); select fk_ok('credential_vault_ssh_cert_library_hst', 'history_id', 'credential_library_history_base' , 'history_id'); + select has_trigger('credential_vault_ldap_library_hst', 'insert_credential_library_history_subtype'); + select has_trigger('credential_vault_ldap_library_hst', 'delete_credential_library_history_subtype'); + select fk_ok('credential_vault_ldap_library_hst', 'history_id', 'credential_library_history_base' , 'history_id'); + select results_eq( 'select ' - '(select count(*) from credential_vault_library_hst) + ' - '(select count(*) from credential_vault_ssh_cert_library_hst)', + '(select count(*) from credential_vault_generic_library_hst) + ' + '(select count(*) from credential_vault_ssh_cert_library_hst) + ' + '(select count(*) from credential_vault_ldap_library_hst)', 'select count(*) from credential_library_history_base' ); diff --git a/internal/db/sqltest/tests/pagination/credential_library.sql b/internal/db/sqltest/tests/pagination/credential_library.sql index 8d1f4a277..99e662d02 100644 --- a/internal/db/sqltest/tests/pagination/credential_library.sql +++ b/internal/db/sqltest/tests/pagination/credential_library.sql @@ -2,14 +2,18 @@ -- SPDX-License-Identifier: BUSL-1.1 begin; - select plan(9); + select plan(12); -- Verify the trigger functions exist and are declared properly select has_function('update_credential_library_table_update_time'); select volatility_is('update_credential_library_table_update_time', 'volatile'); select isnt_strict('update_credential_library_table_update_time'); - select has_trigger('credential_vault_library', 'update_credential_library_table_update_time'); - select has_trigger('credential_vault_ssh_cert_library', 'update_credential_library_table_update_time'); + + select has_function('update_credential_vault_library_table_update_time'); + select volatility_is('update_credential_vault_library_table_update_time', 'volatile'); + select isnt_strict('update_credential_vault_library_table_update_time'); + select has_trigger('credential_vault_generic_library', 'update_credential_vault_library_table_update_time'); + select has_trigger('credential_vault_ssh_cert_library', 'update_credential_vault_library_table_update_time'); select has_index('credential_library', 'credential_library_create_time_public_id_idx', array['create_time', 'public_id']); select has_index('credential_library', 'credential_library_update_time_public_id_idx', array['update_time', 'public_id']); @@ -17,14 +21,14 @@ begin; -- when an insert into a subtype table happens works, we check that the -- create time of the entries in both tables match prepare credential_library_create_time as select create_time from credential_library where public_id='cvl_______b1'; - prepare credential_vault_library_create_time as select create_time from credential_vault_library where public_id='cvl_______b1'; + prepare credential_vault_library_create_time as select create_time from credential_vault_generic_library where public_id='cvl_______b1'; select results_eq('credential_library_create_time','credential_vault_library_create_time'); -- To test the trigger that updates the update_time of the base table, -- we update one of the existing creds and check that the base table -- entry has the same update_time as the subtype one. - update credential_vault_library set name='blue black vault library' where public_id='cvl_______b1'; + update credential_vault_generic_library set name='blue black vault library' where public_id='cvl_______b1'; prepare credential_library_update_time as select update_time from credential_library where public_id='cvl_______b1'; - prepare credential_vault_library_update_time as select update_time from credential_vault_library where public_id='cvl_______b1'; + prepare credential_vault_library_update_time as select update_time from credential_vault_generic_library where public_id='cvl_______b1'; select results_eq('credential_library_update_time','credential_vault_library_update_time'); select * from finish(); diff --git a/internal/db/sqltest/tests/recording/recording_dynamic_credentials.sql b/internal/db/sqltest/tests/recording/recording_dynamic_credentials.sql index dbe090e8d..286040df5 100644 --- a/internal/db/sqltest/tests/recording/recording_dynamic_credentials.sql +++ b/internal/db/sqltest/tests/recording/recording_dynamic_credentials.sql @@ -5,7 +5,7 @@ begin; select plan(31); select has_table('recording_dynamic_credential'); - select has_view('credential_vault_library_hst_aggregate', 'view for aggregate vault library history info does not exist'); + select has_view('credential_vault_generic_library_hst_aggregate', 'view for aggregate vault generic library history info does not exist'); select has_view('credential_vault_ssh_cert_library_hst_aggregate', 'view for aggregate vault ssh cert library history info does not exist'); -- tests a fk column referencing a history table @@ -55,7 +55,7 @@ begin; prepare select_recording_vault_libraries as select recording_id::text, public_id::text, store_public_id::text, purposes::text - from credential_vault_library_hst_aggregate + from credential_vault_generic_library_hst_aggregate where recording_id = 'sr1_____cora' order by public_id; diff --git a/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql index a13d3d8d6..71057a0ef 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/session_multiple_sessions.sql @@ -46,7 +46,7 @@ begin; select is(count(*), 1::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; -- change the crediential for the target - update credential_vault_library set vault_path = '/secrets/tcp/admin' where public_id = 'vl______wvl1'; + update credential_vault_generic_library set vault_path = '/secrets/tcp/admin' where public_id = 'vl______wvl1'; -- start another session, should result in a new credential dimension insert into session @@ -80,7 +80,7 @@ begin; select is(count(*), 3::bigint) from wh_credential_dimension where organization_id = 'o_____widget'; -- change the crediential again for the target - update credential_vault_library set vault_path = '/secrets/tcp/user' where vault_path = '/secrets/tcp/admin'; + update credential_vault_generic_library set vault_path = '/secrets/tcp/user' where vault_path = '/secrets/tcp/admin'; -- start another session, should result in a one new credential dimensions since one changed insert into session @@ -101,7 +101,7 @@ begin; -- remove all credentials from the target -- then test creating a session - delete from credential_vault_library; + delete from credential_vault_generic_library; insert into session ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) values diff --git a/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql index d7d436cda..26e912b3c 100644 --- a/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql +++ b/internal/db/sqltest/tests/wh/credential_dimension/three_credentials_one_change.sql @@ -69,7 +69,7 @@ begin; join wh_session_accumulating_fact on wh_session_accumulating_fact.session_id = wh_session_connection_accumulating_fact.session_id where wh_session_connection_accumulating_fact.session_id = 's1____walter'; - update credential_vault_library set vault_path = '/secrets/tcp/user' where public_id = 'vl______wvl2'; + update credential_vault_generic_library set vault_path = '/secrets/tcp/user' where public_id = 'vl______wvl2'; insert into session ( project_id, target_id, user_id, auth_token_id, certificate, endpoint, public_id) 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 565723d3b..e75ec5bcb 100644 --- a/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto +++ b/internal/proto/controller/api/resources/credentiallibraries/v1/credential_library.proto @@ -77,6 +77,11 @@ message CredentialLibrary { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "vault-generic" ]; + VaultLdapCredentialLibraryAttributes vault_ldap_credential_library_attributes = 104 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "vault-ldap" + ]; } // Output only. The available actions on this resource for this user. @@ -210,3 +215,15 @@ message VaultSSHCertificateCredentialLibraryAttributes { } ]; // @gotags: `class:"public"` } + +// The attributes of a vault LDAP Credential Library. +message VaultLdapCredentialLibraryAttributes { + // 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"` +} 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 a549c625a..25a3b02d9 100644 --- a/internal/proto/controller/storage/credential/vault/store/v1/vault.proto +++ b/internal/proto/controller/storage/credential/vault/store/v1/vault.proto @@ -535,3 +535,52 @@ message SshPrivateKeyOverride { // @inject_tag: `gorm:"default:null"` string private_key_passphrase_attribute = 4; } + +message LdapCredentialLibrary { + // 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" + }]; + + // credential_type is always ssh_certificate + // @inject_tag: `gorm:"default:null"` + string credential_type = 9; +} diff --git a/internal/tests/cli/boundary/_credential_libraries.bash b/internal/tests/cli/boundary/_credential_libraries.bash index a4ba27e68..534e6ec1c 100644 --- a/internal/tests/cli/boundary/_credential_libraries.bash +++ b/internal/tests/cli/boundary/_credential_libraries.bash @@ -18,6 +18,17 @@ function create_vault_generic_library() { create vault-generic $@ } +function create_vault_ldap_library() { + boundary credential-libraries \ + create vault-ldap $@ +} + +function update_vault_ldap_library() { + boundary credential-libraries \ + update vault-ldap $@ +} + + function create_vault_library() { boundary credential-libraries \ create vault $@ diff --git a/internal/tests/cli/boundary/credential_libraries.bats b/internal/tests/cli/boundary/credential_libraries.bats index 33d086aac..47f9677f9 100644 --- a/internal/tests/cli/boundary/credential_libraries.bats +++ b/internal/tests/cli/boundary/credential_libraries.bats @@ -52,7 +52,7 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } -@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-generic libary" { +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-generic library" { skip_if_no_vault local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) @@ -106,7 +106,7 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } -@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ssh-certificate libary" { +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ssh-certificate library" { skip_if_no_vault local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) @@ -587,6 +587,69 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } +@test "boundary/credential-libraries: can create $NEW_VAULT_LIB vault-ldap library in credential store $NEW_STORE" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_vault_ldap_library \ + -name $NEW_VAULT_LIB -credential-store-id $csid \ + -vault-path /ldap/static-cred/einstein \ + echo "$output" + [ "$status" -eq 0 ] +} + + +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ldap library" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_vault_ldap_library \ + -name $NEW_VAULT_LIB -credential-store-id $csid \ + -vault-path /ldap/static-cred/einstein \ + echo "$output" + [ "$status" -eq 1 ] +} + +@test "boundary/credential-libraries: can update $NEW_VAULT_LIB vault-ldap library description" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run update_vault_ldap_library -id $clid -description hellothere + echo "$output" + [ "$status" -eq 0 ] + + run read_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] + got=$(echo "$output") + + run field_eq "$got" ".item.attributes.description" "null" + [ "$status" -eq 0 ] +} + +@test "boundary/credential-libraries: can read $NEW_VAULT_LIB vault-ldap library" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run read_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/credential-libraries: can delete $NEW_VAULT_LIB vault-ldaplibrary" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run delete_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] + run has_status_code "$output" "204" + [ "$status" -eq 0 ] +} + # Note, deleting the cred store will revoke the vault token @test "boundary/credential-stores: cleanup can delete $NEW_STORE vault store" { skip_if_no_vault diff --git a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go index aa2402579..ba98a316b 100644 --- a/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go +++ b/sdk/pbs/controller/api/resources/credentiallibraries/credential_library.pb.go @@ -58,6 +58,7 @@ type CredentialLibrary struct { // *CredentialLibrary_VaultCredentialLibraryAttributes // *CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes // *CredentialLibrary_VaultGenericCredentialLibraryAttributes + // *CredentialLibrary_VaultLdapCredentialLibraryAttributes Attrs isCredentialLibrary_Attrs `protobuf_oneof:"attrs"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty" class:"public"` // @gotags: `class:"public"` @@ -205,6 +206,15 @@ func (x *CredentialLibrary) GetVaultGenericCredentialLibraryAttributes() *VaultC return nil } +func (x *CredentialLibrary) GetVaultLdapCredentialLibraryAttributes() *VaultLdapCredentialLibraryAttributes { + if x != nil { + if x, ok := x.Attrs.(*CredentialLibrary_VaultLdapCredentialLibraryAttributes); ok { + return x.VaultLdapCredentialLibraryAttributes + } + } + return nil +} + func (x *CredentialLibrary) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -247,6 +257,10 @@ type CredentialLibrary_VaultGenericCredentialLibraryAttributes struct { VaultGenericCredentialLibraryAttributes *VaultCredentialLibraryAttributes `protobuf:"bytes,103,opt,name=vault_generic_credential_library_attributes,json=vaultGenericCredentialLibraryAttributes,proto3,oneof"` } +type CredentialLibrary_VaultLdapCredentialLibraryAttributes struct { + VaultLdapCredentialLibraryAttributes *VaultLdapCredentialLibraryAttributes `protobuf:"bytes,104,opt,name=vault_ldap_credential_library_attributes,json=vaultLdapCredentialLibraryAttributes,proto3,oneof"` +} + func (*CredentialLibrary_Attributes) isCredentialLibrary_Attrs() {} func (*CredentialLibrary_VaultCredentialLibraryAttributes) isCredentialLibrary_Attrs() {} @@ -256,6 +270,8 @@ func (*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes) isCrede func (*CredentialLibrary_VaultGenericCredentialLibraryAttributes) isCredentialLibrary_Attrs() {} +func (*CredentialLibrary_VaultLdapCredentialLibraryAttributes) isCredentialLibrary_Attrs() {} + // The attributes of a vault typed Credential Library. type VaultCredentialLibraryAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -438,11 +454,57 @@ func (x *VaultSSHCertificateCredentialLibraryAttributes) GetAdditionalValidPrinc return nil } +// The attributes of a vault LDAP Credential Library. +type VaultLdapCredentialLibraryAttributes struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The path in Vault to request credentials from. + Path *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=path,proto3" json:"path,omitempty" class:"public"` // @gotags: `class:"public"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VaultLdapCredentialLibraryAttributes) Reset() { + *x = VaultLdapCredentialLibraryAttributes{} + mi := &file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VaultLdapCredentialLibraryAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VaultLdapCredentialLibraryAttributes) ProtoMessage() {} + +func (x *VaultLdapCredentialLibraryAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VaultLdapCredentialLibraryAttributes.ProtoReflect.Descriptor instead. +func (*VaultLdapCredentialLibraryAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDescGZIP(), []int{3} +} + +func (x *VaultLdapCredentialLibraryAttributes) GetPath() *wrapperspb.StringValue { + if x != nil { + return x.Path + } + return nil +} + var File_controller_api_resources_credentiallibraries_v1_credential_library_proto protoreflect.FileDescriptor const file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc = "" + "\n" + - "Hcontroller/api/resources/credentiallibraries/v1/credential_library.proto\x12/controller.api.resources.credentiallibraries.v1\x1a.controller/api/resources/scopes/v1/scope.proto\x1a*controller/custom_options/v1/options.proto\x1a\x1bgoogle/api/visibility.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xae\v\n" + + "Hcontroller/api/resources/credentiallibraries/v1/credential_library.proto\x12/controller.api.resources.credentiallibraries.v1\x1a.controller/api/resources/scopes/v1/scope.proto\x1a*controller/custom_options/v1/options.proto\x1a\x1bgoogle/api/visibility.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x84\r\n" + "\x11CredentialLibrary\x12\x0e\n" + "\x02id\x18\n" + " \x01(\tR\x02id\x120\n" + @@ -464,7 +526,10 @@ const file_controller_api_resources_credentiallibraries_v1_credential_library_pr "3vault_ssh_certificate_credential_library_attributes\x18f \x01(\v2_.controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributesB-\xa0\xda)\x01\x9a\xe3)\x15vault-ssh-certificate\xfa\xd2\xe4\x93\x02\n" + "\x12\bINTERNALH\x00R.vaultSshCertificateCredentialLibraryAttributes\x12\xd8\x01\n" + "+vault_generic_credential_library_attributes\x18g \x01(\v2Q.controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributesB%\xa0\xda)\x01\x9a\xe3)\rvault-generic\xfa\xd2\xe4\x93\x02\n" + - "\x12\bINTERNALH\x00R'vaultGenericCredentialLibraryAttributes\x12/\n" + + "\x12\bINTERNALH\x00R'vaultGenericCredentialLibraryAttributes\x12\xd3\x01\n" + + "(vault_ldap_credential_library_attributes\x18h \x01(\v2U.controller.api.resources.credentiallibraries.v1.VaultLdapCredentialLibraryAttributesB\"\xa0\xda)\x01\x9a\xe3)\n" + + "vault-ldap\xfa\xd2\xe4\x93\x02\n" + + "\x12\bINTERNALH\x00R$vaultLdapCredentialLibraryAttributes\x12/\n" + "\x12authorized_actions\x18\xac\x02 \x03(\tR\x12authorized_actions\x12/\n" + "\x0fcredential_type\x18\xb6\x02 \x01(\tB\x04\xa0\xda)\x01R\x0fcredential_type\x12b\n" + "\x1ccredential_mapping_overrides\x18\xc0\x02 \x01(\v2\x17.google.protobuf.StructB\x04\xa0\xda)\x01R\x1ccredential_mapping_overridesB\a\n" + @@ -507,7 +572,11 @@ const file_controller_api_resources_credentiallibraries_v1_credential_library_pr "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a=\n" + "\x0fExtensionsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01BhZfgithub.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries;credentiallibrariesb\x06proto3" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"~\n" + + "$VaultLdapCredentialLibraryAttributes\x12V\n" + + "\x04path\x18\n" + + " \x01(\v2\x1c.google.protobuf.StringValueB$\xa0\xda)\x01\xc2\xdd)\x1c\n" + + "\x0fattributes.path\x12\tVaultPathR\x04pathBhZfgithub.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries;credentiallibrariesb\x06proto3" var ( file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDescOnce sync.Once @@ -521,47 +590,50 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro return file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDescData } -var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_goTypes = []any{ (*CredentialLibrary)(nil), // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary (*VaultCredentialLibraryAttributes)(nil), // 1: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes (*VaultSSHCertificateCredentialLibraryAttributes)(nil), // 2: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes - nil, // 3: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry - nil, // 4: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry - (*scopes.ScopeInfo)(nil), // 5: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 6: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 8: google.protobuf.Struct - (*wrapperspb.UInt32Value)(nil), // 9: google.protobuf.UInt32Value + (*VaultLdapCredentialLibraryAttributes)(nil), // 3: controller.api.resources.credentiallibraries.v1.VaultLdapCredentialLibraryAttributes + nil, // 4: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + nil, // 5: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + (*scopes.ScopeInfo)(nil), // 6: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 7: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 9: google.protobuf.Struct + (*wrapperspb.UInt32Value)(nil), // 10: google.protobuf.UInt32Value } var file_controller_api_resources_credentiallibraries_v1_credential_library_proto_depIdxs = []int32{ - 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 + 6, // 0: controller.api.resources.credentiallibraries.v1.CredentialLibrary.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 7, // 1: controller.api.resources.credentiallibraries.v1.CredentialLibrary.name:type_name -> google.protobuf.StringValue + 7, // 2: controller.api.resources.credentiallibraries.v1.CredentialLibrary.description:type_name -> google.protobuf.StringValue + 8, // 3: controller.api.resources.credentiallibraries.v1.CredentialLibrary.created_time:type_name -> google.protobuf.Timestamp + 8, // 4: controller.api.resources.credentiallibraries.v1.CredentialLibrary.updated_time:type_name -> google.protobuf.Timestamp + 9, // 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 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 - 6, // 21: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.additional_valid_principals:type_name -> google.protobuf.StringValue - 22, // [22:22] is the sub-list for method output_type - 22, // [22:22] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 3, // 9: controller.api.resources.credentiallibraries.v1.CredentialLibrary.vault_ldap_credential_library_attributes:type_name -> controller.api.resources.credentiallibraries.v1.VaultLdapCredentialLibraryAttributes + 9, // 10: controller.api.resources.credentiallibraries.v1.CredentialLibrary.credential_mapping_overrides:type_name -> google.protobuf.Struct + 7, // 11: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 7, // 12: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_method:type_name -> google.protobuf.StringValue + 7, // 13: controller.api.resources.credentiallibraries.v1.VaultCredentialLibraryAttributes.http_request_body:type_name -> google.protobuf.StringValue + 7, // 14: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 7, // 15: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.username:type_name -> google.protobuf.StringValue + 7, // 16: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_type:type_name -> google.protobuf.StringValue + 10, // 17: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_bits:type_name -> google.protobuf.UInt32Value + 7, // 18: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ttl:type_name -> google.protobuf.StringValue + 7, // 19: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.key_id:type_name -> google.protobuf.StringValue + 4, // 20: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.critical_options:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.CriticalOptionsEntry + 5, // 21: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.extensions:type_name -> controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.ExtensionsEntry + 7, // 22: controller.api.resources.credentiallibraries.v1.VaultSSHCertificateCredentialLibraryAttributes.additional_valid_principals:type_name -> google.protobuf.StringValue + 7, // 23: controller.api.resources.credentiallibraries.v1.VaultLdapCredentialLibraryAttributes.path:type_name -> google.protobuf.StringValue + 24, // [24:24] is the sub-list for method output_type + 24, // [24:24] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_controller_api_resources_credentiallibraries_v1_credential_library_proto_init() } @@ -574,6 +646,7 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro (*CredentialLibrary_VaultCredentialLibraryAttributes)(nil), (*CredentialLibrary_VaultSshCertificateCredentialLibraryAttributes)(nil), (*CredentialLibrary_VaultGenericCredentialLibraryAttributes)(nil), + (*CredentialLibrary_VaultLdapCredentialLibraryAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -581,7 +654,7 @@ func file_controller_api_resources_credentiallibraries_v1_credential_library_pro GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc), len(file_controller_api_resources_credentiallibraries_v1_credential_library_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/testing/dbtest/docker/Makefile b/testing/dbtest/docker/Makefile index 4b5a3aed1..88f0ae72b 100644 --- a/testing/dbtest/docker/Makefile +++ b/testing/dbtest/docker/Makefile @@ -43,6 +43,7 @@ database-up: -e POSTGRES_PASSWORD=boundary \ -e POSTGRES_USER=boundary \ -e POSTGRES_DB=boundary \ + -e TEST_DATABASE_UP_WITH_OP=$(TEST_DATABASE_UP_WITH_OP) \ -e PGDATA=/pgdata \ --mount type=tmpfs,destination=/pgdata \ -v "$(CWD)/../../../internal/db/schema/migrations":/migrations \ @@ -54,6 +55,7 @@ database-up: @echo "Test database available at: 127.0.0.1:$(TEST_DB_PORT)" @echo "For database logs run:" @echo " docker logs $(TEST_CONTAINER_NAME)" + clean: docker stop $(TEST_CONTAINER_NAME) || true docker rm -v $(TEST_CONTAINER_NAME) || true diff --git a/testing/dbtest/docker/init-db.sh b/testing/dbtest/docker/init-db.sh index 75bc8015b..6ad1abff5 100755 --- a/testing/dbtest/docker/init-db.sh +++ b/testing/dbtest/docker/init-db.sh @@ -62,3 +62,11 @@ done psql -v "ON_ERROR_STOP=1" --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -q <