Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ipa

import "context"

// https://freeipa.readthedocs.io/en/latest/api/group_add_member.html
// https://github.com/freeipa/freeipa/blob/c40ce0e1ff01cbecf2d83377f48c0ace55fd1ed9/ipaserver/plugins/group.py#L633
func (c *Client) GroupAddMember(ctx context.Context, gid string, memberId string, memberType string) (*Response, error) {
options := map[string]any{}

switch memberType {
case "user":
options["user"] = memberId
case "group":
options["group"] = memberId
case "service":
options["service"] = memberId
}

res, err := c.rpcContext(ctx, "group_add_member", []string{gid}, options)
if err != nil {
return nil, err
}

return res, nil
}
238 changes: 157 additions & 81 deletions ipa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ package ipa

import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -97,7 +99,7 @@ type Response struct {
func init() {
// If ca.crt for ipa exists, use it as the cert pool
// otherwise default to system root ca.
pem, err := ioutil.ReadFile("/etc/ipa/ca.crt")
pem, err := os.ReadFile("/etc/ipa/ca.crt")
if err == nil {
ipaCertPool = x509.NewCertPool()
if !ipaCertPool.AppendCertsFromPEM(pem) {
Expand Down Expand Up @@ -180,85 +182,6 @@ func (e *IpaError) Error() string {
return fmt.Sprintf("ipa: error %d - %s", e.Code, e.Message)
}

// Call FreeIPA API with method, params and options
func (c *Client) rpc(method string, params []string, options Options) (*Response, error) {
if options == nil {
options = Options{}
}
options["version"] = IpaClientVersion

data := []interface{}{
params,
options,
}

payload := Options{
"id": 0,
"method": method,
"params": data,
}

b, err := json.Marshal(payload)
if err != nil {
return nil, err
}

ipaUrl := fmt.Sprintf("https://%s/ipa/json", c.host)
if len(c.sessionID) > 0 {
ipaUrl = fmt.Sprintf("https://%s/ipa/session/json", c.host)
}

req, err := http.NewRequest("POST", ipaUrl, bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", fmt.Sprintf("https://%s/ipa/xml", c.host))

if len(c.sessionID) > 0 {
// If session is set, use the session id
req.Header.Set("Cookie", fmt.Sprintf("ipa_session=%s", c.sessionID))
} else if c.krbClient != nil {
// use Kerberos auth (SPNEGO)
spnego.SetSPNEGOHeader(c.krbClient, req, "")
}

if log.IsLevelEnabled(log.TraceLevel) {
dump, _ := httputil.DumpRequestOut(req, true)
log.Tracef("FreeIPA RPC request: %s", dump)
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

if res.StatusCode != 200 {
return nil, fmt.Errorf("IPA RPC called failed with HTTP status code: %d", res.StatusCode)
}

if err = c.setSessionID(res); err != nil {
return nil, err
}

rawJson, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}

log.Tracef("FreeIPA JSON response: %s", string(rawJson))

var ipaRes Response
err = json.Unmarshal(rawJson, &ipaRes)
if err != nil {
return nil, err
}

if ipaRes.Error != nil {
return nil, ipaRes.Error
}

return &ipaRes, nil
}

// Returns FreeIPA server hostname
func (c *Client) Host() string {
return c.host
Expand Down Expand Up @@ -435,6 +358,159 @@ func (c *Client) LoginFromCCache(cpath string) error {
return nil
}

// Call FreeIPA API with method, params and options
func (c *Client) rpc(method string, params []string, options Options) (*Response, error) {
if options == nil {
options = Options{}
}
options["version"] = IpaClientVersion

data := []interface{}{
params,
options,
}

payload := Options{
"id": 0,
"method": method,
"params": data,
}

b, err := json.Marshal(payload)
if err != nil {
return nil, err
}

ipaUrl := fmt.Sprintf("https://%s/ipa/json", c.host)
if len(c.sessionID) > 0 {
ipaUrl = fmt.Sprintf("https://%s/ipa/session/json", c.host)
}

req, err := http.NewRequest("POST", ipaUrl, bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", fmt.Sprintf("https://%s/ipa/xml", c.host))

if len(c.sessionID) > 0 {
// If session is set, use the session id
req.Header.Set("Cookie", fmt.Sprintf("ipa_session=%s", c.sessionID))
} else if c.krbClient != nil {
// use Kerberos auth (SPNEGO)
spnego.SetSPNEGOHeader(c.krbClient, req, "")
}

if log.IsLevelEnabled(log.TraceLevel) {
dump, _ := httputil.DumpRequestOut(req, true)
log.Tracef("FreeIPA RPC request: %s", dump)
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

if res.StatusCode != 200 {
return nil, fmt.Errorf("IPA RPC called failed with HTTP status code: %d", res.StatusCode)
}

if err = c.setSessionID(res); err != nil {
return nil, err
}

rawJson, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

log.Tracef("FreeIPA JSON response: %s", string(rawJson))

var ipaRes Response
err = json.Unmarshal(rawJson, &ipaRes)
if err != nil {
return nil, err
}

if ipaRes.Error != nil {
return nil, ipaRes.Error
}

return &ipaRes, nil
}

func (c *Client) rpcContext(ctx context.Context, method string, arguments []string, options map[string]any) (*Response, error) {
options["version"] = IpaClientVersion

payload := map[string]any{
"id": 0,
"method": method,
"params": []any{
arguments,
options,
},
}

b, err := json.Marshal(payload)
if err != nil {
return nil, err
}

ipaUrl := fmt.Sprintf("https://%s/ipa/json", c.host)
if len(c.sessionID) > 0 {
ipaUrl = fmt.Sprintf("https://%s/ipa/session/json", c.host)
}

req, err := http.NewRequestWithContext(ctx, "POST", ipaUrl, bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", fmt.Sprintf("https://%s/ipa/xml", c.host))

if len(c.sessionID) > 0 {
// If session is set, use the session id
req.Header.Set("Cookie", fmt.Sprintf("ipa_session=%s", c.sessionID))
} else if c.krbClient != nil {
// use Kerberos auth (SPNEGO)
spnego.SetSPNEGOHeader(c.krbClient, req, "")
}

if log.IsLevelEnabled(log.TraceLevel) {
dump, _ := httputil.DumpRequestOut(req, true)
log.Tracef("FreeIPA RPC request: %s", dump)
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()

if res.StatusCode != 200 {
return nil, fmt.Errorf("IPA RPC called failed with HTTP status code: %d", res.StatusCode)
}

if err = c.setSessionID(res); err != nil {
return nil, err
}

rawJson, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

log.Tracef("FreeIPA JSON response: %s", string(rawJson))

ipaRes := &Response{}
err = json.Unmarshal(rawJson, &ipaRes)
if err != nil {
return nil, err
}

if ipaRes.Error != nil {
return nil, ipaRes.Error
}

return ipaRes, nil
}

// Parse a FreeIPA datetime. Datetimes in FreeIPA are returned using a
// class-hint system. Values are stored as an array with a single element
// indicating the type and value, for example, '[{"__datetime__": "YYYY-MM-DDTHH:MM:SSZ"]}'
Expand Down