feat(credential/vault): Implement LDAP Credential Library

This commit is contained in:
Hugo authored and Danielle Miu committed 2025-11-05 16:20:42 -05:00
1 parent ebf0450b03
commit 9e08eafcff
17 files changed
+2375 -105

No files matched your search

+6
View File
@@ -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,
+2 -2
View File
@@ -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
+4 -20
View File
@@ -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=
@@ -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)
})
@@ -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))
}
+8 -2
View File
@@ -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
}
@@ -0,0 +1,105 @@
// 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.
type LdapCredentialLibrary struct {
*store.LdapCredentialLibrary
tableName string `gorm:"-"`
}
// NewLdapCredentialLibrary creates a new in memory LdapCredentialLibrary
// for a Vault backend at vaultPath assigned to storeId.
// Name and description 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"
}
@@ -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"
@@ -684,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,
@@ -1110,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
}
@@ -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)
})
}
}
+10
View File
@@ -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
}
+114 -1
View File
@@ -195,7 +195,8 @@ select sum(reltuples::bigint) as estimate
from pg_class
where oid in (
'credential_vault_generic_library'::regclass,
'credential_vault_ssh_cert_library'::regclass
'credential_vault_ssh_cert_library'::regclass,
'credential_vault_ldap_library'::regclass
)
`
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
}
@@ -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)
})
}
+171 -24
View File
@@ -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,
},
+223 -8
View File
@@ -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")
+191 -1
View File
@@ -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 "<ldapMountPath>/*" {
// 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...)
}
@@ -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;
}