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 }