Add LDAP STS tests and workflow for CI (#13576)

Runs LDAP tests with openldap container on GH Actions
This commit is contained in:
Aditya Manthramurthy 2021-11-04 08:16:30 -07:00 committed by GitHub
parent 64a1904136
commit 01b9ff54d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 306 additions and 20 deletions

45
.github/workflows/ldap-integration.yaml vendored Normal file
View File

@ -0,0 +1,45 @@
name: External IDP Integration Tests
on:
pull_request:
branches:
- master
# This ensures that previous jobs for the PR are cancelled when the PR is
# updated.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs:
ldap-test:
name: LDAP Tests with Go ${{ matrix.go-version }}
runs-on: ubuntu-latest
services:
openldap:
image: quay.io/minio/openldap
ports:
- 389:389
- 636:636
env:
LDAP_ORGANISATION: "MinIO Inc"
LDAP_DOMAIN: "min.io"
LDAP_ADMIN_PASSWORD: "admin"
strategy:
matrix:
go-version: [1.16.x, 1.17.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Test LDAP
env:
LDAP_TEST_SERVER: "localhost:389"
run: |
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
make test-ldap

View File

@ -46,6 +46,12 @@ test-race: verifiers build
@echo "Running unit tests under -race"
@(env bash $(PWD)/buildscripts/race.sh)
test-ldap: build
@echo "Running tests for LDAP integration"
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAMWithLDAPServerSuite ./cmd
@echo "Running tests for LDAP integration with -race"
@GOGC=25 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAMWithLDAPServerSuite ./cmd
verify: ## verify minio various setups
@echo "Verifying build with race"
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null

View File

@ -48,9 +48,7 @@ func newTestSuiteIAM(c TestSuiteCommon) *TestSuiteIAM {
return &TestSuiteIAM{TestSuiteCommon: c}
}
func (s *TestSuiteIAM) SetUpSuite(c *check) {
s.TestSuiteCommon.SetUpSuite(c)
func (s *TestSuiteIAM) iamSetup(c *check) {
var err error
// strip url scheme from endpoint
s.endpoint = strings.TrimPrefix(s.endPoint, "http://")
@ -75,6 +73,18 @@ func (s *TestSuiteIAM) SetUpSuite(c *check) {
}
}
func (s *TestSuiteIAM) SetUpSuite(c *check) {
s.TestSuiteCommon.SetUpSuite(c)
s.iamSetup(c)
}
func (s *TestSuiteIAM) RestartIAMSuite(c *check) {
s.TestSuiteCommon.RestartTestServer(c)
s.iamSetup(c)
}
func (s *TestSuiteIAM) getUserClient(c *check, accessKey, secretKey, sessionToken string) *minio.Client {
client, err := minio.New(s.endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, sessionToken),

View File

@ -275,17 +275,29 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
switch {
case globalOpenIDConfig.ProviderEnabled():
go func() {
ticker := time.NewTicker(sys.iamRefreshInterval)
defer ticker.Stop()
for {
time.Sleep(sys.iamRefreshInterval)
sys.purgeExpiredCredentialsForExternalSSO(ctx)
select {
case <-ticker.C:
sys.purgeExpiredCredentialsForExternalSSO(ctx)
case <-ctx.Done():
return
}
}
}()
case globalLDAPConfig.EnabledWithLookupBind():
go func() {
ticker := time.NewTicker(sys.iamRefreshInterval)
defer ticker.Stop()
for {
time.Sleep(sys.iamRefreshInterval)
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
select {
case <-ticker.C:
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
case <-ctx.Done():
return
}
}
}()
}
@ -308,14 +320,20 @@ func (sys *IAMSys) watch(ctx context.Context) {
err := sys.loadWatchedEvent(ctx, event)
logger.LogIf(ctx, err)
}
return
}
} else {
// Fall back to loading all items
for {
time.Sleep(sys.iamRefreshInterval)
// Fall back to loading all items periodically
ticker := time.NewTicker(sys.iamRefreshInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := sys.Load(ctx, sys.store); err != nil {
logger.LogIf(ctx, err)
}
case <-ctx.Done():
return
}
}
}

View File

@ -19,6 +19,7 @@ package cmd
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
@ -158,6 +159,27 @@ func (s *TestSuiteCommon) SetUpSuite(c *check) {
s.secretKey = s.testServer.SecretKey
}
func (s *TestSuiteCommon) RestartTestServer(c *check) {
// Shutdown.
s.testServer.cancel()
s.testServer.Server.Close()
s.testServer.Obj.Shutdown(context.Background())
// Restart.
ctx, cancel := context.WithCancel(context.Background())
s.testServer.cancel = cancel
s.testServer = initTestServerWithBackend(ctx, c, s.testServer, s.testServer.Obj, s.testServer.rawDiskPaths)
if s.secure {
s.testServer.Server.StartTLS()
} else {
s.testServer.Server.Start()
}
s.client = s.testServer.Server.Client()
s.endPoint = s.testServer.Server.URL
}
func (s *TestSuiteCommon) TearDownSuite(c *check) {
s.testServer.Stop()
}

View File

@ -20,6 +20,8 @@ package cmd
import (
"context"
"fmt"
"os"
"strings"
"testing"
"github.com/minio/madmin-go"
@ -132,3 +134,179 @@ func (s *TestSuiteIAM) TestSTS(c *check) {
c.Fatalf("unexpected non-access-denied err: %v", err)
}
}
const (
EnvTestLDAPServer = "LDAP_TEST_SERVER"
)
func (s *TestSuiteIAM) GetLDAPServer(c *check) string {
return os.Getenv(EnvTestLDAPServer)
}
// SetUpLDAP - expects to setup an LDAP test server using the test LDAP
// container and canned data from https://github.com/minio/minio-ldap-testing
func (s *TestSuiteIAM) SetUpLDAP(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
defer cancel()
serverAddr := s.GetLDAPServer(c)
configCmds := []string{
"identity_ldap",
fmt.Sprintf("server_addr=%s", serverAddr),
"server_insecure=on",
"lookup_bind_dn=cn=admin,dc=min,dc=io",
"lookup_bind_password=admin",
"user_dn_search_base_dn=dc=min,dc=io",
"user_dn_search_filter=(uid=%s)",
"group_search_base_dn=ou=swengg,dc=min,dc=io",
"group_search_filter=(&(objectclass=groupofnames)(member=%d))",
}
_, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
if err != nil {
c.Fatalf("unable to setup LDAP for tests: %v", err)
}
s.RestartIAMSuite(c)
}
func TestIAMWithLDAPServerSuite(t *testing.T) {
testCases := []*TestSuiteIAM{
// Init and run test on FS backend with signature v4.
newTestSuiteIAM(TestSuiteCommon{serverType: "FS", signer: signerV4}),
// Init and run test on FS backend, with tls enabled.
newTestSuiteIAM(TestSuiteCommon{serverType: "FS", signer: signerV4, secure: true}),
// Init and run test on Erasure backend.
newTestSuiteIAM(TestSuiteCommon{serverType: "Erasure", signer: signerV4}),
// Init and run test on ErasureSet backend.
newTestSuiteIAM(TestSuiteCommon{serverType: "ErasureSet", signer: signerV4}),
}
for i, testCase := range testCases {
t.Run(fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.serverType), func(t *testing.T) {
c := &check{t, testCase.serverType}
suite := testCase
if suite.GetLDAPServer(c) == "" {
return
}
suite.SetUpSuite(c)
suite.SetUpLDAP(c)
suite.TestLDAPSTS(c)
suite.TearDownSuite(c)
})
}
}
func (s *TestSuiteIAM) TestLDAPSTS(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
defer cancel()
bucket := getRandomBucketName()
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
if err != nil {
c.Fatalf("bucket create error: %v", err)
}
// Create policy, user and associate policy
policy := "mypolicy"
policyBytes := []byte(fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::%s/*"
]
}
]
}`, bucket))
err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
if err != nil {
c.Fatalf("policy add error: %v", err)
}
ldapID := cr.LDAPIdentity{
Client: s.TestSuiteCommon.client,
STSEndpoint: s.endPoint,
LDAPUsername: "dillon",
LDAPPassword: "dillon",
}
_, err = ldapID.Retrieve()
if err == nil {
c.Fatalf("Expected to fail to create a user with no associated policy!")
}
userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
err = s.adm.SetPolicy(ctx, policy, userDN, false)
if err != nil {
c.Fatalf("Unable to set policy: %v", err)
}
value, err := ldapID.Retrieve()
if err != nil {
c.Fatalf("Expected to generate STS creds, got err: %#v", err)
}
minioClient, err := minio.New(s.endpoint, &minio.Options{
Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
Secure: s.secure,
Transport: s.TestSuiteCommon.client.Transport,
})
if err != nil {
c.Fatalf("Error initializing client: %v", err)
}
// Validate that the client from sts creds can access the bucket.
c.mustListObjects(ctx, minioClient, bucket)
// Validate that the client cannot remove any objects
err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{})
if err.Error() != "Access Denied." {
c.Fatalf("unexpected non-access-denied err: %v", err)
}
// Remove the policy assignment on the user DN:
err = s.adm.SetPolicy(ctx, "", userDN, false)
if err != nil {
c.Fatalf("Unable to remove policy setting: %v", err)
}
_, err = ldapID.Retrieve()
if err == nil {
c.Fatalf("Expected to fail to create a user with no associated policy!")
}
// Set policy via group and validate policy assignment.
groupDN := "cn=projectb,ou=groups,ou=swengg,dc=min,dc=io"
err = s.adm.SetPolicy(ctx, policy, groupDN, true)
if err != nil {
c.Fatalf("Unable to set group policy: %v", err)
}
value, err = ldapID.Retrieve()
if err != nil {
c.Fatalf("Expected to generate STS creds, got err: %#v", err)
}
minioClient, err = minio.New(s.endpoint, &minio.Options{
Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
Secure: s.secure,
Transport: s.TestSuiteCommon.client.Transport,
})
if err != nil {
c.Fatalf("Error initializing client: %v", err)
}
// Validate that the client from sts creds can access the bucket.
c.mustListObjects(ctx, minioClient, bucket)
// Validate that the client cannot remove any objects
err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{})
c.Assert(err.Error(), "Access Denied.")
}

View File

@ -292,13 +292,14 @@ func isSameType(obj1, obj2 interface{}) bool {
// s := StartTestServer(t,"Erasure")
// defer s.Stop()
type TestServer struct {
Root string
Disks EndpointServerPools
AccessKey string
SecretKey string
Server *httptest.Server
Obj ObjectLayer
cancel context.CancelFunc
Root string
Disks EndpointServerPools
AccessKey string
SecretKey string
Server *httptest.Server
Obj ObjectLayer
cancel context.CancelFunc
rawDiskPaths []string
}
// UnstartedTestServer - Configures a temp FS/Erasure backend,
@ -314,16 +315,22 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer {
t.Fatal(err)
}
// set the server configuration.
// set new server configuration.
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("%s", err)
}
return initTestServerWithBackend(ctx, t, testServer, objLayer, disks)
}
// initializes a test server with the given object layer and disks.
func initTestServerWithBackend(ctx context.Context, t TestErrHandler, testServer TestServer, objLayer ObjectLayer, disks []string) TestServer {
// Test Server needs to start before formatting of disks.
// Get credential.
credentials := globalActiveCred
testServer.Obj = objLayer
testServer.rawDiskPaths = disks
testServer.Disks = mustGetPoolEndpoints(disks...)
testServer.AccessKey = credentials.AccessKey
testServer.SecretKey = credentials.SecretKey