github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/keycloak_client.go (about)

     1  package argocd
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	json "encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  type requester interface {
    18  	Do(req *http.Request) (*http.Response, error)
    19  }
    20  
    21  type httpclient struct {
    22  	requester requester
    23  	URL       string
    24  	token     string
    25  }
    26  
    27  // Creates a new realm for Keycloak.
    28  func createRealm(cfg *keycloakConfig) (string, error) {
    29  
    30  	req, err := defaultRequester(cfg.KeycloakServerCert, cfg.VerifyTLS)
    31  	if err != nil {
    32  		return "", err
    33  	}
    34  
    35  	// create a new http client.
    36  	h := &httpclient{
    37  		requester: req,
    38  	}
    39  
    40  	kSvcName := h.getKeycloakURL(cfg.ArgoNamespace)
    41  	if kSvcName != "" {
    42  		cfg.KeycloakURL = kSvcName
    43  	}
    44  
    45  	h.URL = cfg.KeycloakURL
    46  
    47  	// login request updates the auth token for httpclient.
    48  	err = h.login(cfg.Username, cfg.Password)
    49  	if err != nil {
    50  		return "", err
    51  	}
    52  	log.Info(fmt.Sprintf("Access Token for keycloak of ArgoCD %s in namespace %s generated successfully",
    53  		cfg.ArgoName, cfg.ArgoNamespace))
    54  
    55  	realmConfig, err := createRealmConfig(cfg)
    56  	if err != nil {
    57  		return "", err
    58  	}
    59  
    60  	status, _ := h.post(realmConfig)
    61  
    62  	return status, nil
    63  }
    64  
    65  // login requests a new auth token.
    66  func (h *httpclient) login(user, pass string) error {
    67  	form := url.Values{}
    68  	form.Add("username", user)
    69  	form.Add("password", pass)
    70  	form.Add("client_id", "admin-cli")
    71  	form.Add("grant_type", "password")
    72  
    73  	req, err := http.NewRequest(
    74  		"POST",
    75  		fmt.Sprintf("%s%s", h.URL, authURL),
    76  		strings.NewReader(form.Encode()),
    77  	)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    83  	res, err := h.requester.Do(req)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	defer res.Body.Close()
    89  	body, err := io.ReadAll(res.Body)
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	tokenRes := &TokenResponse{}
    95  	err = json.Unmarshal(body, tokenRes)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	if tokenRes.Error != "" {
   101  		return err
   102  	}
   103  
   104  	h.token = tokenRes.AccessToken
   105  
   106  	return nil
   107  }
   108  
   109  // Post the updated realm configuration to keycloak realm API.
   110  func (h *httpclient) post(realmConfig []byte) (string, error) {
   111  	request, err := http.NewRequest("POST",
   112  		fmt.Sprintf("%s%s", h.URL, realmURL),
   113  		bytes.NewBuffer(realmConfig))
   114  
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  
   119  	// set headers.
   120  	request.Header.Set("Content-Type", "application/json")
   121  	request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", h.token))
   122  
   123  	response, err := h.requester.Do(request)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  
   128  	return response.Status, nil
   129  }
   130  
   131  // defaultRequester returns a default client for requesting http endpoints.
   132  func defaultRequester(serverCert []byte, verifyTLS bool) (requester, error) {
   133  	tlsConfig, err := createTLSConfig(serverCert, verifyTLS)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	transport := http.DefaultTransport.(*http.Transport).Clone()
   138  	transport.TLSClientConfig = tlsConfig
   139  
   140  	c := &http.Client{Transport: transport}
   141  	return c, nil
   142  }
   143  
   144  // createTLSConfig constructs and returns a TLS Config with a root CA read
   145  // from the serverCert param if present, or a permissive config which
   146  // is insecure otherwise.
   147  // An Insecure config is returned also when .spec.SSO.verifyTLS is set to false.
   148  func createTLSConfig(serverCert []byte, verifyTLS bool) (*tls.Config, error) {
   149  	if serverCert == nil || !verifyTLS {
   150  		return &tls.Config{InsecureSkipVerify: true}, nil
   151  	}
   152  
   153  	rootCAPool := x509.NewCertPool()
   154  	if ok := rootCAPool.AppendCertsFromPEM(serverCert); !ok {
   155  		return nil, errors.Errorf("unable to successfully load certificate")
   156  	}
   157  	return &tls.Config{RootCAs: rootCAPool}, nil
   158  }
   159  
   160  // Get Keycloak URL.
   161  func (h *httpclient) getKeycloakURL(ns string) string {
   162  
   163  	svc := fmt.Sprintf("https://%s.%s.svc.cluster.local:%s", defaultKeycloakIdentifier, ns, "8443")
   164  	// At normal conditions, Keycloak should be accessible via the service name. However, there are some corner cases (like
   165  	// operator running locally during development or services being inaccessible due to network policies) which requires
   166  	// use of externalURL.
   167  	err := h.validateKeycloakURL(svc)
   168  	if err != nil {
   169  		return ""
   170  	}
   171  
   172  	return svc
   173  }
   174  
   175  func (h *httpclient) validateKeycloakURL(URL string) error {
   176  	req, err := http.NewRequest(
   177  		"GET",
   178  		URL,
   179  		nil,
   180  	)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	res, err := h.requester.Do(req)
   186  	if err != nil {
   187  		log.Info("Cannot access keycloak with Internal service name, trying keycloak Route URL")
   188  		return err
   189  	}
   190  	_ = res.Body.Close()
   191  	return nil
   192  }