Upgrades to Config

* Added config file and config tests
* Configs per stack can be set up depending on their config/STACK folder
and tested appropriately to add config redundancy
This commit is contained in:
🐙PiperYxzzy
2022-08-14 12:55:11 +02:00
parent b198814aa7
commit 9cc37b0d0d
9 changed files with 170 additions and 14 deletions

View File

@@ -1,4 +0,0 @@
{
"AllowFreshAdminGeneration": true,
"AdminEmails": ["admin@admin.invalid"]
}

View File

@@ -2,21 +2,37 @@ package config
import ( import (
"encoding/json" "encoding/json"
"log"
"os" "os"
) )
type StackConfiguration struct { type StackConfiguration struct {
ConfigLoaded bool ConfigLoaded bool
AllowFreshAdminGeneration bool AllowFreshAdminGeneration bool
AdminEmails []string AdminEmails []string
AdminHmacEnv string AdminHmacEnv string
UserHmacEnv string UserHmacEnv string
AuthedRateLimitConfig string
UnauthedRateLimitConfig string
} }
var Environment = os.Getenv("STACK_ENVIRONMENT")
var Config = StackConfiguration{} var Config = StackConfiguration{}
func GetConfigPath(filename string) string {
if Environment == "" {
Environment = "dev"
}
return Environment + "/" + filename
}
func LoadConfig() { func LoadConfig() {
file, _ := os.Open("conf.json") file, err := os.Open(GetConfigPath("conf.json"))
if err != nil {
panic(err)
}
defer file.Close() defer file.Close()
dec := json.NewDecoder(file) dec := json.NewDecoder(file)
if err := dec.Decode(&Config); err != nil { if err := dec.Decode(&Config); err != nil {
@@ -24,4 +40,6 @@ func LoadConfig() {
} }
Config.ConfigLoaded = true Config.ConfigLoaded = true
log.Printf("Loaded Config for stack " + Environment)
} }

68
config/config_test.go Normal file
View File

@@ -0,0 +1,68 @@
package config
import (
"testing"
)
func TestAllConfigs(t *testing.T) {
SingleStackTest(t, "dev", StackConfiguration{
AllowFreshAdminGeneration: true,
AdminEmails: []string{"admin@admin.invalid"},
AdminHmacEnv: "ADMIN_HMAC_ENV",
UserHmacEnv: "USER_HMAC_ENV",
AuthedRateLimitConfig: "ratelimit.auth.json",
UnauthedRateLimitConfig: "ratelimit.unauth.json",
})
}
func SingleStackTest(t *testing.T, stack string, expected StackConfiguration) {
Config = StackConfiguration{}
if Config.ConfigLoaded {
t.Errorf("Config.ConfigLoaded should be false before any processing")
}
if len(Config.AdminEmails) > 0 ||
Config.AdminHmacEnv != "" ||
Config.UserHmacEnv != "" ||
Config.AllowFreshAdminGeneration ||
Config.AuthedRateLimitConfig != "" ||
Config.UnauthedRateLimitConfig != "" { // Extend this IF for any other config values
t.Errorf("Config already has values before loading")
}
Environment = stack
LoadConfig()
if !Config.ConfigLoaded {
t.Errorf("Config was not set to loaded")
}
// Finally test values
if Config.AllowFreshAdminGeneration != expected.AllowFreshAdminGeneration {
t.Errorf("AllowFreshAdminGeneration value not set properly")
}
for i, email := range Config.AdminEmails {
if expected.AdminEmails[i] != email {
t.Errorf("AdminEmails value not set properly, expected %v at %v, was %v", expected.AdminEmails[i], i, email)
}
}
if Config.AdminHmacEnv != expected.AdminHmacEnv {
t.Errorf("AdminHmacEnv value not set properly")
}
if Config.UserHmacEnv != expected.UserHmacEnv {
t.Errorf("UserHmacEnv value not set properly")
}
if Config.AuthedRateLimitConfig != expected.AuthedRateLimitConfig {
t.Errorf("AuthedRateLimitConfig value not set properly")
}
if Config.UnauthedRateLimitConfig != expected.UnauthedRateLimitConfig {
t.Errorf("UnauthedRateLimitConfig value not set properly")
}
}

8
config/dev/conf.json Normal file
View File

@@ -0,0 +1,8 @@
{
"AllowFreshAdminGeneration": true,
"AdminEmails": ["admin@admin.invalid"],
"AdminHmacEnv": "ADMIN_HMAC_ENV",
"UserHmacEnv": "USER_HMAC_ENV",
"AuthedRateLimitConfig": "ratelimit.auth.json",
"UnauthedRateLimitConfig": "ratelimit.unauth.json"
}

View File

@@ -0,0 +1,17 @@
{
"": {"seconds": 60, "max": 30, "_comment": "Global ratelimit."},
"/v1/sec/doot":
{"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"},
"/v1/sec/2fa-doot":
{"seconds": 10, "max": 1, "_comment": "2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept."},
"/v1/adm/doot":
{"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"},
"/v1/adm/2fa-doot":
{"seconds": 10, "max": 1, "_comment": "2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept."}
}

View File

@@ -0,0 +1,19 @@
{
"":
{"seconds": 60, "max": 30, "_comment": "Global unauthenticated ratelimit."},
"/v1/doot":
{"seconds": 5, "max": 5, "_comment": "Unauthenticated DOOT for server monitoring."},
"/v1/login":
{"seconds": 60, "max": 3, "_comment": "Prevent bruteforce attacks on Login."},
"/v1/admin":
{"seconds": 60, "max": 1, "_comment": "Prevent bruteforce attacks on Admin Login."},
"/v1/signup":
{"seconds": 1800, "max": 1, "_comment": "Prevent spam account creation."},
"/v1/forgot":
{"seconds": 60, "max": 1, "_comment": "Slow down 'forgot password' enumeration/spam."}
}

View File

@@ -301,8 +301,7 @@ func UserResetForgottenPassword() gin.HandlerFunc {
resetting.SetPassword(resetVals.NewPassword) resetting.SetPassword(resetVals.NewPassword)
if err := resetting.Save(); err != nil { if err := resetting.Save(); err != nil {
log. log.Println("could not save user", err)
log.Error("could not save user", err)
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
return return
} }

View File

@@ -1,15 +1,23 @@
package controllers package controllers
import ( import (
"encoding/json"
"log" "log"
"net/http" "net/http"
"os"
"regexp" "regexp"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yxzzy-wtf/gin-gonic-prepack/config"
"github.com/yxzzy-wtf/gin-gonic-prepack/util" "github.com/yxzzy-wtf/gin-gonic-prepack/util"
) )
type ruleDescription struct {
seconds int
max int
}
type rule struct { type rule struct {
duration time.Duration duration time.Duration
limit int limit int
@@ -71,6 +79,20 @@ type megabucket struct {
rules map[string]rule rules map[string]rule
} }
func (m *megabucket) loadFromConfig(filename string) {
file, _ := os.Open("conf.json")
defer file.Close()
dec := json.NewDecoder(file)
ruleMap := map[string]ruleDescription{}
if err := dec.Decode(&ruleMap); err != nil {
panic(err)
}
for rkey, r := range ruleMap {
m.rules[rkey] = rule{duration: time.Second * time.Duration(r.seconds), limit: r.max}
}
}
func (m *megabucket) take(signature string, resource string) bool { func (m *megabucket) take(signature string, resource string) bool {
b, ex := m.buckets[signature] b, ex := m.buckets[signature]
if !ex { if !ex {
@@ -86,10 +108,9 @@ func (m *megabucket) take(signature string, resource string) bool {
var unauthed = megabucket{ var unauthed = megabucket{
buckets: map[string]bucket{}, buckets: map[string]bucket{},
rules: map[string]rule{ rules: map[string]rule{},
"*": {duration: time.Second * 10, limit: 20},
},
} }
var unauthLoaded = false
/** /**
* Applies rate limiting to unauthorized actors based on their IP address. * Applies rate limiting to unauthorized actors based on their IP address.
@@ -97,6 +118,11 @@ var unauthed = megabucket{
*/ */
func UnauthRateLimit() gin.HandlerFunc { func UnauthRateLimit() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if !unauthLoaded {
unauthed.loadFromConfig(config.GetConfigPath(config.Config.UnauthedRateLimitConfig))
unauthLoaded = true
}
ip := c.ClientIP() ip := c.ClientIP()
if !unauthed.take(ip, "") { if !unauthed.take(ip, "") {
@@ -108,10 +134,9 @@ func UnauthRateLimit() gin.HandlerFunc {
var authed = megabucket{ var authed = megabucket{
buckets: map[string]bucket{}, buckets: map[string]bucket{},
rules: map[string]rule{ rules: map[string]rule{},
"*": {duration: time.Second * 10, limit: 5},
},
} }
var authLoaded = false
/** /**
* Authorized rate limit. Using the UID of the authorized user as the * Authorized rate limit. Using the UID of the authorized user as the
@@ -119,6 +144,11 @@ var authed = megabucket{
*/ */
func AuthedRateLimit() gin.HandlerFunc { func AuthedRateLimit() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if !authLoaded {
authed.loadFromConfig(config.GetConfigPath(config.Config.AuthedRateLimitConfig))
authLoaded = true
}
pif, exists := c.Get("principal") pif, exists := c.Get("principal")
p := pif.(util.PrincipalInfo) p := pif.(util.PrincipalInfo)
if !exists { if !exists {

View File

@@ -75,6 +75,7 @@ func main() {
v1Admin := v1.Group("/adm", core.AdminAuth()) v1Admin := v1.Group("/adm", core.AdminAuth())
v1Admin.GET("/doot", core.Doot()) v1Admin.GET("/doot", core.Doot())
v1Admin.GET("/2fa-doot", core.LiveTwoFactor(), core.Doot())
// Start server // Start server
if err := http.ListenAndServe(":9091", r); err != nil { if err := http.ListenAndServe(":9091", r); err != nil {