51263a0f07
- Bad usernames and passwords aren't really errors, since they are based on end-user input. - Other kinds of authentication failures are caused by bad configuration so still treat those as errors. - Empty usernames and passwords are already prevented by our endpoint handler, but just to be safe make sure they cause errors inside the authenticator too.
617 lines
21 KiB
Go
617 lines
21 KiB
Go
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
|
|
"go.pinniped.dev/internal/certauthority"
|
|
"go.pinniped.dev/internal/upstreamldap"
|
|
)
|
|
|
|
// Unlike most other integration tests, you can run this test with no special setup, as long as you have Docker.
|
|
// It does not depend on Kubernetes.
|
|
func TestLDAPSearch(t *testing.T) {
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
t.Cleanup(func() {
|
|
cancelFunc() // this will send SIGKILL to the docker process, just in case
|
|
})
|
|
|
|
port := localhostPort(t)
|
|
caBundle := dockerRunLDAPServer(ctx, t, port)
|
|
|
|
provider := func(editFunc func(p *upstreamldap.Provider)) *upstreamldap.Provider {
|
|
provider := &upstreamldap.Provider{
|
|
Name: "test-ldap-provider",
|
|
Host: "127.0.0.1:" + port,
|
|
CABundle: caBundle,
|
|
BindUsername: "cn=admin,dc=pinniped,dc=dev",
|
|
BindPassword: "password",
|
|
UserSearch: &upstreamldap.UserSearch{
|
|
Base: "ou=users,dc=pinniped,dc=dev",
|
|
Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case
|
|
UsernameAttribute: "cn",
|
|
UIDAttribute: "uidNumber",
|
|
},
|
|
}
|
|
if editFunc != nil {
|
|
editFunc(provider)
|
|
}
|
|
return provider
|
|
}
|
|
|
|
pinnyPassword := "password123" // from the LDIF file below
|
|
wallyPassword := "password456" // from the LDIF file below
|
|
|
|
tests := []struct {
|
|
name string
|
|
username string
|
|
password string
|
|
provider *upstreamldap.Provider
|
|
wantError string
|
|
wantAuthResponse *authenticator.Response
|
|
wantUnauthenticated bool
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(nil),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "happy path as a different user",
|
|
username: "wally",
|
|
password: wallyPassword,
|
|
provider: provider(nil),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "wally", UID: "1001", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "using a different user search base",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "dc=pinniped,dc=dev" }),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the user search filter is already wrapped by parenthesis",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "(cn={})" }),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute is dn and a user search filter is provided",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.UsernameAttribute = "dn"
|
|
p.UserSearch.Filter = "cn={}"
|
|
}),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the user search filter allows for different ways of logging in and the first one is used",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
|
}),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the user search filter allows for different ways of logging in and the second one is used",
|
|
username: "pinny.ldap@example.com",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
|
}),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the UIDAttribute is dn",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "dn" }),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the UIDAttribute is sn",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "sn" }),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute is sn",
|
|
username: "seAl", // note that this is not case-sensitive! sn=Seal
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "sn" }),
|
|
wantAuthResponse: &authenticator.Response{
|
|
User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer is case-sensitive
|
|
},
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute is dn and there is no user search filter provided",
|
|
username: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.UsernameAttribute = "dn"
|
|
p.UserSearch.Filter = ""
|
|
}),
|
|
wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`,
|
|
},
|
|
{
|
|
name: "when the bind user username is not a valid DN",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "invalid-dn" }),
|
|
wantError: `error binding as "invalid-dn" before user search: LDAP Result Code 34 "Invalid DN Syntax": invalid DN`,
|
|
},
|
|
{
|
|
name: "when the bind user username is wrong",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "cn=wrong,dc=pinniped,dc=dev" }),
|
|
wantError: `error binding as "cn=wrong,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `,
|
|
},
|
|
{
|
|
name: "when the bind user password is wrong",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.BindPassword = "wrong-password" }),
|
|
wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `,
|
|
},
|
|
{
|
|
name: "when the end user password is wrong",
|
|
username: "pinny",
|
|
password: "wrong-pinny-password",
|
|
provider: provider(nil),
|
|
wantUnauthenticated: true,
|
|
},
|
|
{
|
|
name: "when the end user username is wrong",
|
|
username: "wrong-username",
|
|
password: pinnyPassword,
|
|
provider: provider(nil),
|
|
wantUnauthenticated: true,
|
|
},
|
|
{
|
|
name: "when the user search filter does not compile",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "*" }),
|
|
wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`,
|
|
},
|
|
{
|
|
name: "when there are too many search results for the user",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.Filter = "objectClass=*" // overly broad search filter
|
|
}),
|
|
wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `,
|
|
},
|
|
{
|
|
name: "when the server is unreachable",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:27534" }), // hopefully this port is not in use on the host running tests
|
|
wantError: `error dialing host "127.0.0.1:27534": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:27534: connect: connection refused`,
|
|
},
|
|
{
|
|
name: "when the server is not parsable",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.Host = "too:many:ports" }),
|
|
wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`,
|
|
},
|
|
{
|
|
name: "when the CA bundle is not parsable",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.CABundle = []byte("invalid-pem") }),
|
|
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, port),
|
|
},
|
|
{
|
|
name: "when the CA bundle does not cause the host to be trusted",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.CABundle = nil }),
|
|
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, port),
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute attribute has multiple values in the entry",
|
|
username: "wally.ldap@example.com",
|
|
password: wallyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "mail" }),
|
|
wantError: `found 2 values for attribute "mail" while searching for user "wally.ldap@example.com", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UIDAttribute attribute has multiple values in the entry",
|
|
username: "wally",
|
|
password: wallyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "mail" }),
|
|
wantError: `found 2 values for attribute "mail" while searching for user "wally", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute attribute is not found in the entry",
|
|
username: "wally",
|
|
password: wallyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.Filter = "cn={}"
|
|
p.UserSearch.UsernameAttribute = "attr-does-not-exist"
|
|
}),
|
|
wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UIDAttribute attribute is not found in the entry",
|
|
username: "wally",
|
|
password: wallyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "attr-does-not-exist" }),
|
|
wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute has the wrong case",
|
|
username: "Seal",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "SN" }), // this is case-sensitive
|
|
wantError: `found 0 values for attribute "SN" while searching for user "Seal", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UIDAttribute has the wrong case",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "SN" }), // this is case-sensitive
|
|
wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UsernameAttribute is DN and has the wrong case",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.UsernameAttribute = "DN" // dn must be lower-case
|
|
p.UserSearch.Filter = "cn={}"
|
|
}),
|
|
wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the UIDAttribute is DN and has the wrong case",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) {
|
|
p.UserSearch.UIDAttribute = "DN" // dn must be lower-case
|
|
}),
|
|
wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`,
|
|
},
|
|
{
|
|
name: "when the search base is invalid",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "invalid-base" }),
|
|
wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`,
|
|
},
|
|
{
|
|
name: "when the search base does not exist",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" }),
|
|
wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `,
|
|
},
|
|
{
|
|
name: "when the search base causes no search results",
|
|
username: "pinny",
|
|
password: pinnyPassword,
|
|
provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }),
|
|
wantUnauthenticated: true,
|
|
},
|
|
{
|
|
name: "when there is no username specified",
|
|
username: "",
|
|
password: pinnyPassword,
|
|
provider: provider(nil),
|
|
wantUnauthenticated: true,
|
|
},
|
|
{
|
|
name: "when there is no password specified",
|
|
username: "pinny",
|
|
password: "",
|
|
provider: provider(nil),
|
|
wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 206 "Empty password not allowed by the client": ldap: empty password not allowed by the client`,
|
|
},
|
|
{
|
|
name: "when the user has no password in their entry",
|
|
username: "olive",
|
|
password: "anything",
|
|
provider: provider(nil),
|
|
wantUnauthenticated: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
tt := test
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password)
|
|
|
|
switch {
|
|
case tt.wantError != "":
|
|
require.EqualError(t, err, tt.wantError)
|
|
require.False(t, authenticated)
|
|
require.Nil(t, authResponse)
|
|
case tt.wantUnauthenticated:
|
|
require.NoError(t, err)
|
|
require.False(t, authenticated)
|
|
require.Nil(t, authResponse)
|
|
default:
|
|
require.NoError(t, err)
|
|
require.True(t, authenticated)
|
|
require.Equal(t, tt.wantAuthResponse, authResponse)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func localhostPort(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
recentlyClaimedHostAndPort := unusedPortGrabbingListener.Addr().String()
|
|
require.NoError(t, unusedPortGrabbingListener.Close())
|
|
|
|
splitHostAndPort := strings.Split(recentlyClaimedHostAndPort, ":")
|
|
require.Len(t, splitHostAndPort, 2)
|
|
|
|
return splitHostAndPort[1]
|
|
}
|
|
|
|
func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []byte {
|
|
t.Helper()
|
|
|
|
_, err := exec.LookPath("docker")
|
|
require.NoError(t, err)
|
|
|
|
ca, err := certauthority.New("Test LDAP CA", time.Hour*24)
|
|
require.NoError(t, err)
|
|
|
|
certPEM, keyPEM, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour*24)
|
|
require.NoError(t, err)
|
|
|
|
tempDir, err := ioutil.TempDir("", "pinniped-test-*")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := os.Remove(tempDir)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
writeToNewTempFile(t, tempDir, "cert.pem", certPEM)
|
|
writeToNewTempFile(t, tempDir, "key.pem", keyPEM)
|
|
writeToNewTempFile(t, tempDir, "ca.pem", ca.Bundle())
|
|
writeToNewTempFile(t, tempDir, "test.ldif", []byte(testLDIF))
|
|
|
|
dockerArgs := []string{
|
|
"run",
|
|
"-e", "BITNAMI_DEBUG=true",
|
|
"-e", "LDAP_ADMIN_USERNAME=admin",
|
|
"-e", "LDAP_ADMIN_PASSWORD=password",
|
|
"-e", "LDAP_ENABLE_TLS=yes",
|
|
"-e", "LDAP_TLS_CERT_FILE=/inputs/cert.pem",
|
|
"-e", "LDAP_TLS_KEY_FILE=/inputs/key.pem",
|
|
"-e", "LDAP_TLS_CA_FILE=/inputs/ca.pem",
|
|
"-e", "LDAP_CUSTOM_LDIF_DIR=/inputs",
|
|
"-e", "LDAP_ROOT=dc=pinniped,dc=dev",
|
|
"-v", tempDir + ":/inputs",
|
|
"-p", hostPort + ":1636",
|
|
"-m", "64m",
|
|
"--rm", // automatically delete the container when finished
|
|
"docker.io/bitnami/openldap",
|
|
}
|
|
|
|
t.Log("Starting:", "docker", strings.Join(dockerArgs, " "))
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", dockerArgs...)
|
|
|
|
var stdoutBuf, stderrBuf syncBuffer
|
|
cmd.Stdout = &stdoutBuf
|
|
cmd.Stderr = &stderrBuf
|
|
cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
|
|
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
|
|
|
|
err = cmd.Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
// docker requires an interrupt signal to end the container.
|
|
// This t.Cleanup is registered after the one that cancels the context, so this one will happen first.
|
|
err := cmd.Process.Signal(os.Interrupt)
|
|
require.NoError(t, err)
|
|
time.Sleep(time.Second) // give a moment before we move on, because we'll send SIGKILL in a later t.Cleanup
|
|
})
|
|
|
|
earlyTerminationCh := make(chan bool, 1)
|
|
go func() {
|
|
err = cmd.Wait()
|
|
earlyTerminationCh <- true
|
|
}()
|
|
|
|
terminatedEarly := false
|
|
require.Eventually(t, func() bool {
|
|
t.Log("Waiting for slapd to start...")
|
|
// This substring is contained in the last line of output before the server starts.
|
|
if strings.Contains(stderrBuf.String(), " slapd starting\n") {
|
|
return true
|
|
}
|
|
select {
|
|
case <-earlyTerminationCh:
|
|
terminatedEarly = true
|
|
return true
|
|
default: // ignore when this non-blocking read found no message
|
|
}
|
|
return false
|
|
}, 2*time.Minute, time.Second)
|
|
|
|
require.Falsef(t, terminatedEarly, "docker command ended sooner than expected")
|
|
|
|
t.Log("Detected LDAP server has started successfully")
|
|
return ca.Bundle()
|
|
}
|
|
|
|
func writeToNewTempFile(t *testing.T, dir string, filename string, contents []byte) {
|
|
t.Helper()
|
|
|
|
filePath := path.Join(dir, filename)
|
|
|
|
err := ioutil.WriteFile(filePath, contents, 0600)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
err := os.Remove(filePath)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
var testLDIF = `
|
|
# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! ***
|
|
# Here's a good explanation of LDIF:
|
|
# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system
|
|
|
|
# pinniped.dev (organization, root)
|
|
dn: dc=pinniped,dc=dev
|
|
objectClass: dcObject
|
|
objectClass: organization
|
|
dc: pinniped
|
|
o: example
|
|
|
|
# users, pinniped.dev (organization unit)
|
|
dn: ou=users,dc=pinniped,dc=dev
|
|
objectClass: organizationalUnit
|
|
ou: users
|
|
|
|
# groups, pinniped.dev (organization unit)
|
|
dn: ou=groups,dc=pinniped,dc=dev
|
|
objectClass: organizationalUnit
|
|
ou: groups
|
|
|
|
# beach-groups, groups, pinniped.dev (organization unit)
|
|
dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
|
objectClass: organizationalUnit
|
|
ou: beach-groups
|
|
|
|
# pinny, users, pinniped.dev (user)
|
|
dn: cn=pinny,ou=users,dc=pinniped,dc=dev
|
|
objectClass: inetOrgPerson
|
|
objectClass: posixAccount
|
|
objectClass: shadowAccount
|
|
cn: pinny
|
|
sn: Seal
|
|
givenName: Pinny
|
|
mail: pinny.ldap@example.com
|
|
userPassword: password123
|
|
uid: pinny
|
|
uidNumber: 1000
|
|
gidNumber: 1000
|
|
homeDirectory: /home/pinny
|
|
loginShell: /bin/bash
|
|
gecos: pinny-the-seal
|
|
|
|
# wally, users, pinniped.dev
|
|
dn: cn=wally,ou=users,dc=pinniped,dc=dev
|
|
objectClass: inetOrgPerson
|
|
objectClass: posixAccount
|
|
objectClass: shadowAccount
|
|
cn: wally
|
|
sn: Walrus
|
|
givenName: Wally
|
|
mail: wally.ldap@example.com
|
|
mail: wally.alternate@example.com
|
|
userPassword: password456
|
|
uid: wally
|
|
uidNumber: 1001
|
|
gidNumber: 1001
|
|
homeDirectory: /home/wally
|
|
loginShell: /bin/bash
|
|
gecos: wally-the-walrus
|
|
|
|
# olive, users, pinniped.dev (user without password)
|
|
dn: cn=olive,ou=users,dc=pinniped,dc=dev
|
|
objectClass: inetOrgPerson
|
|
objectClass: posixAccount
|
|
objectClass: shadowAccount
|
|
cn: olive
|
|
sn: Boston Terrier
|
|
givenName: Olive
|
|
mail: olive.ldap@example.com
|
|
uid: olive
|
|
uidNumber: 1002
|
|
gidNumber: 1002
|
|
homeDirectory: /home/olive
|
|
loginShell: /bin/bash
|
|
gecos: olive-the-dog
|
|
|
|
# ball-game-players, beach-groups, groups, pinniped.dev (group of users)
|
|
dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
|
cn: ball-game-players
|
|
objectClass: groupOfNames
|
|
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
|
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
|
|
|
# seals, groups, pinniped.dev (group of users)
|
|
dn: cn=seals,ou=groups,dc=pinniped,dc=dev
|
|
cn: seals
|
|
objectClass: groupOfNames
|
|
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
|
|
|
# walruses, groups, pinniped.dev (group of users)
|
|
dn: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
|
cn: walruses
|
|
objectClass: groupOfNames
|
|
member: cn=wally,ou=users,dc=pinniped,dc=dev
|
|
|
|
# pinnipeds, users, pinniped.dev (group of groups)
|
|
dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
|
|
cn: pinnipeds
|
|
objectClass: groupOfNames
|
|
member: cn=seals,ou=groups,dc=pinniped,dc=dev
|
|
member: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
|
|
|
# mammals, groups, pinniped.dev (group of both groups and users)
|
|
dn: cn=mammals,ou=groups,dc=pinniped,dc=dev
|
|
cn: mammals
|
|
objectClass: groupOfNames
|
|
member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev
|
|
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
|
`
|