cloudfoundry-incubator/stratos

View on GitHub
src/jetstream/authcnsi.go

Summary

Maintainability
A
35 mins
Test Coverage
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "net/url"
    "strings"

    "github.com/labstack/echo/v4"
    log "github.com/sirupsen/logrus"

    "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
    "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config"
    "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens"
)

// CFAdminIdentifier - The scope that Cloud Foundry uses to convey administrative level perms
const CFAdminIdentifier = "cloud_controller.admin"

// Start SSO flow for an Endpoint
func (p *portalProxy) ssoLoginToCNSI(c echo.Context) error {
    log.Debug("ssoLoginToCNSI")
    endpointGUID := c.QueryParam("guid")
    if len(endpointGUID) == 0 {
        return interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Missing target endpoint",
            "Need Endpoint GUID passed as form param")
    }

    _, err := p.GetSessionStringValue(c, "user_id")
    if err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value")
    }

    state := c.QueryParam("state")
    if len(state) == 0 {
        err := interfaces.NewHTTPShadowError(
            http.StatusUnauthorized,
            "SSO Login: State parameter missing",
            "SSO Login: State parameter missing")
        return err
    }

    cnsiRecord, err := p.GetCNSIRecord(endpointGUID)
    if err != nil {
        return interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Requested endpoint not registered",
            "No Endpoint registered with GUID %s: %s", endpointGUID, err)
    }

    // Check if this is first time in the flow, or via the callback
    code := c.QueryParam("code")

    if len(code) == 0 {
        // First time around
        // Use the standard SSO Login Callback endpoint, so this can be allow-listed for Stratos and Endpoint login
        returnURL := getSSORedirectURI(state, state, endpointGUID)
        redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s",
            cnsiRecord.AuthorizationEndpoint, cnsiRecord.ClientId, url.QueryEscape(returnURL))
        c.Redirect(http.StatusTemporaryRedirect, redirectURL)
        return nil
    }

    // Callback
    _, err = p.DoLoginToCNSI(c, endpointGUID, false)
    status := "ok"
    if err != nil {
        status = "fail"
    }

    // Take the user back to Stratos on the endpoints page
    redirect := fmt.Sprintf("/endpoints?cnsi_guid=%s&status=%s", endpointGUID, status)
    c.Redirect(http.StatusTemporaryRedirect, redirect)
    return nil
}

// Connect to the given Endpoint
// Note, an admin user can connect an endpoint as a system endpoint to share it with others

// loginToCNSI godoc
// @Summary Connect to the given endpoint
// @Description An admin user can connect an endpoint as a system endpoint to share it with others.
// @Accept    x-www-form-urlencoded
// @Produce    json
// @Param cnsi_guid formData string true "Endpoint GUID"
// @Param system_shared formData string false "Register as a system endpoint" Enums(true, false)
// @Param connect_type formData string false "Connection type" Enums(creds, none)
// @Param username formData string false "Username"
// @Param password formData string false "Password"
// @Success 201 {object} interfaces.LoginRes "Connected endpoint object"
// @Failure 400 {object} interfaces.ErrorResponseBody "Error response"
// @Failure 401 {object} interfaces.ErrorResponseBody "Error response"
// @Security ApiKeyAuth
// @Router /tokens [post]
func (p *portalProxy) loginToCNSI(c echo.Context) error {
    log.Debug("loginToCNSI")

    var systemSharedToken = false

    params := new(interfaces.LoginToCNSIParams)
    err := interfaces.BindOnce(params, c)
    if err != nil {
        return err
    }

    if len(params.CNSIGUID) == 0 {
        return interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Missing target endpoint",
            "Need Endpoint GUID passed as form param")
    }

    systemSharedValue := params.SystemShared
    if len(systemSharedValue) > 0 {
        systemSharedToken = systemSharedValue == "true"
    }

    resp, err := p.DoLoginToCNSI(c, params.CNSIGUID, systemSharedToken)
    if err != nil {
        return err
    }

    jsonString, err := json.Marshal(resp)
    if err != nil {
        return err
    }

    c.Response().Header().Set("Content-Type", "application/json")
    c.Response().Write(jsonString)
    return nil
}

func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*interfaces.LoginRes, error) {

    cnsiRecord, err := p.GetCNSIRecord(cnsiGUID)
    if err != nil {
        return nil, interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Requested endpoint not registered",
            "No Endpoint registered with GUID %s: %s", cnsiGUID, err)
    }

    // Get the User ID since we save the CNSI token against the Console user guid, not the CNSI user guid so that we can look it up easily
    userID, err := p.GetSessionStringValue(c, "user_id")
    if err != nil {
        return nil, echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value")
    }

    // admins are note allowed to connect to user created endpoints
    if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled {

        if len(cnsiRecord.Creator) != 0 && cnsiRecord.Creator != userID {
            return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users")
        }

        // search for system or personal endpoints and check if they are connected
        // automatically disconnect other endpoint if already connected to same url
        cnsiList, err := p.listCNSIByAPIEndpoint(cnsiRecord.APIEndpoint.String())
        if err != nil {
            return nil, echo.NewHTTPError(
                http.StatusBadRequest,
                "Failed to retrieve list of CNSIs",
                "Failed to retrieve list of CNSIs: %v", err,
            )
        }

        for _, cnsi := range cnsiList {
            if (cnsi.Creator == userID || len(cnsi.Creator) == 0) && cnsi.GUID != cnsiGUID {
                _, ok := p.GetCNSITokenRecord(cnsi.GUID, userID)
                if ok {
                    p.ClearCNSIToken(*cnsi, userID)
                }
            }
        }
    }

    // Register as a system endpoint?
    if systemSharedToken {
        user, err := p.StratosAuthService.GetUser(userID)
        if err != nil {
            return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user")
        }

        // User needs to be an admin
        if !user.Admin {
            return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator")
        }

        // We are all good to go - change the userID, so we record this token against the system-shared user and not this specific user
        // This is how we identify system-shared endpoint tokens
        userID = tokens.SystemSharedUserGuid
    }

    // Ask the endpoint type to connect
    for _, plugin := range p.Plugins {
        endpointPlugin, err := plugin.GetEndpointPlugin()
        if err != nil {
            // Plugin doesn't implement an Endpoint Plugin interface, skip
            continue
        }

        endpointType := endpointPlugin.GetType()
        if cnsiRecord.CNSIType == endpointType {
            tokenRecord, isAdmin, err := endpointPlugin.Connect(c, cnsiRecord, userID)
            if err != nil {
                if shadowError, ok := err.(interfaces.ErrHTTPShadow); ok {
                    return nil, shadowError
                }
                return nil, interfaces.NewHTTPShadowError(
                    http.StatusBadRequest,
                    "Could not connect to the endpoint",
                    "Could not connect to the endpoint: %s", err)
            }

            err = p.setCNSITokenRecord(cnsiGUID, userID, *tokenRecord)
            if err != nil {
                return nil, interfaces.NewHTTPShadowError(
                    http.StatusBadRequest,
                    "Failed to save Token for endpoint",
                    "Error occurred: %s", err)
            }

            // Validate the connection - some endpoints may want to validate that the connected endpoint
            err = endpointPlugin.Validate(userID, cnsiRecord, *tokenRecord)
            if err != nil {
                // Clear the token
                p.ClearCNSIToken(cnsiRecord, userID)
                return nil, interfaces.NewHTTPShadowError(
                    http.StatusBadRequest,
                    "Could not connect to the endpoint",
                    "Could not connect to the endpoint: %s", err)
            }

            resp := &interfaces.LoginRes{
                Account:     userID,
                TokenExpiry: tokenRecord.TokenExpiry,
                APIEndpoint: cnsiRecord.APIEndpoint,
                Admin:       isAdmin,
            }

            cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, tokenRecord)
            if ok {
                // If this is a system shared endpoint, then remove some metadata that should be send back to other users
                santizeInfoForSystemSharedTokenUser(cnsiUser, systemSharedToken)
                resp.User = cnsiUser
            } else {
                // Need to record a user
                resp.User = &interfaces.ConnectedUser{
                    GUID:   "Unknown",
                    Name:   "Unknown",
                    Scopes: []string{"read"},
                    Admin:  true,
                }
            }

            return resp, nil
        }
    }

    return nil, interfaces.NewHTTPShadowError(
        http.StatusBadRequest,
        "Endpoint connection not supported",
        "Endpoint connection not supported")
}

func (p *portalProxy) DoLoginToCNSIwithConsoleUAAtoken(c echo.Context, theCNSIrecord interfaces.CNSIRecord) error {
    userID, err := p.GetSessionStringValue(c, "user_id")
    if err != nil {
        return errors.New("could not find correct session value")
    }
    uaaToken, err := p.GetUAATokenRecord(userID)
    if err == nil { // Found the user's UAA token
        u, err := p.GetUserTokenInfo(uaaToken.AuthToken)
        if err != nil {
            return errors.New("could not parse current user UAA token")
        }
        cfEndpointSpec, _ := p.GetEndpointTypeSpec("cf")
        cnsiInfo, _, err := cfEndpointSpec.Info(theCNSIrecord.APIEndpoint.String(), true)
        if err != nil {
            log.Fatal("Could not get the info for Cloud Foundry", err)
            return err
        }

        uaaURL, err := url.Parse(cnsiInfo.TokenEndpoint)
        if err != nil {
            return fmt.Errorf("invalid authorization endpoint URL %s %s", cnsiInfo.TokenEndpoint, err)
        }

        if uaaURL.String() == p.GetConfig().ConsoleConfig.UAAEndpoint.String() { // CNSI UAA server matches Console UAA server
            uaaToken.LinkedGUID = uaaToken.TokenGUID
            err = p.setCNSITokenRecord(theCNSIrecord.GUID, u.UserGUID, uaaToken)

            // Update the endpoint to indicate that SSO Login is okay
            repo, dbErr := p.GetStoreFactory().EndpointStore()
            if dbErr == nil {
                theCNSIrecord.SSOAllowed = true
                repo.Update(theCNSIrecord, p.Config.EncryptionKeyInBytes)
            }
            // Return error from the login
            return err
        }
        return fmt.Errorf("the auto-registered endpoint UAA server does not match console UAA server")
    }
    log.Warn("Could not find current user UAA token")
    return err
}

func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) {
    if isSysystemShared {
        cnsiUser.GUID = tokens.SystemSharedUserGuid // Used by front end also
        cnsiUser.Scopes = make([]string, 0)
        cnsiUser.Name = "system_shared"
    }
}

func (p *portalProxy) ConnectOAuth2(c echo.Context, cnsiRecord interfaces.CNSIRecord) (*interfaces.TokenRecord, error) {
    uaaRes, u, _, err := p.FetchOAuth2Token(cnsiRecord, c)
    if err != nil {
        return nil, err
    }
    tokenRecord := p.InitEndpointTokenRecord(u.TokenExpiry, uaaRes.AccessToken, uaaRes.RefreshToken, false)
    return &tokenRecord, nil
}

func (p *portalProxy) FetchOAuth2Token(cnsiRecord interfaces.CNSIRecord, c echo.Context) (*interfaces.UAAResponse, *interfaces.JWTUserTokenInfo, *interfaces.CNSIRecord, error) {
    endpoint := cnsiRecord.AuthorizationEndpoint

    tokenEndpoint := fmt.Sprintf("%s/oauth/token", endpoint)

    uaaRes, u, err := p.login(c, cnsiRecord.SkipSSLValidation, cnsiRecord.ClientId, cnsiRecord.ClientSecret, tokenEndpoint)

    if err != nil {
        if httpError, ok := err.(interfaces.ErrHTTPRequest); ok {
            // Try and parse the Response into UAA error structure (p.login only handles UAA requests)
            errMessage := ""
            authError := &interfaces.UAAErrorResponse{}
            if err := json.Unmarshal([]byte(httpError.Response), authError); err == nil {
                errMessage = fmt.Sprintf(": %s", authError.ErrorDescription)
            }
            return nil, nil, nil, interfaces.NewHTTPShadowError(
                httpError.Status,
                fmt.Sprintf("Could not connect to the endpoint%s", errMessage),
                "Could not connect to the endpoint: %s", err)
        }

        return nil, nil, nil, interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Login failed",
            "Login failed: %v", err)
    }
    return uaaRes, u, &cnsiRecord, nil
}

// logoutOfCNSI godoc
// @Summary Disconnect from endpoint
// @Description
// @Accept    x-www-form-urlencoded
// @Produce    json
// @Param cnsi_guid path string true "Endpoint GUID"
// @Success 200
// @Failure 400 {object} interfaces.ErrorResponseBody "Error response"
// @Failure 401 {object} interfaces.ErrorResponseBody "Error response"
// @Security ApiKeyAuth
// @Router /tokens/{cnsi_guid} [delete]
func (p *portalProxy) logoutOfCNSI(c echo.Context) error {
    log.Debug("logoutOfCNSI")

    cnsiGUID := c.Param("cnsi_guid")

    if len(cnsiGUID) == 0 {
        return interfaces.NewHTTPShadowError(
            http.StatusBadRequest,
            "Missing target endpoint",
            "Need CNSI GUID passed as form param")
    }

    userGUID, err := p.GetSessionStringValue(c, "user_id")
    if err != nil {
        return fmt.Errorf("Could not find correct session value: %s", err)
    }

    cnsiRecord, err := p.GetCNSIRecord(cnsiGUID)
    if err != nil {
        return fmt.Errorf("Unable to load CNSI record: %s", err)
    }

    // Get the existing token to see if it is connected as a system shared endpoint
    tr, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID)
    if ok && tr.SystemShared {
        // User needs to be an admin
        user, err := p.StratosAuthService.GetUser(userGUID)
        if err != nil {
            return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - could not check user")
        }

        if !user.Admin {
            return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - user is not an administrator")
        }
        userGUID = tokens.SystemSharedUserGuid
    }

    // Clear the token
    return p.ClearCNSIToken(cnsiRecord, userGUID)
}

func (p *portalProxy) DoAuthFlowRequest(cnsiRequest *interfaces.CNSIRequest, req *http.Request, authHandler interfaces.AuthHandlerFunc) (*http.Response, error) {

    // get a cnsi token record and a cnsi record
    tokenRec, cnsi, err := p.getCNSIRequestRecords(cnsiRequest)
    if err != nil {
        return nil, fmt.Errorf("Unable to retrieve Endpoint records: %v", err)
    }
    return authHandler(tokenRec, cnsi)
}

// Clear the CNSI token
func (p *portalProxy) ClearCNSIToken(cnsiRecord interfaces.CNSIRecord, userGUID string) error {
    // If cnsi is cf AND cf is auto-register only clear the entry
    p.Config.AutoRegisterCFUrl = strings.TrimRight(p.Config.AutoRegisterCFUrl, "/")
    if cnsiRecord.CNSIType == "cf" && p.GetConfig().AutoRegisterCFUrl == cnsiRecord.APIEndpoint.String() {
        log.Debug("Setting token record as disconnected")

        tokenRecord := p.InitEndpointTokenRecord(0, "cleared_token", "cleared_token", true)
        if err := p.setCNSITokenRecord(cnsiRecord.GUID, userGUID, tokenRecord); err != nil {
            return fmt.Errorf("Unable to clear token: %s", err)
        }
    } else {
        log.Debug("Deleting Token")
        if err := p.deleteCNSIToken(cnsiRecord.GUID, userGUID); err != nil {
            return fmt.Errorf("Unable to delete token: %s", err)
        }
    }

    return nil
}

func (p *portalProxy) GetCNSIUser(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, bool) {
    user, _, ok := p.GetCNSIUserAndToken(cnsiGUID, userGUID)
    return user, ok
}

func (p *portalProxy) GetCNSIUserAndToken(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, *interfaces.TokenRecord, bool) {
    log.Debug("GetCNSIUserAndToken")

    // get the uaa token record
    cfTokenRecord, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID)
    if !ok {
        msg := "Unable to retrieve CNSI token record."
        log.Debug(msg)
        return nil, nil, false
    }

    cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, &cfTokenRecord)

    // If this is a system shared endpoint, then remove some metadata that should not be send back to other users
    santizeInfoForSystemSharedTokenUser(cnsiUser, cfTokenRecord.SystemShared)

    return cnsiUser, &cfTokenRecord, ok
}

func (p *portalProxy) GetCNSIUserFromToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) {
    log.Debug("GetCNSIUserFromToken")

    // Custom handler for the Auth type available?
    authProvider := p.GetAuthProvider(cfTokenRecord.AuthType)
    if authProvider.UserInfo != nil {
        return authProvider.UserInfo(cnsiGUID, cfTokenRecord)
    }

    // Default
    return p.GetCNSIUserFromOAuthToken(cnsiGUID, cfTokenRecord)
}

func (p *portalProxy) GetCNSIUserFromBasicToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) {
    return &interfaces.ConnectedUser{
        GUID: cfTokenRecord.RefreshToken,
        Name: cfTokenRecord.RefreshToken,
    }, true
}

func (p *portalProxy) GetCNSIUserFromOAuthToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) {
    var cnsiUser *interfaces.ConnectedUser
    var scope = []string{}

    // get the scope out of the JWT token data
    userTokenInfo, err := p.GetUserTokenInfo(cfTokenRecord.AuthToken)
    if err != nil {
        msg := "Unable to find scope information in the CNSI UAA Auth Token: %s"
        log.Errorf(msg, err)
        return nil, false
    }

    // add the uaa entry to the output
    cnsiUser = &interfaces.ConnectedUser{
        GUID:   userTokenInfo.UserGUID,
        Name:   userTokenInfo.UserName,
        Scopes: userTokenInfo.Scope,
    }
    scope = userTokenInfo.Scope

    // is the user an CF admin?
    cnsiRecord, err := p.GetCNSIRecord(cnsiGUID)
    if err != nil {
        msg := "Unable to load CNSI record: %s"
        log.Errorf(msg, err)
        return nil, false
    }
    // TODO should be an extension point
    if cnsiRecord.CNSIType == "cf" {
        cnsiAdmin := strings.Contains(strings.Join(scope, ""), p.Config.CFAdminIdentifier)
        cnsiUser.Admin = cnsiAdmin
    }

    return cnsiUser, true
}

// Helper to initialize a token record using the specified parameters
func (p *portalProxy) InitEndpointTokenRecord(expiry int64, authTok string, refreshTok string, disconnect bool) interfaces.TokenRecord {
    tokenRecord := interfaces.TokenRecord{
        AuthToken:    authTok,
        RefreshToken: refreshTok,
        TokenExpiry:  expiry,
        Disconnected: disconnect,
        AuthType:     interfaces.AuthTypeOAuth2,
    }

    return tokenRecord
}

func (p *portalProxy) deleteCNSIToken(cnsiID string, userGUID string) error {
    log.Debug("deleteCNSIToken")

    err := p.unsetCNSITokenRecord(cnsiID, userGUID)
    if err != nil {
        log.Errorf("%v", err)
        return err
    }

    return nil
}