434 lines
11 KiB
Go
434 lines
11 KiB
Go
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2015 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const CA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
|
|
|
type AuthError struct {
|
|
msg string
|
|
}
|
|
|
|
func (v *AuthError) Error() string {
|
|
return v.msg
|
|
}
|
|
|
|
func newAuthError(msg string) error {
|
|
return &AuthError{msg}
|
|
}
|
|
|
|
// helpers
|
|
func namedArrayObject(key string, name string, m map[string]interface{}) []map[string]interface{} {
|
|
nm := make(map[string]interface{})
|
|
nm[key] = m
|
|
nm["name"] = name
|
|
s := make([]map[string]interface{}, 1)
|
|
s[0] = nm
|
|
return s
|
|
}
|
|
|
|
// Struct for decoding json
|
|
type userInfo struct {
|
|
FullName string `json:"fullName"`
|
|
MetaData struct {
|
|
Name string `json:"name"`
|
|
} `json:"metadata"`
|
|
}
|
|
|
|
type apiVersions struct {
|
|
Versions []string `json:"versions"`
|
|
}
|
|
|
|
// Simple client
|
|
type Client struct {
|
|
host string
|
|
version string
|
|
|
|
caData string
|
|
insecure bool
|
|
requireOpenshift bool
|
|
isOpenshift bool
|
|
|
|
userAPI string
|
|
client *http.Client
|
|
creds *Credentials
|
|
}
|
|
|
|
func doRequest(client *http.Client, method string, path string, auth string, body []byte) (*http.Response, error) {
|
|
var req *http.Request
|
|
var err error
|
|
if body != nil {
|
|
req, err = http.NewRequest(method, path, bytes.NewReader(body))
|
|
} else {
|
|
req, err = http.NewRequest(method, path, nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if auth != "" {
|
|
req.Header.Add("Authorization", auth)
|
|
}
|
|
return client.Do(req)
|
|
}
|
|
|
|
func (self *Client) fetchVersion(authHeader string) error {
|
|
path := fmt.Sprintf("%s/api", self.host)
|
|
resp, err := doRequest(self.client, "GET", path, authHeader, nil)
|
|
|
|
// Treat connection errors as internal errors and invalid
|
|
// responses as auth errors
|
|
if err != nil {
|
|
return errors.New(fmt.Sprintf("Couldn't connect to the api: %s", err))
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return newAuthError(fmt.Sprintf("Couldn't get api version: %s", resp.Status))
|
|
}
|
|
|
|
data := apiVersions{}
|
|
deErr := json.NewDecoder(resp.Body).Decode(&data)
|
|
if deErr != nil {
|
|
return newAuthError(fmt.Sprintf("Couldn't get api version: %s", deErr))
|
|
}
|
|
|
|
if len(data.Versions) < 1 {
|
|
return newAuthError(fmt.Sprintf("Couldn't get api version: invalid data"))
|
|
}
|
|
|
|
self.version = data.Versions[0]
|
|
return nil
|
|
}
|
|
|
|
func (self *Client) apiStatus(resource string, auth string) (int, error) {
|
|
path := fmt.Sprintf("%s/api/%s/%s", self.host, self.version, resource)
|
|
resp, rErr := doRequest(self.client, "GET", path, auth, nil)
|
|
if rErr != nil {
|
|
return 0, errors.New(fmt.Sprintf("Couldn't connect to api: %s", rErr))
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
func (self *Client) confirmBearerAuth(creds *Credentials) error {
|
|
// There are no bugs we have to work around here
|
|
// so just make sure we get a 200 or 403 to
|
|
// a namespace call
|
|
|
|
status, e := self.apiStatus("namespaces", creds.GetHeader())
|
|
if e == nil && status != 403 && status != 200 {
|
|
newAuthError(fmt.Sprintf("Couldn't verify bearer token with api: %s", status))
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (self *Client) confirmBasicAuth(creds *Credentials) error {
|
|
// Issue a request to the the /api API endpoint without any
|
|
// auth data.
|
|
// If we get 401 in response then we know our creds were good and we can log the user in.
|
|
// If we get a 200 or a 403 then we don't know if are creds were correct and we need to
|
|
// make more calls to figure it out.
|
|
// Any other code is treated as an error.
|
|
var e error
|
|
var status int
|
|
status, e = self.apiStatus("", "")
|
|
success := status == 401
|
|
|
|
if status == 200 || status == 403 {
|
|
// Either /api is open or the current user, possibly (system:anonymous)
|
|
// doesn't have permissions on it
|
|
// Send a request to the /api/$version/namespaces endpoint with a
|
|
// Authorization header that is guarenteed to be invalid.
|
|
// This should return a 200 if the whole api is open or a 401 if the
|
|
// api is protected.
|
|
status, e = self.apiStatus("namespaces", "Basic Og==")
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
// Some versions of kubernetes return 403 instead of 401
|
|
// when presented with bad basic auth data. In those cases
|
|
// we need to refuse authentication, as we have no way
|
|
// know if the credentials we have are in fact valid.
|
|
// https://github.com/kubernetes/kubernetes/pull/41775
|
|
if status == 403 {
|
|
e = errors.New("This version of kubernetes is not supported. Turn off anonymous auth or upgrade.")
|
|
} else if status == 200 {
|
|
success = true
|
|
} else if status == 401 {
|
|
if creds.GetHeader() != "" {
|
|
success = true
|
|
} else {
|
|
status = 403
|
|
}
|
|
}
|
|
}
|
|
|
|
if !success && e == nil {
|
|
e = newAuthError(fmt.Sprintf("Couldn't verify authentication with api: %s", status))
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (self *Client) confirmCreds(creds *Credentials) error {
|
|
// Do this explictly so that we know we have a valid response
|
|
// Kubernetes doesn't provide any way for a caller
|
|
// to find out who it is, so we need to confirm the creds
|
|
// we got some other way.
|
|
err := self.fetchVersion(creds.GetHeader())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we are here we got a version for the api from the /api endpoint,
|
|
// using the credentials the user gave us.
|
|
// This happens when either
|
|
// a) /api is protected and the credentials are correct
|
|
// or
|
|
// b) /api is open in which case we have no idea if our creds were correct
|
|
// Confirming them is different for Basic or Bearer auth
|
|
var e error
|
|
if creds.UserName != "" {
|
|
e = self.confirmBasicAuth(creds)
|
|
} else {
|
|
creds.UserName = "Unknown"
|
|
e = self.confirmBearerAuth(creds)
|
|
}
|
|
|
|
creds.DisplayName = creds.UserName
|
|
return e
|
|
}
|
|
|
|
func (self *Client) fetchUserData(creds *Credentials) error {
|
|
resp, err := self.DoRequest("GET", self.userAPI, "users/~", creds, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
// 404 or 403 are both responses we can
|
|
// get when the oapi/users endpoint doesn't exists
|
|
notFound := resp.StatusCode == 404 || resp.StatusCode == 403
|
|
if notFound && self.requireOpenshift {
|
|
return errors.New("Couldn't connect: Incompatible API")
|
|
|
|
// This might be kubernetes, it doesn't have a way to
|
|
// get user data, if we have a username try to
|
|
// see if we can connect to it anyways
|
|
} else if notFound {
|
|
return self.confirmCreds(creds)
|
|
} else if resp.StatusCode != 200 {
|
|
return newAuthError(fmt.Sprintf("Couldn't get user data: %s", resp.Status))
|
|
}
|
|
|
|
self.isOpenshift = true
|
|
data := userInfo{}
|
|
deErr := json.NewDecoder(resp.Body).Decode(&data)
|
|
if deErr != nil {
|
|
return newAuthError(fmt.Sprintf("Couldn't get user json data: %s", deErr))
|
|
}
|
|
|
|
creds.UserName = data.MetaData.Name
|
|
if creds.UserName != "" {
|
|
creds.DisplayName = data.FullName
|
|
if creds.DisplayName == "" {
|
|
creds.DisplayName = creds.UserName
|
|
}
|
|
} else {
|
|
return newAuthError(fmt.Sprintf("Openshift user data wasn't valid: %v", data))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *Client) DoRequest(method string, api string, resource string,
|
|
creds *Credentials, body []byte) (*http.Response, error) {
|
|
if self.host == "" {
|
|
return nil, errors.New("No kubernetes available")
|
|
}
|
|
|
|
authHeader := ""
|
|
if creds != nil {
|
|
authHeader = creds.GetHeader()
|
|
}
|
|
|
|
if self.version == "" {
|
|
err := self.fetchVersion(authHeader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
path := fmt.Sprintf("%s/%s/%s/%s", self.host, api, self.version, resource)
|
|
resp, rErr := doRequest(self.client, method, path, authHeader, body)
|
|
if rErr != nil {
|
|
return nil, errors.New(fmt.Sprintf("Couldn't connect: %s", rErr))
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (self *Client) Login(authLine string) (map[string]interface{}, error) {
|
|
parts := strings.SplitN(authLine, " ", 2)
|
|
if len(parts) == 0 {
|
|
return nil, newAuthError("Invalid Authorization line")
|
|
}
|
|
authData := ""
|
|
authType := parts[0]
|
|
if len(parts) == 2 {
|
|
authData = parts[1]
|
|
}
|
|
|
|
creds, err := NewCredentials(authType, authData)
|
|
if err == nil {
|
|
err = self.fetchUserData(creds)
|
|
}
|
|
|
|
if err != nil {
|
|
if creds != nil && creds.authType == "negotiate" {
|
|
return nil, newAuthError(fmt.Sprintf("Negotiate failed: %s", err))
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Login successfull, save creds
|
|
self.creds = creds
|
|
user_data := creds.GetApiUserMap()
|
|
cluster := make(map[string]interface{})
|
|
cluster["server"] = self.host
|
|
if self.caData != "" {
|
|
cluster["certificate-authority-data"] = self.caData
|
|
}
|
|
|
|
cluster["insecure-skip-tls-verify"] = self.insecure
|
|
clusters := namedArrayObject("cluster", "container-cluster", cluster)
|
|
|
|
context := make(map[string]interface{})
|
|
context["cluster"] = "container-cluster"
|
|
context["user"] = user_data["name"]
|
|
contexts := namedArrayObject("context", "container-context", context)
|
|
|
|
users := make([]map[string]interface{}, 1)
|
|
users[0] = user_data
|
|
|
|
login_data := make(map[string]interface{})
|
|
login_data["apiVersion"] = self.version
|
|
login_data["displayName"] = creds.DisplayName
|
|
login_data["current-context"] = "container-context"
|
|
login_data["clusters"] = clusters
|
|
login_data["contexts"] = contexts
|
|
login_data["users"] = users
|
|
|
|
return login_data, nil
|
|
}
|
|
|
|
func (self *Client) CleanUp() error {
|
|
if self.creds != nil && self.isOpenshift {
|
|
token := self.creds.GetToken()
|
|
if token != "" {
|
|
path := fmt.Sprintf("oauthaccesstokens/%s", token)
|
|
resp, err := self.DoRequest("DELETE", self.userAPI, path, self.creds, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
log.Println(fmt.Sprintf("Invalid token cleanup response: %d", resp.StatusCode))
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewClient() *Client {
|
|
var caData []byte = nil
|
|
var pool *x509.CertPool
|
|
|
|
ac := new(Client)
|
|
ac.insecure = false
|
|
ac.host = os.Getenv("KUBERNETES_SERVICE_HOST")
|
|
if ac.host != "" {
|
|
// assume we are always on https
|
|
ac.host = fmt.Sprintf("https://%s", ac.host)
|
|
port := os.Getenv("KUBERNETES_SERVICE_PORT")
|
|
if port != "" {
|
|
ac.host = fmt.Sprintf("%s:%s", ac.host, port)
|
|
}
|
|
}
|
|
|
|
ac.userAPI = os.Getenv("KUBERNETES_USER_API")
|
|
if ac.userAPI == "" {
|
|
ac.userAPI = "oapi"
|
|
}
|
|
|
|
ac.requireOpenshift, _ = strconv.ParseBool(os.Getenv("REGISTRY_ONLY"))
|
|
|
|
ac.insecure, _ = strconv.ParseBool(os.Getenv("KUBERNETES_INSECURE"))
|
|
if !ac.insecure {
|
|
data := os.Getenv("KUBERNETES_CA_DATA")
|
|
if data != "" {
|
|
caData = []byte(data)
|
|
}
|
|
|
|
if caData == nil {
|
|
var err error
|
|
caData, err = ioutil.ReadFile(CA_PATH)
|
|
if err != nil {
|
|
log.Println(fmt.Sprintf("Couldn't load CA data: %s", err))
|
|
}
|
|
}
|
|
|
|
if caData != nil {
|
|
pool = x509.NewCertPool()
|
|
pool.AppendCertsFromPEM(caData)
|
|
ac.caData = base64.StdEncoding.EncodeToString(caData)
|
|
}
|
|
}
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{RootCAs: pool, InsecureSkipVerify: ac.insecure},
|
|
}
|
|
ac.client = &http.Client{Transport: tr}
|
|
|
|
return ac
|
|
}
|