package freeipa import ( "crypto/tls" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "testing" "time" ) // Unused port for testing. const httpsPort = 8831 // Test login handler. func handleLogin(w http.ResponseWriter, req *http.Request) { // Logins are form data posts. req.ParseForm() // Check username/password equals test credentials. user := req.Form.Get("user") password := req.Form.Get("password") if user == "test" && password == "testpassword" { // Successful login send session cookie. cookie := http.Cookie{} cookie.Name = "ipa_session" cookie.Value = "correct-session-secret" cookie.Expires = time.Now().Add(30 * time.Minute) cookie.Secure = true cookie.HttpOnly = true cookie.Path = "/ipa" http.SetCookie(w, &cookie) w.Header().Set("IPASESSION", "correct-session-secret") } else { // Invalid login, send rejection. w.Header().Set("X-IPA-Rejection-Reason", "invalid-password") http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) fmt.Fprintf(w, ` 401 Unauthorized

Invalid Authentication

kinit: Password incorrect while getting initial credentials

`) } } // Send JSON file to HTTP request. func sendJSONFile(w http.ResponseWriter, filePath string) { f, err := os.Open(filePath) if err != nil { log.Fatalln(err) } defer f.Close() io.Copy(w, f) } // General invalid json error response for testing error handling. func sendInvalidJSON(w http.ResponseWriter) { sendJSONFile(w, "test/invalid_json.json") } // Handle the json session test request. func handleJSON(w http.ResponseWriter, req *http.Request) { // If session cookie doesn't exist, something is wrong. Send unauthenticated response. cookie, err := req.Cookie("ipa_session") if err != nil || cookie.Value != "correct-session-secret" { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } // Generally json response from here. w.Header().Set("Content-Type", "application/json") // Get the request body and parse it out. res := new(Request) err = json.NewDecoder(req.Body).Decode(res) if err != nil { // If the json decode fails, send the error. sendInvalidJSON(w) return } // For testing, we'll consider user_add/user_find as an accepted method, all others will error. if res.Method == "user_add" { // Send user add response data. sendJSONFile(w, "test/user_add_response.json") } else if res.Method == "user_find" { // Send user add response data. sendJSONFile(w, "test/user_find_response.json") } else { // An unexpected method received for testing, send error message. sendInvalidJSON(w) } } // General library tests with test server. func TestClient(t *testing.T) { // Spin up test server using port specified above. srvAddr := fmt.Sprintf("127.0.0.1:%d", httpsPort) http.HandleFunc("/ipa/session/login_password", handleLogin) http.HandleFunc("/ipa/session/json", handleJSON) isListening := make(chan bool) go func() { l, err := net.Listen("tcp", srvAddr) if err != nil { log.Fatal("Listen: ", err) } isListening <- true err = http.ServeTLS(l, nil, "test/cert.pem", "test/key.pem") if err != nil { log.Fatal("Serve: ", err) } }() // Allow the http server to initialize. <-isListening // Test server has a self signed certificate, ignore invalid certs. transportConfig := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } // Connect using wrong password to confirm invalid login responses are handled correctly. _, err := Connect(srvAddr, transportConfig, "test", "wrong-password") if err == nil || err.Error() != "login failed: unauthorized response (1201)" { t.Fatalf("expected login failure") } // Connect using correct password to confirm valid logins are handled correctly. client, err := Connect(srvAddr, transportConfig, "test", "testpassword") if err != nil { t.Fatalf("error: %s", err) } // Setup test user_add request. params := make(map[string]interface{}) params["givenname"] = "FreeIPA" params["sn"] = "Test" params["userpassword"] = "test-password" req := NewRequest( "user_add", []interface{}{"username"}, params, ) // Send the request to the test server. resp, err := client.Do(req) if err != nil { t.Fatalf("error: %s", err) } // Test reading bool key from response. v, _ := resp.GetBool("has_keytab") if !v { t.Errorf("expected true boolean") } // Test reading string from response. s, _ := resp.GetString("krbcanonicalname") if s != "username@EXAMPLE.COM" { t.Errorf("unexpected string: %s", s) } // Test reading date from response. d, _ := resp.GetDateTime("krblastpwdchange") year, month, day := d.Date() if year != 2023 || month != 8 || day != 10 { t.Errorf("unexpected date: %s", d) } // Test reading base64 data from response. b, _ := resp.GetData("krbextradata") if len(b) != 27 { t.Errorf("unexpected data: %v", b) } // Test reading a non-existant value from response. s, ok := resp.GetString("non-existant") if s != "" || ok { t.Errorf("expected empty string: %s", s) } a, ok := resp.GetStrings("objectclass") if !ok || len(a) != 13 { t.Errorf("unexpected data: %v", a) } // Test receiving an error message from the test server. req.Method = "invalid" _, err = client.Do(req) if err == nil || err.Error() != "JSONError (909): Invalid JSON-RPC request: Expecting property name enclosed in double quotes: line 4 column 4 (char 44)" { t.Fatalf("unexpected error: %s", err) } // Test user_find. params = make(map[string]interface{}) params["pkey_only"] = true params["sizelimit"] = 0 req = NewRequest( "user_find", []interface{}{""}, params, ) // Send the request to the test server. resp, err = client.Do(req) if err != nil { t.Fatalf("error: %s", err) } // The response should have a count as its an array. if resp.Result.Count != 2 { t.Error("expected 2 users") } // Confirm the array actually counts the same. if resp.CountResults() != 2 { t.Error("expected 2 users") } // Confirm an string at index works withou the array encapsulation. dn, _ := resp.GetStringAtIndex(0, "dn") if dn != "uid=admin,cn=users,cn=accounts,dc=example,dc=com" { t.Errorf("unexpected string: %s", dn) } // Confirm the UID string at index works because it is array encapsulated. uid, _ := resp.GetStringAtIndex(1, "uid") if uid != "johnny.bravo" { t.Errorf("unexpected string: %s", uid) } }