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