diff --git a/freeipa/client.go b/freeipa/client.go index 13028ca..fa4517d 100644 --- a/freeipa/client.go +++ b/freeipa/client.go @@ -31,17 +31,25 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "net/http/cookiejar" "net/url" + + k5client "github.com/jcmturner/gokrb5/v8/client" + k5config "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/keytab" + "github.com/jcmturner/gokrb5/v8/spnego" + "github.com/pkg/errors" ) // Client holds a connection to a FreeIPA server. type Client struct { - host string - hc *http.Client - user string - pw string + host string + hc *http.Client + user string + pw string + k5client *k5client.Client } // Error is an error returned by the FreeIPA server in a JSON response. @@ -78,6 +86,46 @@ func Connect(host string, tspt *http.Transport, user, pw string) (*Client, error return c, nil } +func ConnectWithKerberos(host string, tspt *http.Transport, k5ConnectOpts *KerberosConnectOptions) (*Client, error) { + jar, e := cookiejar.New(&cookiejar.Options{ + PublicSuffixList: nil, // this should be fine, since we only use one server + }) + if e != nil { + return nil, e + } + + krb5Config, err := k5config.NewFromReader(k5ConnectOpts.Krb5ConfigReader) + if err != nil { + return nil, errors.WithMessage(err, "reading kerberos configuration") + } + + ktBytes, err := ioutil.ReadAll(k5ConnectOpts.KeytabReader) + if err != nil { + return nil, errors.WithMessage(err, "reading keytab") + } + + kt := keytab.New() + if err := kt.Unmarshal(ktBytes); err != nil { + return nil, errors.WithMessage(err, "parsing keytab") + } + + k5client := k5client.NewWithKeytab(k5ConnectOpts.Username, k5ConnectOpts.Realm, kt, krb5Config) + + c := &Client{ + host: host, + hc: &http.Client{ + Transport: tspt, + Jar: jar, + }, + user: k5ConnectOpts.Username, + k5client: k5client, + } + if e := c.login(); e != nil { + return nil, fmt.Errorf("initial login falied: %v", e) + } + return c, nil +} + func (c *Client) exec(req *request) (io.ReadCloser, error) { res, e := c.sendRequest(req) if e != nil { @@ -103,6 +151,10 @@ func (c *Client) exec(req *request) (io.ReadCloser, error) { } func (c *Client) login() error { + if c.k5client != nil { + return c.loginWithKerberos() + } + data := url.Values{ "user": []string{c.user}, "password": []string{c.pw}, @@ -117,6 +169,30 @@ func (c *Client) login() error { return nil } +func (c *Client) loginWithKerberos() error { + + k5LoginEndpoint := fmt.Sprintf("https://%s/ipa/session/login_kerberos", c.host) + spnegoCl := spnego.NewClient(c.k5client, c.hc, "") + + req, err := http.NewRequest("POST", k5LoginEndpoint, nil) + if err != nil { + return errors.WithMessage(err, "building login HTTP request") + } + + req.Header.Add("Referer", fmt.Sprintf("https://%s/ipa", c.host)) + + res, err := spnegoCl.Do(req) + if err != nil { + return errors.Wrap(err, "logging in using Kerberos") + } + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected http status code: %v", res.StatusCode) + } + + return nil +} + func (c *Client) sendRequest(req *request) (*http.Response, error) { reqB, e := json.Marshal(req) if e != nil { diff --git a/freeipa/example_test.go b/freeipa/example_test.go index ac49e52..a2ebae6 100644 --- a/freeipa/example_test.go +++ b/freeipa/example_test.go @@ -6,6 +6,7 @@ import ( "log" "math/rand" "net/http" + "os" "time" "github.com/tehwalris/go-freeipa/freeipa" @@ -67,3 +68,51 @@ func Example_errorHandling() { // Output: FreeIPA error 4001: somemissinguid: user not found // (matched expected error code) } + +func Example_kerberosLogin() { + + krb5Principal := "host/cc.in2p3.fr" + krb5Realm := "CC.IN2P3.FR" + + krb5KtFd, err := os.Open("/etc/krb5.keytab") + if err != nil { + log.Fatal(err) + } + defer krb5KtFd.Close() + + krb5Fd, err := os.Open("/etc/krb5.conf") + if err != nil { + log.Fatal(err) + } + defer krb5Fd.Close() + + krb5ConnectOption := &freeipa.KerberosConnectOptions{ + Krb5ConfigReader: krb5Fd, + KeytabReader: krb5KtFd, + Username: krb5Principal, + Realm: krb5Realm, + } + + tspt := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + + c, err := freeipa.ConnectWithKerberos("dc1.test.local", tspt, krb5ConnectOption) + if err != nil { + log.Fatal(err) + } + + sizeLimit := 5 + res, err := c.UserFind("", &freeipa.UserFindArgs{}, &freeipa.UserFindOptionalArgs{ + Sizelimit: &sizeLimit, + }) + if err != nil { + log.Fatal(err) + } + + for _, user := range res.Result { + fmt.Printf("User[%s] HOME=%s\n", user.UID, *user.Homedirectory) + } +} diff --git a/freeipa/kerberos.go b/freeipa/kerberos.go new file mode 100644 index 0000000..4622850 --- /dev/null +++ b/freeipa/kerberos.go @@ -0,0 +1,10 @@ +package freeipa + +import "io" + +type KerberosConnectOptions struct { + Krb5ConfigReader io.Reader + KeytabReader io.Reader + Username string + Realm string +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0d2ae68 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/tehwalris/go-freeipa + +go 1.15 + +require ( + github.com/jcmturner/gokrb5/v8 v8.4.1 + github.com/pkg/errors v0.9.1 +)