Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for API keys #4515

Merged
merged 8 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/jetstream/apikeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"net/http"

"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/apikeys"
"github.com/labstack/echo"
log "github.com/sirupsen/logrus"
)

func (p *portalProxy) addAPIKey(c echo.Context) error {
log.Debug("addAPIKey")

userGUID := c.Get("user_id").(string)
comment := c.FormValue("comment")

if len(comment) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty")
}

apiKeysRepo, err := apikeys.NewPgsqlAPIKeysRepository(p.DatabaseConnectionPool)
if err != nil {
log.Errorf("Database error getting repo for API keys: %v", err)
return err
}

apiKey, err := apiKeysRepo.AddAPIKey(userGUID, comment)
if err != nil {
log.Errorf("Error adding API key %v", err)
return err
}

return c.JSON(http.StatusOK, apiKey)
}

func (p *portalProxy) listAPIKeys(c echo.Context) error {
log.Debug("listAPIKeys")

apiKeysRepo, err := apikeys.NewPgsqlAPIKeysRepository(p.DatabaseConnectionPool)
if err != nil {
log.Errorf("Database error getting repo for API keys: %v", err)
return err
}

userGUID := c.Get("user_id").(string)

apiKeys, err := apiKeysRepo.ListAPIKeys(userGUID)
if err != nil {
log.Errorf("Error listing API keys %v", err)
return nil
}

return c.JSON(http.StatusOK, apiKeys)
}

func (p *portalProxy) deleteAPIKey(c echo.Context) error {
log.Debug("deleteAPIKey")

userGUID := c.Get("user_id").(string)
keyGUID := c.FormValue("guid")

if len(keyGUID) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty")
}

apiKeysRepo, err := apikeys.NewPgsqlAPIKeysRepository(p.DatabaseConnectionPool)
if err != nil {
log.Errorf("Database error getting repo for API keys: %v", err)
return err
}

if err = apiKeysRepo.DeleteAPIKey(userGUID, keyGUID); err != nil {
log.Errorf("Error deleting API key %v", err)
return echo.NewHTTPError(http.StatusBadRequest, "Error deleting API key")
}

return nil
}
21 changes: 21 additions & 0 deletions src/jetstream/datastore/20200814140918_ApiKeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package datastore

import (
"database/sql"

"bitbucket.org/liamstask/goose/lib/goose"
)

func init() {
RegisterMigration(20200814140918, "ApiKeys", func(txn *sql.Tx, conf *goose.DBConf) error {
apiTokenTable := "CREATE TABLE IF NOT EXISTS api_keys ("
apiTokenTable += "guid VARCHAR(36) NOT NULL UNIQUE,"
apiTokenTable += "secret VARCHAR(36) NOT NULL UNIQUE,"
apiTokenTable += "user_guid VARCHAR(36) NOT NULL,"
apiTokenTable += "comment VARCHAR(255) NOT NULL,"
apiTokenTable += "PRIMARY KEY (guid) );"

_, err := txn.Exec(apiTokenTable)
return err
})
}
19 changes: 15 additions & 4 deletions src/jetstream/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -905,8 +905,19 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) {

// All routes in the session group need the user to be authenticated
sessionGroup := pp.Group("/v1")
sessionGroup.Use(p.sessionMiddleware)
sessionGroup.Use(p.xsrfMiddleware)
sessionGroup.Use(p.sessionMiddleware())
sessionGroup.Use(p.xsrfMiddleware())

sessionGroup.POST("/api_keys", p.addAPIKey)
sessionGroup.GET("/api_keys", p.listAPIKeys)
sessionGroup.DELETE("/api_keys", p.deleteAPIKey)

apiKeyGroupConfig := MiddlewareConfig{Skipper: p.apiKeySkipper}

apiKeyGroup := pp.Group("/v1")
apiKeyGroup.Use(p.apiKeyMiddleware)
apiKeyGroup.Use(p.sessionMiddlewareWithConfig(apiKeyGroupConfig))
apiKeyGroup.Use(p.xsrfMiddlewareWithConfig(apiKeyGroupConfig))

for _, plugin := range p.Plugins {
middlewarePlugin, err := plugin.GetMiddlewarePlugin()
Expand All @@ -932,8 +943,8 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) {
sessionAuthGroup.GET("/session/verify", p.verifySession)

// CNSI operations
sessionGroup.GET("/cnsis", p.listCNSIs)
sessionGroup.GET("/cnsis/registered", p.listRegisteredCNSIs)
apiKeyGroup.GET("/cnsis", p.listCNSIs)
apiKeyGroup.GET("/cnsis/registered", p.listRegisteredCNSIs)

// Info
sessionGroup.GET("/info", p.info)
Expand Down
216 changes: 160 additions & 56 deletions src/jetstream/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"crypto/subtle"
"database/sql"
"errors"
"fmt"
"net/http"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/labstack/echo"
log "github.com/sirupsen/logrus"

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

Expand All @@ -28,6 +30,15 @@ const StratosSSOHeader = "x-stratos-sso-login"
// Header to communicate any error during SSO
const StratosSSOErrorHeader = "x-stratos-sso-error"

// APIKeyContextKey - context
const APIKeySkipperContextKey = "valid_api_key"

// APIKeyHeader - API key authentication header name
const APIKeyHeader = "Authentication"

// APIKeyAuthScheme - API key authentication scheme
const APIKeyAuthScheme = "Bearer"

func handleSessionError(config interfaces.PortalConfig, c echo.Context, err error, doNotLog bool, msg string) error {
log.Debug("handleSessionError")

Expand Down Expand Up @@ -65,75 +76,113 @@ func handleSessionError(config interfaces.PortalConfig, c echo.Context, err erro
)
}

func (p *portalProxy) sessionMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Debug("sessionMiddleware")
type (
// Skipper - skipper function for middlewares
Skipper func(echo.Context) bool

p.removeEmptyCookie(c)
// MiddlewareConfig defines the config for Logger middleware.
MiddlewareConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
}
)

userID, err := p.GetSessionValue(c, "user_id")
if err == nil {
c.Set("user_id", userID)
return h(c)
}
func (p *portalProxy) sessionMiddleware() echo.MiddlewareFunc {

// Don't log an error if we are verifying the session, as a failure is not an error
isVerify := strings.HasSuffix(c.Request().RequestURI, "/auth/session/verify")
if isVerify {
// Tell the frontend what the Cookie Domain is so it can check if sessions will work
c.Response().Header().Set(StratosDomainHeader, p.Config.CookieDomain)
}
return p.sessionMiddlewareWithConfig(MiddlewareConfig{})
}

// Clear any session cookie
cookie := new(http.Cookie)
cookie.Name = p.SessionCookieName
cookie.Value = ""
cookie.Expires = time.Now().Add(-24 * time.Hour)
cookie.Domain = p.SessionStoreOptions.Domain
cookie.HttpOnly = p.SessionStoreOptions.HttpOnly
cookie.Secure = p.SessionStoreOptions.Secure
cookie.Path = p.SessionStoreOptions.Path
cookie.MaxAge = 0
c.SetCookie(cookie)

return handleSessionError(p.Config, c, err, isVerify, "User session could not be found")
func (p *portalProxy) sessionMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc {
// Default skipper function always returns false
if config.Skipper == nil {
config.Skipper = func(c echo.Context) bool { return false }
}
}

// Support for Angular XSRF
func (p *portalProxy) xsrfMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Debug("xsrfMiddleware")
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Debug("sessionMiddleware")

// Only do this for mutating requests - i.e. we can ignore for GET or HEAD requests
if c.Request().Method == "GET" || c.Request().Method == "HEAD" {
return h(c)
}
if config.Skipper(c) {
log.Debug("Skipping sessionMiddleware")
return h(c)
}

// Routes registered with /apps are assumed to be web apps that do their own XSRF
if strings.HasPrefix(c.Request().URL.String(), "/pp/v1/apps/") {
return h(c)
p.removeEmptyCookie(c)

userID, err := p.GetSessionValue(c, "user_id")
if err == nil {
c.Set("user_id", userID)
return h(c)
}

// Don't log an error if we are verifying the session, as a failure is not an error
isVerify := strings.HasSuffix(c.Request().RequestURI, "/auth/session/verify")
if isVerify {
// Tell the frontend what the Cookie Domain is so it can check if sessions will work
c.Response().Header().Set(StratosDomainHeader, p.Config.CookieDomain)
}

// Clear any session cookie
cookie := new(http.Cookie)
cookie.Name = p.SessionCookieName
cookie.Value = ""
cookie.Expires = time.Now().Add(-24 * time.Hour)
cookie.Domain = p.SessionStoreOptions.Domain
cookie.HttpOnly = p.SessionStoreOptions.HttpOnly
cookie.Secure = p.SessionStoreOptions.Secure
cookie.Path = p.SessionStoreOptions.Path
cookie.MaxAge = 0
c.SetCookie(cookie)

return handleSessionError(p.Config, c, err, isVerify, "User session could not be found")
}
}
}

errMsg := "Failed to get stored XSRF token from user session"
token, err := p.GetSessionStringValue(c, XSRFTokenSessionName)
if err == nil {
// Check the token against the header
requestToken := c.Request().Header.Get(XSRFTokenHeader)
if len(requestToken) > 0 {
if compareTokens(requestToken, token) {
return h(c)
func (p *portalProxy) xsrfMiddleware() echo.MiddlewareFunc {
return p.xsrfMiddlewareWithConfig(MiddlewareConfig{})
}

func (p *portalProxy) xsrfMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc {
// Default skipper function always returns false
if config.Skipper == nil {
config.Skipper = func(c echo.Context) bool { return false }
}

return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Debug("xsrfMiddleware")

// Only do this for mutating requests - i.e. we can ignore for GET or HEAD requests
if c.Request().Method == "GET" || c.Request().Method == "HEAD" {
return h(c)
}

// Routes registered with /apps are assumed to be web apps that do their own XSRF
if strings.HasPrefix(c.Request().URL.String(), "/pp/v1/apps/") {
return h(c)
}

errMsg := "Failed to get stored XSRF token from user session"
token, err := p.GetSessionStringValue(c, XSRFTokenSessionName)
if err == nil {
// Check the token against the header
requestToken := c.Request().Header.Get(XSRFTokenHeader)
if len(requestToken) > 0 {
if compareTokens(requestToken, token) {
return h(c)
}
errMsg = "Supplied XSRF Token does not match"
} else {
errMsg = "XSRF Token was not supplied in the header"
}
errMsg = "Supplied XSRF Token does not match"
} else {
errMsg = "XSRF Token was not supplied in the header"
}
return interfaces.NewHTTPShadowError(
http.StatusUnauthorized,
"XSRF Token could not be found or does not match",
"XSRF Token error: %s", errMsg,
)
}
return interfaces.NewHTTPShadowError(
http.StatusUnauthorized,
"XSRF Token could not be found or does not match",
"XSRF Token error: %s", errMsg,
)
}
}

Expand Down Expand Up @@ -254,3 +303,58 @@ func retryAfterUpgradeMiddleware(h echo.HandlerFunc, env *env.VarSet) echo.Handl
return h(c)
}
}

func getAPIKeyFromHeader(c echo.Context) (string, error) {
header := c.Request().Header.Get(APIKeyHeader)

l := len(APIKeyAuthScheme)
if len(header) > l+1 && header[:l] == APIKeyAuthScheme {
return header[l+1:], nil
}

return "", errors.New("No API key in the header")
}

func (p *portalProxy) apiKeyMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Debug("apiKeyMiddleware")

apiKey, err := getAPIKeyFromHeader(c)
if err != nil {
log.Debugf("apiKeyMiddleware: %v", err)
return h(c)
}

apiKeysRepo, err := apikeys.NewPgsqlAPIKeysRepository(p.DatabaseConnectionPool)
if err != nil {
log.Errorf("apiKeyMiddleware: %v", err)
return h(c)
}

userID, err := apiKeysRepo.GetAPIKeyUserID(apiKey)
if err != nil {
switch {
case err == sql.ErrNoRows:
log.Debug("apiKeyMiddleware: Invalid API key supplied")
default:
log.Warnf("apiKeyMiddleware: %v", err)
}

return h(c)
}

c.Set(APIKeySkipperContextKey, true)
c.Set("user_id", userID)

// some endpoints check not only the context store, but also the contents of the session store
sessionValues := make(map[string]interface{})
sessionValues["user_id"] = userID
p.setSessionValues(c, sessionValues)

return h(c)
}
}

func (p *portalProxy) apiKeySkipper(c echo.Context) bool {
return c.Get(APIKeySkipperContextKey) != nil && c.Get(APIKeySkipperContextKey).(bool) == true
}
Loading