go-freeipa/client.go

188 lines
5.0 KiB
Go
Raw Normal View History

2023-08-10 17:22:52 -05:00
package freeipa
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
2024-03-08 14:23:23 -06:00
"strings"
2023-08-10 17:22:52 -05:00
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"
)
// The base object for connections to FreeIPA API.
2023-08-10 17:22:52 -05:00
type Client struct {
uriBase string
client *http.Client
user string
password string
krb5 *krb5client.Client
}
// Internal function with common init code for each connection type, mainly sets http.Client and uriBase.
2023-08-10 17:22:52 -05:00
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
}
// Make a new client and login using standard username/password.
2023-08-10 17:22:52 -05:00
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 using standard credentials.
2023-08-10 17:22:52 -05:00
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.
2024-03-08 14:23:23 -06:00
req, err := http.NewRequest("POST", c.uriBase+"/session/login_password", strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("error building login request: %s", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Referer", c.uriBase)
// Perform the login request.
res, err := c.client.Do(req)
if err != nil {
return err
2023-08-10 17:22:52 -05:00
}
// 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
}
// Options for connecting to Kerberos.
2023-08-10 17:22:52 -05:00
type KerberosConnectOptions struct {
Krb5ConfigReader io.Reader
KeytabReader io.Reader
User string
Realm string
}
// Create a new client using Kerberos authentication.
2023-08-10 17:22:52 -05:00
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
}
// Login using kerberos client. The regular login function will call this function if needed.
2023-08-10 17:22:52 -05:00
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
}