src/jetstream/plugins/kubernetes/auth/gke.go
package auth
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
"github.com/labstack/echo/v4"
log "github.com/sirupsen/logrus"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"github.com/SermoDigital/jose/jws"
)
const (
gkeConfigType = "authorized_user"
googleOAuthEndpoint = "https://www.googleapis.com/oauth2/v4/token"
)
// GKEConfig is the format of the config file we expect for GKE authentication
type GKEConfig struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RefreshToken string `json:"refresh_token"`
Type string `json:"type"`
Email string `json:"email"`
}
// GKEKubeAuth is GKE Authentication for Kubernetes
type GKEKubeAuth struct {
portalProxy interfaces.PortalProxy
}
const authConnectTypeGKE = "gke-auth"
// InitGKEKubeAuth creates a GKEKubeAuth
func InitGKEKubeAuth(portalProxy interfaces.PortalProxy) KubeAuthProvider {
return &GKEKubeAuth{portalProxy: portalProxy}
}
// GetName returns the provider name
func (c *GKEKubeAuth) GetName() string {
return authConnectTypeGKE
}
func (c *GKEKubeAuth) AddAuthInfo(info *clientcmdapi.AuthInfo, tokenRec interfaces.TokenRecord) error {
gkeInfo := &GKEConfig{}
err := json.Unmarshal([]byte(tokenRec.RefreshToken), &gkeInfo)
if err != nil {
return err
}
info.Token = tokenRec.AuthToken
return nil
}
// FetchToken will create a token for the GKE Authentication using the POSTed data
func (c *GKEKubeAuth) FetchToken(cnsiRecord interfaces.CNSIRecord, ec echo.Context) (*interfaces.TokenRecord, *interfaces.CNSIRecord, error) {
log.Debug("FetchToken (GKE)")
// We should already have the refresh token in the body sent to us
req := ec.Request()
// Need to extract the parameters from the request body
defer req.Body.Close()
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, nil, err
}
gkeInfo := &GKEConfig{}
err = json.Unmarshal(body, &gkeInfo)
if err != nil {
return nil, nil, err
}
// Type needs to be "authorized_user"
if gkeInfo.Type != gkeConfigType || len(gkeInfo.RefreshToken) == 0 {
return nil, nil, errors.New("Invalid configuration file")
}
oauthToken, err := c.refreshGKEToken(cnsiRecord.SkipSSLValidation, gkeInfo.ClientID, gkeInfo.ClientSecret, gkeInfo.RefreshToken)
if err != nil {
return nil, nil, fmt.Errorf("Could not refresh the GKE token: %v+", err)
}
token, err := jws.ParseJWT([]byte(oauthToken.IDToken))
if err != nil {
log.Info(err)
return nil, nil, errors.New("Can not parse JWT Access token")
}
email := token.Claims().Get("email")
if emailAddress, ok := email.(string); ok {
gkeInfo.Email = emailAddress
}
tokenInfo, err := json.Marshal(gkeInfo)
if err != nil {
return nil, nil, err
}
// Create a new token record - we need to store the client ID and secret as well, so cheekily use the refresh token for this
tokenRecord := c.portalProxy.InitEndpointTokenRecord(0, oauthToken.AccessToken, string(tokenInfo), false)
tokenRecord.AuthType = authConnectTypeGKE
return &tokenRecord, &cnsiRecord, nil
}
// GetUserFromToken gets the username from the GKE Token
func (c *GKEKubeAuth) GetUserFromToken(cnsiGUID string, tokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) {
log.Debug("GetUserFromToken (GKE)")
gkeInfo := &GKEConfig{}
err := json.Unmarshal([]byte(tokenRecord.RefreshToken), &gkeInfo)
if err != nil {
return nil, false
}
return &interfaces.ConnectedUser{
GUID: gkeInfo.Email,
Name: gkeInfo.Email,
}, true
}
func (c *GKEKubeAuth) DoFlowRequest(cnsiRequest *interfaces.CNSIRequest, req *http.Request) (*http.Response, error) {
log.Debug("doGKEFlowRequest")
authHandler := c.portalProxy.OAuthHandlerFunc(cnsiRequest, req, c.RefreshGKEToken)
return c.portalProxy.DoAuthFlowRequest(cnsiRequest, req, authHandler)
}
// RefreshGKEToken will refresh a GKE token
func (c *GKEKubeAuth) RefreshGKEToken(skipSSLValidation bool, cnsiGUID, userGUID, client, clientSecret, tokenEndpoint string) (t interfaces.TokenRecord, err error) {
log.Debug("RefreshGKEToken")
now := time.Now()
userToken, ok := c.portalProxy.GetCNSITokenRecordWithDisconnected(cnsiGUID, userGUID)
if !ok {
return t, fmt.Errorf("Info could not be found for user with GUID %s", userGUID)
}
// Refresh token is the GKE info
var gkeInfo GKEConfig
err = json.Unmarshal([]byte(userToken.RefreshToken), &gkeInfo)
if err != nil {
return userToken, fmt.Errorf("Could not get the GKE info from the refresh token: %v+", err)
}
oauthToken, err := c.refreshGKEToken(skipSSLValidation, gkeInfo.ClientID, gkeInfo.ClientSecret, gkeInfo.RefreshToken)
if err != nil {
return userToken, fmt.Errorf("Could not refresh the GKE token: %v+", err)
}
userToken.AuthToken = oauthToken.AccessToken
duration := time.Duration(oauthToken.ExpiresIn) * time.Second
expiry := now.Add(duration).Unix()
userToken.TokenExpiry = expiry
return userToken, nil
}
func (c *GKEKubeAuth) refreshGKEToken(skipSSLValidation bool, clientID, clientSecret, refreshToken string) (u interfaces.UAAResponse, err error) {
log.Debug("refreshGKEToken")
tokenInfo := interfaces.UAAResponse{}
// Go and get a new access token
httpClient := c.portalProxy.GetHttpClient(skipSSLValidation)
body := fmt.Sprintf("client_secret=%s&refresh_token=%s&client_id=%s&grant_type=refresh_token", url.QueryEscape(clientSecret), url.QueryEscape(refreshToken), url.QueryEscape(clientID))
resp, err := httpClient.Post(googleOAuthEndpoint, "application/x-www-form-urlencoded", strings.NewReader(body))
if err != nil {
return tokenInfo, err
}
// Check status code
if resp.StatusCode != 200 {
return tokenInfo, fmt.Errorf("Failed to get access token: %s", resp.Status)
}
// Parse the response
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tokenInfo, err
}
err = json.Unmarshal(respBody, &tokenInfo)
return tokenInfo, err
}
func (c *GKEKubeAuth) RegisterJetstreamAuthType(portal interfaces.PortalProxy) {
// Register auth type with Jetstream
c.portalProxy.AddAuthProvider(c.GetName(), interfaces.AuthProvider{
Handler: c.DoFlowRequest,
UserInfo: c.GetUserFromToken,
})
}