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…
Reference in New Issue
Block a user