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 }