Initial commit
This commit is contained in:
		
						commit
						cb4d9a5c2e
					
				
							
								
								
									
										19
									
								
								License.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								License.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					Copyright (c) 2023 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights
 | 
				
			||||||
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					SOFTWARE.
 | 
				
			||||||
							
								
								
									
										61
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					# go-freeipa
 | 
				
			||||||
 | 
					A FreeIPA API client library for GoLang.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Install
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go get github.com/grmrgecko/go-freeipa
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Example
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
					    "crypto/tls"
 | 
				
			||||||
 | 
					    "log"
 | 
				
			||||||
 | 
					    "net/http"
 | 
				
			||||||
 | 
					    freeipa "github.com/grmrgecko/go-freeipa"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
					    // Setup TLS configurations.
 | 
				
			||||||
 | 
					    tlsConifg := tls.Config{InsecureSkipVerify: false}
 | 
				
			||||||
 | 
					    transportConfig := &http.Transport{
 | 
				
			||||||
 | 
					        TLSClientConfig: &tlsConifg,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Connect/login to FreeIPA server.
 | 
				
			||||||
 | 
					    client, err := freeipa.Connect("ipa.example.com", transportConfig, "username", "password")
 | 
				
			||||||
 | 
					    if err!=nil {
 | 
				
			||||||
 | 
					        log.Fatalln(err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make a user.
 | 
				
			||||||
 | 
					    params := make(map[string]interface{})
 | 
				
			||||||
 | 
					    params["pkey_only"] = true
 | 
				
			||||||
 | 
					    params["sizelimit"] = 0
 | 
				
			||||||
 | 
					    req := freeipa.NewRequest(
 | 
				
			||||||
 | 
					        "user_find",
 | 
				
			||||||
 | 
					        []interface{}{""},
 | 
				
			||||||
 | 
					        params,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send the request to the test server.
 | 
				
			||||||
 | 
					    resp, err := client.Do(req)
 | 
				
			||||||
 | 
					    if err != nil {
 | 
				
			||||||
 | 
					        log.Fatalln(err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Print information about response.
 | 
				
			||||||
 | 
					    log.Println("Found users:", resp.Result.Count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dn, ok := resp.GetStringAtIndex(0, "dn")
 | 
				
			||||||
 | 
					    if !ok {
 | 
				
			||||||
 | 
					        log.Fatalln("Unable to get dn value from FreeIPA")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.Println("Got first user DN:", dn)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## References
 | 
				
			||||||
 | 
					If you're looking for help on what API methods there are and the arguments they accept, the documentation at FreeIPA should help:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[https://github.com/freeipa/freeipa/tree/master/doc/api](https://github.com/freeipa/freeipa/tree/master/doc/api)
 | 
				
			||||||
							
								
								
									
										178
									
								
								client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								client.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					package freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/cookiejar"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						krb5client "github.com/jcmturner/gokrb5/v8/client"
 | 
				
			||||||
 | 
						krb5config "github.com/jcmturner/gokrb5/v8/config"
 | 
				
			||||||
 | 
						"github.com/jcmturner/gokrb5/v8/keytab"
 | 
				
			||||||
 | 
						"github.com/jcmturner/gokrb5/v8/spnego"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Client: The base object for connections to FreeIPA API.
 | 
				
			||||||
 | 
					type Client struct {
 | 
				
			||||||
 | 
						uriBase  string
 | 
				
			||||||
 | 
						client   *http.Client
 | 
				
			||||||
 | 
						user     string
 | 
				
			||||||
 | 
						password string
 | 
				
			||||||
 | 
						krb5     *krb5client.Client
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// init: Common init code for each connection type, mainly sets http.Client and uriBase.
 | 
				
			||||||
 | 
					func (c *Client) init(host string, transport *http.Transport) error {
 | 
				
			||||||
 | 
						// Create a cookie jar to store FreeIPA session cookies.
 | 
				
			||||||
 | 
						jar, err := cookiejar.New(&cookiejar.Options{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Setup client using provided transport configurations and the cookie jar.
 | 
				
			||||||
 | 
						c.client = &http.Client{
 | 
				
			||||||
 | 
							Transport: transport,
 | 
				
			||||||
 | 
							Jar:       jar,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set uriBase using the provided host and test to verify a valid URL is produced.
 | 
				
			||||||
 | 
						c.uriBase = fmt.Sprintf("https://%s/ipa", host)
 | 
				
			||||||
 | 
						_, err = url.Parse(c.uriBase)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Connect: Make a new client using standard username/password login.
 | 
				
			||||||
 | 
					func Connect(host string, transport *http.Transport, user, password string) (*Client, error) {
 | 
				
			||||||
 | 
						// Make the client config and save credentials.
 | 
				
			||||||
 | 
						client := &Client{
 | 
				
			||||||
 | 
							user:     user,
 | 
				
			||||||
 | 
							password: password,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize common configurations.
 | 
				
			||||||
 | 
						err := client.init(host, transport)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Login using credentials.
 | 
				
			||||||
 | 
						err = client.login()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("login failed: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return client, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// login: Login using standard credentials.
 | 
				
			||||||
 | 
					func (c *Client) login() error {
 | 
				
			||||||
 | 
						// If login is called, but kerberos client is configured, use kerberos login instead.
 | 
				
			||||||
 | 
						// This allows standard re-authentication calls to work with both kerbeos and standard authenciation.
 | 
				
			||||||
 | 
						if c.krb5 != nil {
 | 
				
			||||||
 | 
							return c.loginWithKerberos()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Setup form data with credentials.
 | 
				
			||||||
 | 
						data := url.Values{
 | 
				
			||||||
 | 
							"user":     []string{c.user},
 | 
				
			||||||
 | 
							"password": []string{c.password},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Authenticate using standard credentials with the http client.
 | 
				
			||||||
 | 
						res, e := c.client.PostForm(c.uriBase+"/session/login_password", data)
 | 
				
			||||||
 | 
						if e != nil {
 | 
				
			||||||
 | 
							return e
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If an error occurs, provide details if possible on why.
 | 
				
			||||||
 | 
						if res.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							if res.StatusCode == http.StatusUnauthorized {
 | 
				
			||||||
 | 
								return unauthorizedHTTPError(res)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fmt.Errorf("unexpected http status code: %d", res.StatusCode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Successful authentication.
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KerberosConnectOptions: Options for connecting to Kerberos.
 | 
				
			||||||
 | 
					type KerberosConnectOptions struct {
 | 
				
			||||||
 | 
						Krb5ConfigReader io.Reader
 | 
				
			||||||
 | 
						KeytabReader     io.Reader
 | 
				
			||||||
 | 
						User             string
 | 
				
			||||||
 | 
						Realm            string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConnectWithKerberos: Create a new client using Kerberos authentication.
 | 
				
			||||||
 | 
					func ConnectWithKerberos(host string, transport *http.Transport, options *KerberosConnectOptions) (*Client, error) {
 | 
				
			||||||
 | 
						// Read the kerberos configuration file for server connection information.
 | 
				
			||||||
 | 
						krb5Config, err := krb5config.NewFromReader(options.Krb5ConfigReader)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error reading kerberos configuration: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Read the keytab data.
 | 
				
			||||||
 | 
						ktData, err := io.ReadAll(options.KeytabReader)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error reading keytab: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse the keytab data.
 | 
				
			||||||
 | 
						kt := keytab.New()
 | 
				
			||||||
 | 
						err = kt.Unmarshal(ktData)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error parsing keytab: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Setup kerberos client with keytab and config.
 | 
				
			||||||
 | 
						krb5 := krb5client.NewWithKeytab(options.User, options.Realm, kt, krb5Config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Setup the client with kerberos's client for authentication.
 | 
				
			||||||
 | 
						client := &Client{
 | 
				
			||||||
 | 
							user: options.User,
 | 
				
			||||||
 | 
							krb5: krb5,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the common configurations.
 | 
				
			||||||
 | 
						err = client.init(host, transport)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Login using kerberos authentication.
 | 
				
			||||||
 | 
						err = client.login()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("login failed: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return client, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// loginWithKerberos: Authenticate using kerberos client.
 | 
				
			||||||
 | 
					func (c *Client) loginWithKerberos() error {
 | 
				
			||||||
 | 
						// Wrapper for authenticating with Kerberos credentials.
 | 
				
			||||||
 | 
						spnegoCl := spnego.NewClient(c.krb5, c.client, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Setup request for authenticate.
 | 
				
			||||||
 | 
						req, err := http.NewRequest("POST", c.uriBase+"/session/login_kerberos", nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error building login request: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req.Header.Add("Referer", c.uriBase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Perform authenticate using Kerberos.
 | 
				
			||||||
 | 
						res, err := spnegoCl.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error logging in using Kerberos: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If an error occurs, return it.
 | 
				
			||||||
 | 
						if res.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return fmt.Errorf("unexpected http status code: %d", res.StatusCode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Successful authentication.
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										239
									
								
								client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								client_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,239 @@
 | 
				
			|||||||
 | 
					package freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Unused port for testing.
 | 
				
			||||||
 | 
					const httpsPort = 8831
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleLogin: 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, `<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					<title>401 Unauthorized</title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<h1>Invalid Authentication</h1>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
					<strong>kinit: Password incorrect while getting initial credentials
 | 
				
			||||||
 | 
					</strong>
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendInvalidJSON: General invalid json error response for testing error handling.
 | 
				
			||||||
 | 
					func sendInvalidJSON(w http.ResponseWriter) {
 | 
				
			||||||
 | 
						f, err := os.Open("test/invalid_json.json")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer f.Close()
 | 
				
			||||||
 | 
						io.Copy(w, f)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleJSON: 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.
 | 
				
			||||||
 | 
							f, err := os.Open("test/user_add_response.json")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer f.Close()
 | 
				
			||||||
 | 
							io.Copy(w, f)
 | 
				
			||||||
 | 
						} else if res.Method == "user_find" {
 | 
				
			||||||
 | 
							// Send user add response data.
 | 
				
			||||||
 | 
							f, err := os.Open("test/user_find_response.json")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer f.Close()
 | 
				
			||||||
 | 
							io.Copy(w, f)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// An unexpected method received for testing, send error message.
 | 
				
			||||||
 | 
							sendInvalidJSON(w)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestLogin: General library tests with test server.
 | 
				
			||||||
 | 
					func TestLogin(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)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							err := http.ListenAndServeTLS(srvAddr, "test/cert.pem", "test/key.pem", nil)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatal("ListenAndServe: ", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						// Allow the http server to initialize.
 | 
				
			||||||
 | 
						time.Sleep(100 * time.Millisecond)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 <invalid-password> (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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										162
									
								
								errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								errors.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					package freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Standard FreeIPA error codes.
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PublicErrorCode                           = 900
 | 
				
			||||||
 | 
						VersionErrorCode                          = 901
 | 
				
			||||||
 | 
						UnknownErrorCode                          = 902
 | 
				
			||||||
 | 
						InternalErrorCode                         = 903
 | 
				
			||||||
 | 
						ServerInternalErrorCode                   = 904
 | 
				
			||||||
 | 
						CommandErrorCode                          = 905
 | 
				
			||||||
 | 
						ServerCommandErrorCode                    = 906
 | 
				
			||||||
 | 
						NetworkErrorCode                          = 907
 | 
				
			||||||
 | 
						ServerNetworkErrorCode                    = 908
 | 
				
			||||||
 | 
						JSONErrorCode                             = 909
 | 
				
			||||||
 | 
						XMLRPCMarshallErrorCode                   = 910
 | 
				
			||||||
 | 
						RefererErrorCode                          = 911
 | 
				
			||||||
 | 
						EnvironmentErrorCode                      = 912
 | 
				
			||||||
 | 
						SystemEncodingErrorCode                   = 913
 | 
				
			||||||
 | 
						AuthenticationErrorCode                   = 1000
 | 
				
			||||||
 | 
						KerberosErrorCode                         = 1100
 | 
				
			||||||
 | 
						CCacheErrorCode                           = 1101
 | 
				
			||||||
 | 
						ServiceErrorCode                          = 1102
 | 
				
			||||||
 | 
						NoCCacheErrorCode                         = 1103
 | 
				
			||||||
 | 
						TicketExpiredCode                         = 1104
 | 
				
			||||||
 | 
						BadCCachePermsCode                        = 1105
 | 
				
			||||||
 | 
						BadCCacheFormatCode                       = 1106
 | 
				
			||||||
 | 
						CannotResolveKDCCode                      = 1107
 | 
				
			||||||
 | 
						SessionErrorCode                          = 1200
 | 
				
			||||||
 | 
						InvalidSessionPasswordCode                = 1201
 | 
				
			||||||
 | 
						PasswordExpiredCode                       = 1202
 | 
				
			||||||
 | 
						KrbPrincipalExpiredCode                   = 1203
 | 
				
			||||||
 | 
						UserLockedCode                            = 1204
 | 
				
			||||||
 | 
						AuthorizationErrorCode                    = 2000
 | 
				
			||||||
 | 
						ACIErrorCode                              = 2100
 | 
				
			||||||
 | 
						InvocationErrorCode                       = 3000
 | 
				
			||||||
 | 
						EncodingErrorCode                         = 3001
 | 
				
			||||||
 | 
						BinaryEncodingErrorCode                   = 3002
 | 
				
			||||||
 | 
						ZeroArgumentErrorCode                     = 3003
 | 
				
			||||||
 | 
						MaxArgumentErrorCode                      = 3004
 | 
				
			||||||
 | 
						OptionErrorCode                           = 3005
 | 
				
			||||||
 | 
						OverlapErrorCode                          = 3006
 | 
				
			||||||
 | 
						RequirementErrorCode                      = 3007
 | 
				
			||||||
 | 
						ConversionErrorCode                       = 3008
 | 
				
			||||||
 | 
						ValidationErrorCode                       = 3009
 | 
				
			||||||
 | 
						NoSuchNamespaceErrorCode                  = 3010
 | 
				
			||||||
 | 
						PasswordMismatchCode                      = 3011
 | 
				
			||||||
 | 
						NotImplementedErrorCode                   = 3012
 | 
				
			||||||
 | 
						NotConfiguredErrorCode                    = 3013
 | 
				
			||||||
 | 
						PromptFailedCode                          = 3014
 | 
				
			||||||
 | 
						DeprecationErrorCode                      = 3015
 | 
				
			||||||
 | 
						NotAForestRootErrorCode                   = 3016
 | 
				
			||||||
 | 
						ExecutionErrorCode                        = 4000
 | 
				
			||||||
 | 
						NotFoundCode                              = 4001
 | 
				
			||||||
 | 
						DuplicateEntryCode                        = 4002
 | 
				
			||||||
 | 
						HostServiceCode                           = 4003
 | 
				
			||||||
 | 
						MalformedServicePrincipalCode             = 4004
 | 
				
			||||||
 | 
						RealmMismatchCode                         = 4005
 | 
				
			||||||
 | 
						RequiresRootCode                          = 4006
 | 
				
			||||||
 | 
						AlreadyPosixGroupCode                     = 4007
 | 
				
			||||||
 | 
						MalformedUserPrincipalCode                = 4008
 | 
				
			||||||
 | 
						AlreadyActiveCode                         = 4009
 | 
				
			||||||
 | 
						AlreadyInactiveCode                       = 4010
 | 
				
			||||||
 | 
						HasNSAccountLockCode                      = 4011
 | 
				
			||||||
 | 
						NotGroupMemberCode                        = 4012
 | 
				
			||||||
 | 
						RecursiveGroupCode                        = 4013
 | 
				
			||||||
 | 
						AlreadyGroupMemberCode                    = 4014
 | 
				
			||||||
 | 
						Base64DecodeErrorCode                     = 4015
 | 
				
			||||||
 | 
						RemoteRetrieveErrorCode                   = 4016
 | 
				
			||||||
 | 
						SameGroupErrorCode                        = 4017
 | 
				
			||||||
 | 
						DefaultGroupErrorCode                     = 4018
 | 
				
			||||||
 | 
						DNSNotARecordErrorCode                    = 4019
 | 
				
			||||||
 | 
						ManagedGroupErrorCode                     = 4020
 | 
				
			||||||
 | 
						ManagedPolicyErrorCode                    = 4021
 | 
				
			||||||
 | 
						FileErrorCode                             = 4022
 | 
				
			||||||
 | 
						NoCertificateErrorCode                    = 4023
 | 
				
			||||||
 | 
						ManagedGroupExistsErrorCode               = 4024
 | 
				
			||||||
 | 
						ReverseMemberErrorCode                    = 4025
 | 
				
			||||||
 | 
						AttrValueNotFoundCode                     = 4026
 | 
				
			||||||
 | 
						SingleMatchExpectedCode                   = 4027
 | 
				
			||||||
 | 
						AlreadyExternalGroupCode                  = 4028
 | 
				
			||||||
 | 
						ExternalGroupViolationCode                = 4029
 | 
				
			||||||
 | 
						PosixGroupViolationCode                   = 4030
 | 
				
			||||||
 | 
						EmptyResultCode                           = 4031
 | 
				
			||||||
 | 
						InvalidDomainLevelErrorCode               = 4032
 | 
				
			||||||
 | 
						ServerRemovalErrorCode                    = 4033
 | 
				
			||||||
 | 
						OperationNotSupportedForPrincipalTypeCode = 4034
 | 
				
			||||||
 | 
						HTTPRequestErrorCode                      = 4035
 | 
				
			||||||
 | 
						RedundantMappingRuleCode                  = 4036
 | 
				
			||||||
 | 
						CSRTemplateErrorCode                      = 4037
 | 
				
			||||||
 | 
						AlreadyContainsValueErrorCode             = 4038
 | 
				
			||||||
 | 
						BuiltinErrorCode                          = 4100
 | 
				
			||||||
 | 
						HelpErrorCode                             = 4101
 | 
				
			||||||
 | 
						LDAPErrorCode                             = 4200
 | 
				
			||||||
 | 
						MidairCollisionCode                       = 4201
 | 
				
			||||||
 | 
						EmptyModlistCode                          = 4202
 | 
				
			||||||
 | 
						DatabaseErrorCode                         = 4203
 | 
				
			||||||
 | 
						LimitsExceededCode                        = 4204
 | 
				
			||||||
 | 
						ObjectclassViolationCode                  = 4205
 | 
				
			||||||
 | 
						NotAllowedOnRDNCode                       = 4206
 | 
				
			||||||
 | 
						OnlyOneValueAllowedCode                   = 4207
 | 
				
			||||||
 | 
						InvalidSyntaxCode                         = 4208
 | 
				
			||||||
 | 
						BadSearchFilterCode                       = 4209
 | 
				
			||||||
 | 
						NotAllowedOnNonLeafCode                   = 4210
 | 
				
			||||||
 | 
						DatabaseTimeoutCode                       = 4211
 | 
				
			||||||
 | 
						DNSDataMismatchCode                       = 4212
 | 
				
			||||||
 | 
						TaskTimeoutCode                           = 4213
 | 
				
			||||||
 | 
						TimeLimitExceededCode                     = 4214
 | 
				
			||||||
 | 
						SizeLimitExceededCode                     = 4215
 | 
				
			||||||
 | 
						AdminLimitExceededCode                    = 4216
 | 
				
			||||||
 | 
						CertificateErrorCode                      = 4300
 | 
				
			||||||
 | 
						CertificateOperationErrorCode             = 4301
 | 
				
			||||||
 | 
						CertificateFormatErrorCode                = 4302
 | 
				
			||||||
 | 
						MutuallyExclusiveErrorCode                = 4303
 | 
				
			||||||
 | 
						NonFatalErrorCode                         = 4304
 | 
				
			||||||
 | 
						AlreadyRegisteredErrorCode                = 4305
 | 
				
			||||||
 | 
						NotRegisteredErrorCode                    = 4306
 | 
				
			||||||
 | 
						DependentEntryCode                        = 4307
 | 
				
			||||||
 | 
						LastMemberErrorCode                       = 4308
 | 
				
			||||||
 | 
						ProtectedEntryErrorCode                   = 4309
 | 
				
			||||||
 | 
						CertificateInvalidErrorCode               = 4310
 | 
				
			||||||
 | 
						SchemaUpToDateCode                        = 4311
 | 
				
			||||||
 | 
						DNSErrorCode                              = 4400
 | 
				
			||||||
 | 
						DNSResolverErrorCode                      = 4401
 | 
				
			||||||
 | 
						TrustErrorCode                            = 4500
 | 
				
			||||||
 | 
						TrustTopologyConflictErrorCode            = 4501
 | 
				
			||||||
 | 
						GenericErrorCode                          = 5000
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authentication rejection reasons.
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						passwordExpiredUnauthorizedReason        = "password-expired"
 | 
				
			||||||
 | 
						invalidSessionPasswordUnauthorizedReason = "invalid-password"
 | 
				
			||||||
 | 
						krbPrincipalExpiredUnauthorizedReason    = "krbprincipal-expired"
 | 
				
			||||||
 | 
						userLockedUnauthorizedReason             = "user-locked"
 | 
				
			||||||
 | 
						rejectionReasonHTTPHeader                = "X-Ipa-Rejection-Reason"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// unauthorizedHTTPError: Add information from the rejection reason header to unauthorized error.
 | 
				
			||||||
 | 
					func unauthorizedHTTPError(resp *http.Response) error {
 | 
				
			||||||
 | 
						var errorCode int
 | 
				
			||||||
 | 
						rejectionReason := resp.Header.Get(rejectionReasonHTTPHeader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch rejectionReason {
 | 
				
			||||||
 | 
						case passwordExpiredUnauthorizedReason:
 | 
				
			||||||
 | 
							errorCode = PasswordExpiredCode
 | 
				
			||||||
 | 
						case invalidSessionPasswordUnauthorizedReason:
 | 
				
			||||||
 | 
							errorCode = InvalidSessionPasswordCode
 | 
				
			||||||
 | 
						case krbPrincipalExpiredUnauthorizedReason:
 | 
				
			||||||
 | 
							errorCode = KrbPrincipalExpiredCode
 | 
				
			||||||
 | 
						case userLockedUnauthorizedReason:
 | 
				
			||||||
 | 
							errorCode = UserLockedCode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							errorCode = GenericErrorCode
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Errorf("unauthorized response <%s> (%d)", rejectionReason, errorCode)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					module github.com/grmrgecko/go-freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require github.com/jcmturner/gokrb5/v8 v8.4.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/hashicorp/go-uuid v1.0.3 // indirect
 | 
				
			||||||
 | 
						github.com/jcmturner/aescts/v2 v2.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/jcmturner/gofork v1.7.6 // indirect
 | 
				
			||||||
 | 
						github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
 | 
				
			||||||
 | 
						github.com/jcmturner/rpc/v2 v2.0.3 // indirect
 | 
				
			||||||
 | 
						golang.org/x/crypto v0.6.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/net v0.7.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										69
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
 | 
				
			||||||
 | 
					github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 | 
				
			||||||
 | 
					github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
 | 
				
			||||||
 | 
					github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 | 
				
			||||||
 | 
					github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 | 
				
			||||||
 | 
					github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
 | 
				
			||||||
 | 
					github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 | 
				
			||||||
 | 
					github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
 | 
				
			||||||
 | 
					github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
 | 
				
			||||||
 | 
					github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
 | 
				
			||||||
 | 
					github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
 | 
				
			||||||
 | 
					github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
 | 
				
			||||||
 | 
					github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
 | 
				
			||||||
 | 
					github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
 | 
				
			||||||
 | 
					github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
 | 
				
			||||||
 | 
					github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
 | 
				
			||||||
 | 
					github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
 | 
				
			||||||
 | 
					github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
 | 
				
			||||||
 | 
					github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
				
			||||||
 | 
					github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
				
			||||||
 | 
					golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 | 
				
			||||||
 | 
					golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
 | 
				
			||||||
 | 
					golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
				
			||||||
 | 
					golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
							
								
								
									
										89
									
								
								request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								request.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					package freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Standard API version definitation.
 | 
				
			||||||
 | 
					var apiVersion = "2.237"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Request format.
 | 
				
			||||||
 | 
					type Request struct {
 | 
				
			||||||
 | 
						Method string        `json:"method"`
 | 
				
			||||||
 | 
						Params []interface{} `json:"params"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewRequest: Create a new request providing method, args, and parameters.
 | 
				
			||||||
 | 
					func NewRequest(method string, args []interface{}, parms map[string]interface{}) *Request {
 | 
				
			||||||
 | 
						// Add API version to the parameters.
 | 
				
			||||||
 | 
						parms["version"] = apiVersion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the request.
 | 
				
			||||||
 | 
						req := &Request{
 | 
				
			||||||
 | 
							Method: method,
 | 
				
			||||||
 | 
							Params: []interface{}{
 | 
				
			||||||
 | 
								args,
 | 
				
			||||||
 | 
								parms,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return new request.
 | 
				
			||||||
 | 
						return req
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Do: Have the client do the request.
 | 
				
			||||||
 | 
					func (c *Client) Do(req *Request) (*Response, error) {
 | 
				
			||||||
 | 
						// Send request.
 | 
				
			||||||
 | 
						res, err := c.sendRequest(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer res.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If request is unauthorized, attempt to re-authenticate.
 | 
				
			||||||
 | 
						if res.StatusCode == http.StatusUnauthorized {
 | 
				
			||||||
 | 
							// Login.
 | 
				
			||||||
 | 
							err = c.login()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("renewed login failed: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Re-send the request, now that we're authenticated.
 | 
				
			||||||
 | 
							res, err = c.sendRequest(req)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We expect a 200 response, otherwise re-authentication failed or some other error occured.
 | 
				
			||||||
 | 
						if res.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("unexpected http status code: %d", res.StatusCode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse the response from the body.
 | 
				
			||||||
 | 
						return ParseResponse(res.Body)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// sendRequest: Encode and send the request to the session.
 | 
				
			||||||
 | 
					func (c *Client) sendRequest(request *Request) (*http.Response, error) {
 | 
				
			||||||
 | 
						// Encode to JSON.
 | 
				
			||||||
 | 
						data, err := json.Marshal(request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make request with JSON data.
 | 
				
			||||||
 | 
						req, err := http.NewRequest("POST", c.uriBase+"/session/json", bytes.NewBuffer(data))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req.Header.Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
						req.Header.Set("Accept", "application/json")
 | 
				
			||||||
 | 
						req.Header.Set("Referer", c.uriBase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Perform the request.
 | 
				
			||||||
 | 
						return c.client.Do(req)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										340
									
								
								response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								response.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,340 @@
 | 
				
			|||||||
 | 
					package freeipa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// General date/time format in LDAP.
 | 
				
			||||||
 | 
					// https://github.com/freeipa/freeipa/blob/ipa-4-7/ipalib/constants.py#L271
 | 
				
			||||||
 | 
					const LDAPGeneralizedTimeFormat = "20060102150405Z"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Message: Used in providing extra messages and error response.
 | 
				
			||||||
 | 
					type Message struct {
 | 
				
			||||||
 | 
						Type    string `json:"type"`
 | 
				
			||||||
 | 
						Message string `json:"message"`
 | 
				
			||||||
 | 
						Code    int    `json:"code"`
 | 
				
			||||||
 | 
						Name    string `json:"name"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// string: Convert the message into a combind string.
 | 
				
			||||||
 | 
					func (t *Message) string() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("%v (%v): %v", t.Name, t.Code, t.Message)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Result: Standard result in response from FreeIPA.
 | 
				
			||||||
 | 
					type Result struct {
 | 
				
			||||||
 | 
						Count     int        `json:"count"`
 | 
				
			||||||
 | 
						Truncated bool       `json:"truncated"`
 | 
				
			||||||
 | 
						Messages  []*Message `json:"messages,omitempty"`
 | 
				
			||||||
 | 
						// This result differs depending on response,
 | 
				
			||||||
 | 
						// read the API documentation below for information.
 | 
				
			||||||
 | 
						// https://github.com/freeipa/freeipa/tree/master/doc/api
 | 
				
			||||||
 | 
						Result  interface{} `json:"result"`
 | 
				
			||||||
 | 
						Summary string      `json:"summary,omitempty"`
 | 
				
			||||||
 | 
						Value   string      `json:"value,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Response: Standard response from FreeIPA.
 | 
				
			||||||
 | 
					type Response struct {
 | 
				
			||||||
 | 
						Error     *Message `json:"error"`
 | 
				
			||||||
 | 
						Result    *Result  `json:"result"`
 | 
				
			||||||
 | 
						Version   string   `json:"version"`
 | 
				
			||||||
 | 
						Principal string   `json:"principal"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ParseResponse: Parse response from reader.
 | 
				
			||||||
 | 
					func ParseResponse(body io.Reader) (*Response, error) {
 | 
				
			||||||
 | 
						// Decode JSON response.
 | 
				
			||||||
 | 
						res := new(Response)
 | 
				
			||||||
 | 
						err := json.NewDecoder(body).Decode(res)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If an error was provided from the API, return it.
 | 
				
			||||||
 | 
						if res.Error != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf(res.Error.string())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// We expect result to be provided on a valid response.
 | 
				
			||||||
 | 
						if res.Result == nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no result in response")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// A valid response was decoded, return it.
 | 
				
			||||||
 | 
						return res, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BoolResult: Decode results which are boolean formatted, usually used to indicate success or state.
 | 
				
			||||||
 | 
					func (r *Response) BoolResult() bool {
 | 
				
			||||||
 | 
						if r.Result == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						v, ok := r.Result.Result.(bool)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Response) CountResults() int {
 | 
				
			||||||
 | 
						if r.Result == nil {
 | 
				
			||||||
 | 
							return -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						a, ok := r.Result.Result.([]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return len(a)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetAtIndex: Get an interface for a key.
 | 
				
			||||||
 | 
					func (r *Response) GetAtIndex(index int, key string) ([]interface{}, bool) {
 | 
				
			||||||
 | 
						if r.Result == nil {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						a, ok := r.Result.Result.([]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Make sure we don't overflow.
 | 
				
			||||||
 | 
						if len(a) < index {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						d := a[index]
 | 
				
			||||||
 | 
						dict, ok := d.(map[string]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						v, ok := dict[key]
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						a, ok = v.([]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							// Apparently FreeIPA sometimes returns a string outside of an array, so this catches that.
 | 
				
			||||||
 | 
							return []interface{}{v}, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return a, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get: Get an interface for a key.
 | 
				
			||||||
 | 
					func (r *Response) Get(key string) ([]interface{}, bool) {
 | 
				
			||||||
 | 
						if r.Result == nil {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						dict, ok := r.Result.Result.(map[string]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						v, ok := dict[key]
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						a, ok := v.([]interface{})
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							// Apparently FreeIPA sometimes returns a string outside of an array, so this catches that.
 | 
				
			||||||
 | 
							return []interface{}{v}, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return a, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetBoolProcess: Process bool element.
 | 
				
			||||||
 | 
					func (r *Response) GetBoolProcess(v interface{}) (bool, bool) {
 | 
				
			||||||
 | 
						a, ok := v.(bool)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return false, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return a, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetBoolAtIndex: Get a boolean from a key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetBoolAtIndex(index int, key string) (bool, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return false, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetBoolProcess(v[0])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetBool: Get a boolean from a key.
 | 
				
			||||||
 | 
					func (r *Response) GetBool(key string) (bool, bool) {
 | 
				
			||||||
 | 
						v, ok := r.Get(key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return false, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetBoolProcess(v[0])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetStringProcess: Process sub element with string.
 | 
				
			||||||
 | 
					func (r *Response) GetStringProcess(v []interface{}) ([]string, bool) {
 | 
				
			||||||
 | 
						var res []string
 | 
				
			||||||
 | 
						for _, p := range v {
 | 
				
			||||||
 | 
							s, ok := p.(string)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res = append(res, s)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetStringsAtIndex: Get string value for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetStringsAtIndex(index int, key string) ([]string, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return []string{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetStringProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetStrings: Get string value for key.
 | 
				
			||||||
 | 
					func (r *Response) GetStrings(key string) ([]string, bool) {
 | 
				
			||||||
 | 
						v, ok := r.Get(key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return []string{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetStringProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetStringAtIndex: Get string value for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetStringAtIndex(index int, key string) (string, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetStringsAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetString: Get string value for key.
 | 
				
			||||||
 | 
					func (r *Response) GetString(key string) (string, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetStrings(key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDataProcess: Process a sub element with bytes.
 | 
				
			||||||
 | 
					func (r *Response) GetDataProcess(v []interface{}) ([][]byte, bool) {
 | 
				
			||||||
 | 
						var res [][]byte
 | 
				
			||||||
 | 
						for _, p := range v {
 | 
				
			||||||
 | 
							var bytes []byte
 | 
				
			||||||
 | 
							dict, ok := p.(map[string]interface{})
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b, ok := dict["__base64__"]
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							s, ok := b.(string)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							bytes, err := base64.StdEncoding.DecodeString(s)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res = append(res, bytes)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDatasAtIndex: Get byte array for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetDatasAtIndex(index int, key string) ([][]byte, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return [][]byte{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetDataProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDatas: Get byte array for key.
 | 
				
			||||||
 | 
					func (r *Response) GetDatas(key string) ([][]byte, bool) {
 | 
				
			||||||
 | 
						v, ok := r.Get(key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return [][]byte{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetDataProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDataAtIndex: Get byte array for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetDataAtIndex(index int, key string) ([]byte, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetDatasAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return []byte{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetData: Get byte array for key.
 | 
				
			||||||
 | 
					func (r *Response) GetData(key string) ([]byte, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetDatas(key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return []byte{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDateTimeProcess: Process a sub element with a date/time value.
 | 
				
			||||||
 | 
					func (r *Response) GetDateTimeProcess(v []interface{}) ([]time.Time, bool) {
 | 
				
			||||||
 | 
						var res []time.Time
 | 
				
			||||||
 | 
						for _, p := range v {
 | 
				
			||||||
 | 
							dict, ok := p.(map[string]interface{})
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							d, ok := dict["__datetime__"]
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							s, ok := d.(string)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							dateTime, err := time.Parse(LDAPGeneralizedTimeFormat, s)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return res, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							res = append(res, dateTime)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return res, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDateTimesAtIndex: Get date time value for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetDateTimesAtIndex(index int, key string) ([]time.Time, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return []time.Time{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetDateTimeProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDateTimes: Get date time value for key.
 | 
				
			||||||
 | 
					func (r *Response) GetDateTimes(key string) ([]time.Time, bool) {
 | 
				
			||||||
 | 
						v, ok := r.Get(key)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return []time.Time{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return r.GetDateTimeProcess(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDateTimeAtIndex: Get date time value for key at an index.
 | 
				
			||||||
 | 
					func (r *Response) GetDateTimeAtIndex(index int, key string) (time.Time, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetDateTimesAtIndex(index, key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return time.Time{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDateTime: Get date time value for key.
 | 
				
			||||||
 | 
					func (r *Response) GetDateTime(key string) (time.Time, bool) {
 | 
				
			||||||
 | 
						v, ok := r.GetDateTimes(key)
 | 
				
			||||||
 | 
						if !ok || len(v) < 1 {
 | 
				
			||||||
 | 
							return time.Time{}, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								test/cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/cert.pem
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIICMjCCAZugAwIBAgIQEAkA4KUMlYMXTLf8HKWnNzANBgkqhkiG9w0BAQsFADAS
 | 
				
			||||||
 | 
					MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
 | 
				
			||||||
 | 
					MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
 | 
				
			||||||
 | 
					iQKBgQCuulmxBijxcvxHoK43bPjvsCcSjaItYouIYbfM1WKIm4GBMbKRwCYNSQav
 | 
				
			||||||
 | 
					CirwgSiIEHtF4Xtzz/8ObNCPE46o2/0p9C925KzXmdpNlVPiXGOEY4R0ReHF6FjE
 | 
				
			||||||
 | 
					u8oa/imgSxPsfd4rg4tY1YIdeT28+7nzTqnW9s64m539mpg+JwIDAQABo4GGMIGD
 | 
				
			||||||
 | 
					MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8E
 | 
				
			||||||
 | 
					BTADAQH/MB0GA1UdDgQWBBSh/VW/iZWe+Fvd2ZFHApB8uj8EczAsBgNVHREEJTAj
 | 
				
			||||||
 | 
					gglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQADgYEAMvOwyek82nbjgE2dUmh2pYuE115iRmCOv3NoxLqq0XWYTfyqi0I2PTGU
 | 
				
			||||||
 | 
					Q5fmi1KNY075KxMN9PHHDeJwmUb10tu7ghkKe/6Il71eOvjQmKtsATLpad6dmHFF
 | 
				
			||||||
 | 
					6ormGkTzz3OPiz5whzZrdlonFgGdHPwHJqy9MTlDw+8ZH/x5RfA=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										14
									
								
								test/invalid_json.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/invalid_json.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"result": null,
 | 
				
			||||||
 | 
						"version": "4.6.8",
 | 
				
			||||||
 | 
						"error": {
 | 
				
			||||||
 | 
							"message": "Invalid JSON-RPC request: Expecting property name enclosed in double quotes: line 4 column 4 (char 44)",
 | 
				
			||||||
 | 
							"code": 909,
 | 
				
			||||||
 | 
							"data": {
 | 
				
			||||||
 | 
								"error": "Expecting property name enclosed in double quotes: line 4 column 4 (char 44)"
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"name": "JSONError"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"id": null,
 | 
				
			||||||
 | 
						"principal": "admin@EXAMPLE.COM"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								test/key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								test/key.pem
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK66WbEGKPFy/Eeg
 | 
				
			||||||
 | 
					rjds+O+wJxKNoi1ii4hht8zVYoibgYExspHAJg1JBq8KKvCBKIgQe0Xhe3PP/w5s
 | 
				
			||||||
 | 
					0I8Tjqjb/Sn0L3bkrNeZ2k2VU+JcY4RjhHRF4cXoWMS7yhr+KaBLE+x93iuDi1jV
 | 
				
			||||||
 | 
					gh15Pbz7ufNOqdb2zribnf2amD4nAgMBAAECgYAs4C+pB6v8V0v0GZClK5fD97oR
 | 
				
			||||||
 | 
					Sc8dWPH9VRufwC5OZ6IbTGhQhsk/IEJXMoVUv9dpGtKOYBsU45beXZQzKxK4XsVK
 | 
				
			||||||
 | 
					6DTZT4+jgp+IJuM0GEguYHClTYjDjRlDSZe4SAq7pWEr/pFiE1u0MXbkRenDkXox
 | 
				
			||||||
 | 
					X4LXw/SKF3P/x06OAQJBAMK59BjxvATHaAYhABt2rDi9JjSBcceM6oO2y4hAZJCy
 | 
				
			||||||
 | 
					1NGG098xO4Ne3xuYuldBsnoLKk+G3bF5Vqws8mC0UisCQQDltW+RRIVFeo9rxNHC
 | 
				
			||||||
 | 
					NWEJbNZD9FJWsgrBo6vGG7ET3erdXxU3iWYx2Z986BVPhSTcPdRJSwPf6hi/xlkr
 | 
				
			||||||
 | 
					ElH1AkBOP2j+KQ1TokmDxPkFEC/ucNuMV8O/2zlVijvJWY7PsnzgYVx8II14ocPn
 | 
				
			||||||
 | 
					k/y1GXo9noT3BgvJyCdy8nDHOU6XAkEAo9wfcALvBrb85CWMc/tb8zs+RU9eBRYQ
 | 
				
			||||||
 | 
					cj1s5W8PjFp7ldqj6fALhHf3O0TbHtSdjLZWXsoyQ2JcsUCujvkMmQJAU+ivqOZP
 | 
				
			||||||
 | 
					evb4YZ23LLharBxMCSW4AkmYav5zlp4FE74ieqUsHOxGWt2mcdgERjZ0b7TyG5C+
 | 
				
			||||||
 | 
					KRh6+NazFEwmww==
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										96
									
								
								test/user_add_response.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								test/user_add_response.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "result": {
 | 
				
			||||||
 | 
					      "result": {
 | 
				
			||||||
 | 
					        "has_keytab": true,
 | 
				
			||||||
 | 
					        "cn": [
 | 
				
			||||||
 | 
					          "FreeIPA Test"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "krbcanonicalname": [
 | 
				
			||||||
 | 
					          "username@EXAMPLE.COM"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "memberof_group": [
 | 
				
			||||||
 | 
					          "ipausers"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "has_password": true,
 | 
				
			||||||
 | 
					        "homedirectory": [
 | 
				
			||||||
 | 
					          "/home/example.com/username"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "uid": [
 | 
				
			||||||
 | 
					          "username"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "loginshell": [
 | 
				
			||||||
 | 
					          "/bin/bash"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "uidnumber": [
 | 
				
			||||||
 | 
					          "866001000"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "krbextradata": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "__base64__": "AAJ/ZHJvb3QvYWRtaW5ARVhBTVBMRS5DT00A"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "mail": [
 | 
				
			||||||
 | 
					          "username@example.com"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "dn": "uid=username,cn=users,cn=accounts,dc=example,dc=com",
 | 
				
			||||||
 | 
					        "displayname": [
 | 
				
			||||||
 | 
					          "FreeIPA Test"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "mepmanagedentry": [
 | 
				
			||||||
 | 
					          "cn=username,cn=groups,cn=accounts,dc=example,dc=com"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "ipauniqueid": [
 | 
				
			||||||
 | 
					          "e8b12c28-3744-11ee-ad07-141877671fe2"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "krbprincipalname": [
 | 
				
			||||||
 | 
					          "username@EXAMPLE.COM"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "givenname": [
 | 
				
			||||||
 | 
					          "FreeIPA"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "krbpasswordexpiration": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "__datetime__": "20230810061238Z"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "objectclass": [
 | 
				
			||||||
 | 
					          "top",
 | 
				
			||||||
 | 
					          "person",
 | 
				
			||||||
 | 
					          "organizationalperson",
 | 
				
			||||||
 | 
					          "inetorgperson",
 | 
				
			||||||
 | 
					          "inetuser",
 | 
				
			||||||
 | 
					          "posixaccount",
 | 
				
			||||||
 | 
					          "krbprincipalaux",
 | 
				
			||||||
 | 
					          "krbticketpolicyaux",
 | 
				
			||||||
 | 
					          "ipaobject",
 | 
				
			||||||
 | 
					          "ipasshuser",
 | 
				
			||||||
 | 
					          "nexcessuser",
 | 
				
			||||||
 | 
					          "ipaSshGroupOfPubKeys",
 | 
				
			||||||
 | 
					          "mepOriginEntry"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "gidnumber": [
 | 
				
			||||||
 | 
					          "866001000"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "gecos": [
 | 
				
			||||||
 | 
					          "FreeIPA Test"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "sn": [
 | 
				
			||||||
 | 
					          "Heath"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "krblastpwdchange": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "__datetime__": "20230810061238Z"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "initials": [
 | 
				
			||||||
 | 
					          "FH"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "value": "username",
 | 
				
			||||||
 | 
					      "summary": "Added user \"username\""
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "version": "4.6.8",
 | 
				
			||||||
 | 
					    "error": null,
 | 
				
			||||||
 | 
					    "id": null,
 | 
				
			||||||
 | 
					    "principal": "admin@EXAMPLE.COM"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
							
								
								
									
										30
									
								
								test/user_find_response.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								test/user_find_response.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "result": {
 | 
				
			||||||
 | 
					      "count": 2,
 | 
				
			||||||
 | 
					      "truncated": false,
 | 
				
			||||||
 | 
					      "result": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "dn": "uid=admin,cn=users,cn=accounts,dc=example,dc=com",
 | 
				
			||||||
 | 
					          "uid": [
 | 
				
			||||||
 | 
					            "admin"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "dn": "uid=johnny.bravo,cn=users,cn=accounts,dc=example,dc=com",
 | 
				
			||||||
 | 
					          "ipasshpubkey": [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "__base64__": "c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCZ1FDbTAwQ2sxc2l3eFY1N3lDb3R3V0VRd2lpbjhHK2tmeU9hWWkwUEtqQUgzNzN2VnJsY09qVEJCaEZTV2w4QUdlWmlqMXc3aEhka1VYTTZNbXdWVzVLQmJ4N3lGeFBmcU1lRjNsWVh1WlFpdW52VHlIZlRRK3lqVnVwOVNtVHJBT1NCWklsOHpUOFlXQmZPVVJwN2pPdDRPMUdPNW50VTRRTFVQTlUzdFdWdDFPUmZ6K3NEWVpmVGZ0bFpHbzc3RVJTdm9vd0t3ekFmTmd1SktMb2tJWm84bXJySm5SYlVJdDFIVGgxTU8wTk1yU2I1Rk1wd2JCMXdWNERINVdwMFJSR1BJV2VheE5SL2dtV2tNSURTYWpFZDZUTXpidThMZ2hqa0FDWWtYSThUZmNUMWl1Rlh4OGh0YWRuT2Jwc0pYTUNPYk5td0RRa2xwQ3NHSXVkZWpGQVQ0THhHcWZzZVZSbVprOG96WTlSKzhLRmtzMkEybG1HNFVmUERtRlVWSUVNYmV3SjhHN09rTFRPajVEQWV0b01wVEh1RE04SkdpNVpXaHV2bGh5L2w5NU5XUFBidE8rZ0tTMTVQTml1UGNORExMdGNSQ2NIbUg2TUZiVWUyTC9oSm5VS2NxUVNYMmhqcnJzdXhPVXkvNFZ4Q3BDQUJqQ244U2Z4c3J3Y1JLaU09IHJvb3RAZXhhbXBsZS5jb20="
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "uid": [
 | 
				
			||||||
 | 
					            "johnny.bravo"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "summary": "2 users matched"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "version": "4.6.8",
 | 
				
			||||||
 | 
					    "error": null,
 | 
				
			||||||
 | 
					    "id": null,
 | 
				
			||||||
 | 
					    "principal": "admin@EXAMPLE.COM"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user