github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/saml_auth_unit_test.go (about)

     1  //go:build unit || ALL
     2  
     3  /*
     4   * Copyright 2020 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     5   */
     6  
     7  package govcd
     8  
     9  import (
    10  	"io"
    11  	"log"
    12  	"net/http"
    13  	"net/http/httptest"
    14  	"net/url"
    15  	"os"
    16  	"regexp"
    17  	"testing"
    18  )
    19  
    20  // testVcdMockAuthToken is the expected vcdCli.Client.VCDToken value after `Authentication()`
    21  // function passes mock SAML authentication process
    22  // #nosec G101 -- These credentials are fake for testing purposes
    23  const testVcdMockAuthToken = "e3b02b30b8ff4e87ac38db785b0172b5"
    24  
    25  // samlMockServer struct allows to attach HTTP handlers to use additional variables (like
    26  // *testing.T) inside those handlers
    27  type samlMockServer struct {
    28  	t *testing.T
    29  }
    30  
    31  // TestSamlAdfsAuthenticate is a unit test using mock vCD and ADFS server endpoint to follow
    32  // complete SAML auth flow. The `testVcdMockAuthToken` is expected as an outcome token because
    33  // mock servers return static responses.
    34  //
    35  // Note. A test using real infrastructure is defined in `saml_auth_test.go`
    36  func TestSamlAdfsAuthenticate(t *testing.T) {
    37  	// Spawn mock ADFS server
    38  	adfsServer := testSpawnAdfsServer(t)
    39  	adfsServerHost := adfsServer.URL
    40  	defer adfsServer.Close()
    41  
    42  	// Spawn mock vCD instance just enough to cover login details
    43  	vcdServer := spawnVcdServer(t, adfsServerHost, "my-org")
    44  	vcdServerHost := vcdServer.URL
    45  	defer vcdServer.Close()
    46  
    47  	// Setup vCD client pointing to mock API
    48  	vcdUrl, err := url.Parse(vcdServerHost + "/api")
    49  	if err != nil {
    50  		t.Errorf("got errors: %s", err)
    51  	}
    52  	vcdCli := NewVCDClient(*vcdUrl, true, WithSamlAdfs(true, ""))
    53  	err = vcdCli.Authenticate("fakeUser", "fakePass", "my-org")
    54  	if err != nil {
    55  		t.Errorf("got errors: %s", err)
    56  	}
    57  
    58  	// After authentication
    59  	if vcdCli.Client.VCDToken != testVcdMockAuthToken {
    60  		t.Errorf("received token does not match specified one")
    61  	}
    62  }
    63  
    64  // spawnVcdServer establishes a mock vCD server with endpoints required to satisfy authentication
    65  func spawnVcdServer(t *testing.T, adfsServerHost, org string) *httptest.Server {
    66  	mockServer := samlMockServer{t}
    67  	mux := http.NewServeMux()
    68  	mux.HandleFunc("/cloud/org/"+org+"/saml/metadata/alias/vcd", mockServer.vCDSamlMetadataHandler)
    69  	mux.HandleFunc("/login/"+org+"/saml/login/alias/vcd", mockServer.getVcdAdfsRedirectHandler(adfsServerHost))
    70  	mux.HandleFunc("/api/sessions", mockServer.vCDLoginHandler)
    71  	mux.HandleFunc("/api/versions", mockServer.vCDApiVersionHandler)
    72  	mux.HandleFunc("/api/org", mockServer.vCDApiOrgHandler)
    73  
    74  	server := httptest.NewTLSServer(mux)
    75  	if os.Getenv("GOVCD_DEBUG") != "" {
    76  		log.Printf("vCD mock server now listening on %s...\n", server.URL)
    77  	}
    78  	return server
    79  }
    80  
    81  // vcdLoginHandler serves mock "/api/sessions"
    82  func (mockServer *samlMockServer) vCDLoginHandler(w http.ResponseWriter, r *http.Request) {
    83  	// We expect POST method and not anything else
    84  	if r.Method != http.MethodPost {
    85  		w.WriteHeader(500)
    86  		return
    87  	}
    88  
    89  	expectedHeader := goldenString(mockServer.t, "REQ_api_sessions", "", false)
    90  	if r.Header.Get("Authorization") != expectedHeader {
    91  		w.WriteHeader(500)
    92  		return
    93  	}
    94  
    95  	headers := w.Header()
    96  	headers.Add("X-Vcloud-Authorization", testVcdMockAuthToken)
    97  
    98  	resp := goldenBytes(mockServer.t, "RESP_api_sessions", []byte{}, false)
    99  	_, err := w.Write(resp)
   100  	if err != nil {
   101  		panic(err)
   102  	}
   103  }
   104  
   105  // vCDApiVersionHandler server mock "/api/versions"
   106  func (mockServer *samlMockServer) vCDApiVersionHandler(w http.ResponseWriter, r *http.Request) {
   107  	// We expect GET method and not anything else
   108  	if r.Method != http.MethodGet {
   109  		w.WriteHeader(500)
   110  		return
   111  	}
   112  
   113  	resp := goldenBytes(mockServer.t, "RESP_api_versions", []byte{}, false)
   114  	_, err := w.Write(resp)
   115  	if err != nil {
   116  		panic(err)
   117  	}
   118  }
   119  
   120  // vCDApiOrgHandler serves mock "/api/org"
   121  func (mockServer *samlMockServer) vCDApiOrgHandler(w http.ResponseWriter, r *http.Request) {
   122  	// We expect GET method and not anything else
   123  	if r.Method != http.MethodGet {
   124  		w.WriteHeader(500)
   125  		return
   126  	}
   127  
   128  	resp := goldenBytes(mockServer.t, "RESP_api_org", []byte{}, false)
   129  	_, err := w.Write(resp)
   130  	if err != nil {
   131  		panic(err)
   132  	}
   133  }
   134  
   135  // vCDSamlMetadataHandler serves mock "/cloud/org/" + org + "/saml/metadata/alias/vcd"
   136  func (mockServer *samlMockServer) vCDSamlMetadataHandler(w http.ResponseWriter, r *http.Request) {
   137  	re := goldenBytes(mockServer.t, "RESP_cloud_org_my-org_saml_metadata_alias_vcd", []byte{}, false)
   138  	_, _ = w.Write(re)
   139  }
   140  func (mockServer *samlMockServer) getVcdAdfsRedirectHandler(adfsServerHost string) func(w http.ResponseWriter, r *http.Request) {
   141  	return func(w http.ResponseWriter, r *http.Request) {
   142  		if r.Method != http.MethodGet {
   143  			w.WriteHeader(500)
   144  			return
   145  		}
   146  		headers := w.Header()
   147  		locationHeaderPayload := goldenString(mockServer.t, "RESP_HEADER_login_my-org_saml_login_alias_vcd", "", false)
   148  		headers.Add("Location", adfsServerHost+locationHeaderPayload)
   149  
   150  		w.WriteHeader(http.StatusFound)
   151  	}
   152  }
   153  
   154  // testSpawnAdfsServer spawns mock HTTPS server to server ADFS auth endpoint
   155  // "/adfs/services/trust/13/usernamemixed"
   156  func testSpawnAdfsServer(t *testing.T) *httptest.Server {
   157  	mockServer := samlMockServer{t}
   158  	mux := http.NewServeMux()
   159  	mux.HandleFunc("/adfs/services/trust/13/usernamemixed", mockServer.adfsSamlAuthHandler)
   160  	server := httptest.NewTLSServer(mux)
   161  	if os.Getenv("GOVCD_DEBUG") != "" {
   162  		log.Printf("ADFS mock server now listening on %s...\n", server.URL)
   163  	}
   164  	return server
   165  }
   166  
   167  // adfsSamlAuthHandler checks that POST request with expected payload is sent and serves response
   168  // sample ADFS response
   169  func (mockServer *samlMockServer) adfsSamlAuthHandler(w http.ResponseWriter, r *http.Request) {
   170  	// it must be POST method and not anything else
   171  	if r.Method != http.MethodPost {
   172  		w.WriteHeader(500)
   173  		return
   174  	}
   175  
   176  	// Replace known dynamic strings to 'REPLACED' string
   177  	gotBody, _ := io.ReadAll(r.Body)
   178  	gotBodyString := string(gotBody)
   179  	re := regexp.MustCompile(`(<a:To s:mustUnderstand="1">).*(</a:To>)`)
   180  	gotBodyString = re.ReplaceAllString(gotBodyString, `${1}REPLACED${2}`)
   181  
   182  	re2 := regexp.MustCompile(`(<u:Created>).*(</u:Created>)`)
   183  	gotBodyString = re2.ReplaceAllString(gotBodyString, `${1}REPLACED${2}`)
   184  
   185  	re3 := regexp.MustCompile(`(<u:Expires>).*(</u:Expires>)`)
   186  	gotBodyString = re3.ReplaceAllString(gotBodyString, `${1}REPLACED${2}`)
   187  
   188  	expectedBody := goldenString(mockServer.t, "REQ_adfs_services_trust_13_usernamemixed", gotBodyString, false)
   189  	if gotBodyString != expectedBody {
   190  		w.WriteHeader(500)
   191  		return
   192  	}
   193  
   194  	resp := goldenBytes(mockServer.t, "RESP_adfs_services_trust_13_usernamemixed", []byte(""), false)
   195  	_, err := w.Write(resp)
   196  	if err != nil {
   197  		panic(err)
   198  	}
   199  }