Initial commit

This commit is contained in:
GRMrGecko 2023-08-10 17:22:52 -05:00
commit cb4d9a5c2e
14 changed files with 1343 additions and 0 deletions

19
License.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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-----

View 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"
}

View 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"
}