From 0bceb409e54b6d01b3e07209af886f6417a74943 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Thu, 6 Jul 2017 20:49:36 +0200 Subject: [PATCH] implement auth middleware --- api/auth.go | 64 +++++++++++++++++++ api/auth_test.go | 141 +++++++++++++++++++++++++++++++++++++++++- config/config.go | 19 +++++- config/config_test.go | 16 ++++- 4 files changed, 233 insertions(+), 7 deletions(-) diff --git a/api/auth.go b/api/auth.go index 778f64e..354dd58 100644 --- a/api/auth.go +++ b/api/auth.go @@ -1 +1,65 @@ package api + +import ( + "net/http" + "strings" + + "github.com/Pterodactyl/wings/config" + "github.com/Pterodactyl/wings/control" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const ( + accessTokenHeader = "X-Access-Token" + accessServerHeader = "X-Access-Server" +) + +type responseError struct { + Error string `json:"error"` +} + +// AuthHandler returns a HandlerFunc that checks request authentication +// permission is a permission string describing the required permission to access the route +func AuthHandler(permission string) gin.HandlerFunc { + return func(c *gin.Context) { + requestToken := c.Request.Header.Get(accessTokenHeader) + requestServer := c.Request.Header.Get(accessServerHeader) + + if requestToken != "" { + // c: master controller, permissions not related to specific server + if strings.HasPrefix(permission, "c:") { + if config.Get().ContainsAuthKey(requestToken) { + return + } + } else { + // All other permission strings not starting with c: require a server to be provided + if requestServer != "" { + server := control.GetServer(requestServer) + if server != nil { + if strings.HasPrefix(permission, "g:") { + if config.Get().ContainsAuthKey(requestToken) { + return + } + } + + if strings.HasPrefix(permission, "s:") { + if server.HasPermission(requestToken, permission) { + return + } + } + } else { + c.JSON(http.StatusNotFound, responseError{"Server defined in " + accessServerHeader + " is not known."}) + } + } else { + c.JSON(http.StatusBadRequest, responseError{"No " + accessServerHeader + " header provided."}) + } + } + } else { + log.Debug("Token missing in request.") + c.JSON(http.StatusBadRequest, responseError{"No " + accessTokenHeader + " header provided."}) + } + c.JSON(http.StatusForbidden, responseError{"You are do not have permission to perform this action."}) + c.Abort() + } +} diff --git a/api/auth_test.go b/api/auth_test.go index 9e4bd93..ff9e7fa 100644 --- a/api/auth_test.go +++ b/api/auth_test.go @@ -1,11 +1,148 @@ package api import ( + "net/http" + "net/http/httptest" "testing" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + + "github.com/Pterodactyl/wings/config" + "github.com/Pterodactyl/wings/control" ) -func TestFunction(t *testing.T) { - assert.Equal(t, 1, 1) +const configFile = "_testdata/config.json" + +func TestAuthHandler(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + t.Run("rejects missing token", func(t *testing.T) { + loadConfiguration(t, false) + + responded, rec := requestMiddlewareWith("c:somepermission", "", "") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusBadRequest) + }) + + t.Run("rejects c:* with invalid key", func(t *testing.T) { + loadConfiguration(t, false) + + responded, rec := requestMiddlewareWith("c:somepermission", "invalidkey", "") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusForbidden) + }) + + t.Run("accepts existing c: key", func(t *testing.T) { + loadConfiguration(t, false) + + responded, rec := requestMiddlewareWith("c:somepermission", "existingkey", "") // TODO: working token + + assert.True(t, responded) + assert.Equal(t, rec.Code, http.StatusOK) + }) + + t.Run("rejects missing server uuid", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("g:test", "existingkey", "") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusBadRequest) + }) + + t.Run("rejects not existing server", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("g:test", "existingkey", "notexistingserver") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusNotFound) + }) + + t.Run("accepts server with existing g: key", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("g:test", "existingkey", "existingserver") + + assert.True(t, responded) + assert.Equal(t, rec.Code, http.StatusOK) + }) + + t.Run("rejects server with not existing g: key", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("g:test", "notexistingkey", "existingserver") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusForbidden) + }) + + t.Run("rejects server with not existing s: key", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("s:test", "notexistingskey", "existingserver") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusForbidden) + }) + + t.Run("accepts server with existing s: key with specific permission", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("s:test", "existingspecificskey", "existingserver") + + assert.True(t, responded) + assert.Equal(t, rec.Code, http.StatusOK) + }) + + t.Run("accepts server with existing s: key with gloabl permission", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("s:test", "existingglobalskey", "existingserver") + + assert.True(t, responded) + assert.Equal(t, rec.Code, http.StatusOK) + }) + + t.Run("rejects server with existing s: key without permission", func(t *testing.T) { + loadConfiguration(t, true) + + responded, rec := requestMiddlewareWith("s:without", "existingspecificskey", "existingserver") + + assert.False(t, responded) + assert.Equal(t, rec.Code, http.StatusForbidden) + }) +} + +func requestMiddlewareWith(neededPermission string, token string, serverUUID string) (responded bool, recorder *httptest.ResponseRecorder) { + router := gin.New() + responded = false + recorder = httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + + router.GET("/", AuthHandler(neededPermission), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted.") + responded = true + }) + + req.Header.Set(accessTokenHeader, token) + req.Header.Set(accessServerHeader, serverUUID) + router.ServeHTTP(recorder, req) + return +} + +func loadConfiguration(t *testing.T, serverConfig bool) { + if err := config.LoadConfiguration(configFile); err != nil { + t.Error(err) + return + } + + if serverConfig { + if err := control.LoadServerConfigurations("_testdata/servers/"); err != nil { + t.Error(err) + } + } } diff --git a/config/config.go b/config/config.go index 8fb2dca..5f744bd 100644 --- a/config/config.go +++ b/config/config.go @@ -76,13 +76,16 @@ type Config struct { // If set to <= 0 logs are kept forever DeleteAfterDays int `mapstructure:"deleteAfterDays"` } `mapstructure:"log"` + + AuthKeys []string `mapstructure:"authKeys"` } var config *Config -func LoadConfiguration(path *string) error { - if path != nil { - viper.SetConfigFile(*path) +// LoadConfiguration loads the configuration from a specified file +func LoadConfiguration(path string) error { + if path != "" { + viper.SetConfigFile(path) } else { viper.AddConfigPath("./") viper.SetConfigName("config") @@ -109,3 +112,13 @@ func Get() *Config { func setDefaults() { } + +// ContainsAuthKey checks wether the config contains a specified authentication key +func (c *Config) ContainsAuthKey(key string) bool { + for _, k := range c.AuthKeys { + if k == key { + return true + } + } + return false +} diff --git a/config/config_test.go b/config/config_test.go index 8261c91..eaa724d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -6,10 +6,22 @@ import ( "github.com/stretchr/testify/assert" ) -var configFile = "../config.example.json" +const configFile = "../config.example.json" func TestLoadConfiguraiton(t *testing.T) { - err := LoadConfiguration(&configFile) + err := LoadConfiguration(configFile) assert.Nil(t, err) assert.Equal(t, Get().Web.ListenHost, "0.0.0.0") } + +func TestContainsAuthKey(t *testing.T) { + t.Run("key exists", func(t *testing.T) { + LoadConfiguration(configFile) + assert.True(t, Get().ContainsAuthKey("somekey")) + }) + + t.Run("key doesn't exist", func(t *testing.T) { + LoadConfiguration(configFile) + assert.False(t, Get().ContainsAuthKey("someotherkey")) + }) +}