From 4b94b962f1e20883e2f0d33b76267ea0ac04077a Mon Sep 17 00:00:00 2001 From: peopleig Date: Tue, 14 Oct 2025 11:47:29 +0530 Subject: [PATCH 01/11] Add plugin functionality and dummy plugin for testing --- api/main.go | 6 +++++- plugins/base.go | 35 +++++++++++++++++++++++++++++++++++ plugins/dummy/plugin.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 plugins/base.go create mode 100644 plugins/dummy/plugin.go diff --git a/api/main.go b/api/main.go index 1ca6c93e..215fc4f3 100644 --- a/api/main.go +++ b/api/main.go @@ -17,6 +17,7 @@ import ( "github.com/sdslabs/beastv4/pkg/remoteManager" "github.com/sdslabs/beastv4/pkg/scheduler" wpool "github.com/sdslabs/beastv4/pkg/workerpool" + "github.com/sdslabs/beastv4/plugins" ) const ( @@ -66,7 +67,7 @@ func RunBeastApiServer(port, defaultauthorpassword string, autoDeploy, healthPro auth.Init(core.ITERATIONS, core.HASH_LENGTH, core.TIMEPERIOD, core.ISSUER, config.Cfg.JWTSecret, []string{core.USER_ROLES["author"]}, []string{core.USER_ROLES["admin"]}, []string{core.USER_ROLES["contestant"]}) remoteManager.Init() database.Init() - + runBeastApiBootsteps(defaultauthorpassword) // Initialize Gin router. @@ -76,6 +77,9 @@ func RunBeastApiServer(port, defaultauthorpassword string, autoDeploy, healthPro router.Use(gin.Logger()) router.Use(gin.Recovery()) + // Initialize all plugins + plugins.InitPlugins(router) + router.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) router.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, HTTPPlainResp{ diff --git a/plugins/base.go b/plugins/base.go new file mode 100644 index 00000000..2c5f3061 --- /dev/null +++ b/plugins/base.go @@ -0,0 +1,35 @@ +package plugins + +import ( + log "github.com/sirupsen/logrus" + + "github.com/gin-gonic/gin" +) + +type Plugin interface { + Name() string + Description() string + Init(*gin.Engine) error +} + +//All plugins will be initialized with the gin engine, at startup +//Logging twice, once when we register and once when we initialize + +var loadedPlugins []Plugin + +func Register(p Plugin) { + loadedPlugins = append(loadedPlugins, p) + log.Infof("Plugin registered: %s\n %s", p.Name(), p.Description()) + +} + +func InitPlugins(router *gin.Engine) { + + for _, p := range loadedPlugins { + log.Infof("Intializing plugin: %s", p.Name()) + err := p.Init(router) + if err != nil { + log.Errorf("Error in Plugin Initializing %s: %v", p.Name(), err) + } + } +} diff --git a/plugins/dummy/plugin.go b/plugins/dummy/plugin.go new file mode 100644 index 00000000..d2d8bf2c --- /dev/null +++ b/plugins/dummy/plugin.go @@ -0,0 +1,29 @@ +package dummy + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sdslabs/beastv4/plugins" +) + +type DummyPlugin struct{} + +func (p *DummyPlugin) Name() string { + return "DummyPlugin" +} + +func (p *DummyPlugin) Description() string { + return "A dummy plugin to do the testing of plugin system" +} + +func (p *DummyPlugin) Init(router *gin.Engine) error { + router.GET("api/plugins/dummy", func(context *gin.Context) { + context.JSON(http.StatusOK, gin.H{"plugin": "The dummy plugin works"}) + }) + return nil +} + +func init() { + plugins.Register(&DummyPlugin{}) +} From 343fc14c923aacc728142e7eb88d82b7edcc6926 Mon Sep 17 00:00:00 2001 From: peopleig Date: Fri, 17 Oct 2025 15:39:49 +0530 Subject: [PATCH 02/11] Add allowed mail domains to config.toml --- _examples/example.config.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/_examples/example.config.toml b/_examples/example.config.toml index fb6d97d5..3be186a1 100644 --- a/_examples/example.config.toml +++ b/_examples/example.config.toml @@ -117,6 +117,11 @@ host = "localhost" port = "5432" sslmode = "prefer" +#Allowed email ids for the Email Verify plugin +[emailverify] +allowed_domains = ["example.com", "organization.example.com"] + + # The following fields are required only while hosting a competition on beast # This section contains information about the competition to be hosted # Structure of the sections with the acceptable fields are: From 4ae61cd41851fd32f2eff271042bcc73fde562a9 Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 09:55:58 +0530 Subject: [PATCH 03/11] Add email_verify plugin --- core/config/config.go | 9 +++-- plugins/email_verify/plugin.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 plugins/email_verify/plugin.go diff --git a/core/config/config.go b/core/config/config.go index 85bd087a..7018aa0a 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -112,7 +112,7 @@ import ( // dbname = "beast" // host = "localhost" // port = "5432" -// sslmode = "prefer" +// sslmode = "prefer" // ``` type BeastConfig struct { AuthorizedKeysFile string `toml:"authorized_keys_file"` @@ -136,7 +136,8 @@ type BeastConfig struct { PidsLimit int64 `toml:"default_pids_limit"` // For SMTP Configuration - MailConfig MailConfig `toml:"mail_config"` + MailConfig MailConfig `toml:"mail_config"` + EmailVerify EmailVerifyConfig `toml:"emailverify"` } func (config *BeastConfig) ValidateConfig() error { @@ -365,6 +366,10 @@ type MailConfig struct { SMTPPort string `toml:"smtpPort"` } +type EmailVerifyConfig struct { + AllowedDomains []string `toml:"allowed_domains"` +} + func UpdateCompetitionInfo(competitionInfo *CompetitionInfo) error { configPath := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_CONFIG_FILE_NAME) var config BeastConfig diff --git a/plugins/email_verify/plugin.go b/plugins/email_verify/plugin.go new file mode 100644 index 00000000..fad0c5d0 --- /dev/null +++ b/plugins/email_verify/plugin.go @@ -0,0 +1,62 @@ +package emailverify + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/sdslabs/beastv4/core/config" +) + +type EmailVerifyPlugin struct { + AllowedDomains []string +} + +func (p *EmailVerifyPlugin) Name() string { + return "EmailVerifyPlugin" +} + +func (p *EmailVerifyPlugin) Description() string { + return "A plugin to restrict registration to certain email domains" +} + +func (p *EmailVerifyPlugin) isAllowedEmail(email string) bool { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return false + } + domain := parts[1] + for _, allowedDomain := range p.AllowedDomains { + if domain == allowedDomain { + return true + } + } + return false +} + +func (p *EmailVerifyPlugin) Init(router *gin.Engine) error { + cfg := config.Cfg + checkFlag := false + if len(cfg.EmailVerify.AllowedDomains) > 0 { + p.AllowedDomains = cfg.EmailVerify.AllowedDomains + checkFlag = true + } else { + // fallback for dev/testing + p.AllowedDomains = []string{"iitroorkee.ac.in"} + } + authGroup := router.Group("/auth") + authGroup.Use(func(c *gin.Context) { + if checkFlag { + if strings.Contains(c.Request.URL.Path, "/register") && c.Request.Method == "POST" { + email := c.PostForm("email") + if !p.isAllowedEmail(email) { + c.JSON(http.StatusForbidden, gin.H{"error": "Registration restricted to organization emails only"}) + c.Abort() + return + } + } + } + c.Next() + }) + return nil +} From fd2e11c47a2cb1597c874912452759d83840566e Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 09:57:09 +0530 Subject: [PATCH 04/11] Add check flag to email_verify plugin --- plugins/email_verify/plugin.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/email_verify/plugin.go b/plugins/email_verify/plugin.go index fad0c5d0..3246b5bd 100644 --- a/plugins/email_verify/plugin.go +++ b/plugins/email_verify/plugin.go @@ -40,9 +40,6 @@ func (p *EmailVerifyPlugin) Init(router *gin.Engine) error { if len(cfg.EmailVerify.AllowedDomains) > 0 { p.AllowedDomains = cfg.EmailVerify.AllowedDomains checkFlag = true - } else { - // fallback for dev/testing - p.AllowedDomains = []string{"iitroorkee.ac.in"} } authGroup := router.Group("/auth") authGroup.Use(func(c *gin.Context) { From 4ce2585d93c345dc25c6d6bcb9ca9d485d8666fb Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 13:38:18 +0530 Subject: [PATCH 05/11] Update Email Verify Plugin Logic --- plugins/email_verify/plugin.go | 133 ++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 12 deletions(-) diff --git a/plugins/email_verify/plugin.go b/plugins/email_verify/plugin.go index 3246b5bd..aca5f900 100644 --- a/plugins/email_verify/plugin.go +++ b/plugins/email_verify/plugin.go @@ -1,13 +1,33 @@ package emailverify import ( + "errors" "net/http" "strings" + log "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" + "github.com/sdslabs/beastv4/core" "github.com/sdslabs/beastv4/core/config" + "github.com/sdslabs/beastv4/core/database" + "github.com/sdslabs/beastv4/pkg/auth" + "github.com/sdslabs/beastv4/plugins" ) +type HTTPPlainResp struct { + Message string `json:"message" example:"Messsage in response to your request"` +} + +type HTTPPlainMapResp struct { + Messages map[string]string `json:"messages" example:"{\"name1\": \"message1\", \"name2\": \"message2\"}"` +} + +type HTTPErrorResp struct { + Error string `json:"error" example:"Error occured while veifying the challenge."` +} + type EmailVerifyPlugin struct { AllowedDomains []string } @@ -27,6 +47,7 @@ func (p *EmailVerifyPlugin) isAllowedEmail(email string) bool { } domain := parts[1] for _, allowedDomain := range p.AllowedDomains { + log.Debugf("Checking domain: %s against allowed domain: %s", domain, allowedDomain) if domain == allowedDomain { return true } @@ -34,6 +55,94 @@ func (p *EmailVerifyPlugin) isAllowedEmail(email string) bool { return false } +func (p *EmailVerifyPlugin) verifyEmailRegister(c *gin.Context, checkFlag bool) { + name := c.PostForm("name") + username := c.PostForm("username") + password := c.PostForm("password") + email := c.PostForm("email") + sshKey := c.PostForm("ssh-key") + + name = strings.TrimSpace(name) + username = strings.TrimSpace(strings.ToLower(username)) + password = strings.TrimSpace(password) + email = strings.TrimSpace(strings.ToLower(email)) + sshKey = strings.TrimSpace(sshKey) + + if username == "" || password == "" || email == "" { + + c.JSON(http.StatusBadRequest, HTTPPlainResp{ + Message: "Username, password and email can not be empty", + }) + return + } + + if len(username) > 12 { + c.JSON(http.StatusBadRequest, HTTPErrorResp{ + Error: "Username cannot be greater than 12 characters", + }) + return + } + if checkFlag { + if !p.isAllowedEmail(email) { + log.Warnf("Email domain not allowed: %s", email) + c.JSON(http.StatusForbidden, gin.H{ + "error": "Registration restricted to organization emails only", + }) + return + } + } + + userEntry := database.User{ + Name: name, + AuthModel: auth.CreateModel(username, password, core.USER_ROLES["contestant"]), + Email: email, + SshKey: sshKey, + } + + if !config.SkipAuthorization { + smtpHost := config.Cfg.MailConfig.SMTPHost + smtpPort := config.Cfg.MailConfig.SMTPPort + + if smtpHost == "" || smtpPort == "" { + log.Errorf("WARNING: SMTP not configured") + } else { + otpEntry, err := database.QueryOTPEntry(email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusUnauthorized, HTTPErrorResp{ + Error: "OTP not found, email not verified", + }) + return + } else { + log.Println("Failed to query OTP:", err) + c.JSON(http.StatusInternalServerError, HTTPErrorResp{ + Error: "Failed to send OTP", + }) + return + } + } + if !otpEntry.Verified { + c.JSON(http.StatusNotAcceptable, HTTPErrorResp{ + Error: "Email not verified, cannot register user", + }) + return + } + } + } + + err := database.CreateUserEntry(&userEntry) + if err != nil { + c.JSON(http.StatusNotAcceptable, HTTPErrorResp{ + Error: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, HTTPPlainResp{ + Message: "User created successfully", + }) +} + func (p *EmailVerifyPlugin) Init(router *gin.Engine) error { cfg := config.Cfg checkFlag := false @@ -41,19 +150,19 @@ func (p *EmailVerifyPlugin) Init(router *gin.Engine) error { p.AllowedDomains = cfg.EmailVerify.AllowedDomains checkFlag = true } - authGroup := router.Group("/auth") - authGroup.Use(func(c *gin.Context) { - if checkFlag { - if strings.Contains(c.Request.URL.Path, "/register") && c.Request.Method == "POST" { - email := c.PostForm("email") - if !p.isAllowedEmail(email) { - c.JSON(http.StatusForbidden, gin.H{"error": "Registration restricted to organization emails only"}) - c.Abort() - return - } - } + + router.Use(func(c *gin.Context) { + if c.Request.URL.Path == "/auth/register" && c.Request.Method == "POST" { + p.verifyEmailRegister(c, checkFlag) + c.Abort() + } else { + c.Next() } - c.Next() }) + return nil } + +func init() { + plugins.Register(&EmailVerifyPlugin{}) +} From aa39819ccfff17aabac1de9b5e3e6ae074826f6e Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 13:40:25 +0530 Subject: [PATCH 06/11] Update order of initializing routes and plugins --- api/main.go | 6 ++---- api/router.go | 5 +++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/main.go b/api/main.go index 215fc4f3..d0e487df 100644 --- a/api/main.go +++ b/api/main.go @@ -17,7 +17,8 @@ import ( "github.com/sdslabs/beastv4/pkg/remoteManager" "github.com/sdslabs/beastv4/pkg/scheduler" wpool "github.com/sdslabs/beastv4/pkg/workerpool" - "github.com/sdslabs/beastv4/plugins" + _ "github.com/sdslabs/beastv4/plugins/dummy" + _ "github.com/sdslabs/beastv4/plugins/email_verify" ) const ( @@ -77,9 +78,6 @@ func RunBeastApiServer(port, defaultauthorpassword string, autoDeploy, healthPro router.Use(gin.Logger()) router.Use(gin.Recovery()) - // Initialize all plugins - plugins.InitPlugins(router) - router.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) router.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, HTTPPlainResp{ diff --git a/api/router.go b/api/router.go index cd98d4e8..f2b19ff4 100644 --- a/api/router.go +++ b/api/router.go @@ -9,6 +9,7 @@ import ( "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "github.com/sdslabs/beastv4/core" + "github.com/sdslabs/beastv4/plugins" ) func dummyHandler(c *gin.Context) { @@ -29,6 +30,10 @@ func initGinRouter() *gin.Engine { } router.Use(cors.New(corsConfig)) router.GET("/dummy", dummyHandler) + + // Initialize all plugins + plugins.InitPlugins(router) + // Authorization routes group authGroup := router.Group("/auth") { From 8cf4896d529dbc6c222568ef4bf155c72fb46e49 Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 13:45:40 +0530 Subject: [PATCH 07/11] Update logging of email verify plugin --- plugins/email_verify/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/email_verify/plugin.go b/plugins/email_verify/plugin.go index aca5f900..62c25316 100644 --- a/plugins/email_verify/plugin.go +++ b/plugins/email_verify/plugin.go @@ -52,6 +52,7 @@ func (p *EmailVerifyPlugin) isAllowedEmail(email string) bool { return true } } + log.Warnf("Email domain not allowed: %s", domain) return false } @@ -84,7 +85,6 @@ func (p *EmailVerifyPlugin) verifyEmailRegister(c *gin.Context, checkFlag bool) } if checkFlag { if !p.isAllowedEmail(email) { - log.Warnf("Email domain not allowed: %s", email) c.JSON(http.StatusForbidden, gin.H{ "error": "Registration restricted to organization emails only", }) From bee98b7701ee036650d5caa4606fe6a83f5cbd5b Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 13:49:29 +0530 Subject: [PATCH 08/11] Update logging of email verify plugin --- plugins/email_verify/plugin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/email_verify/plugin.go b/plugins/email_verify/plugin.go index 62c25316..b31688e2 100644 --- a/plugins/email_verify/plugin.go +++ b/plugins/email_verify/plugin.go @@ -49,6 +49,7 @@ func (p *EmailVerifyPlugin) isAllowedEmail(email string) bool { for _, allowedDomain := range p.AllowedDomains { log.Debugf("Checking domain: %s against allowed domain: %s", domain, allowedDomain) if domain == allowedDomain { + log.Infof("Email domain allowed: %s", domain) return true } } From 10b02bcdd22ff27c3acbbb198decaf858b77c30c Mon Sep 17 00:00:00 2001 From: peopleig Date: Sun, 19 Oct 2025 20:51:17 +0530 Subject: [PATCH 09/11] Add test for verifying plugin functionality --- plugins/email_verify/test.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 plugins/email_verify/test.sh diff --git a/plugins/email_verify/test.sh b/plugins/email_verify/test.sh new file mode 100644 index 00000000..c5fb36ec --- /dev/null +++ b/plugins/email_verify/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +URL="http://localhost:5005/auth/register" + +echo "---- Testing Allowed Domain ----" +response_allowed=$(curl -s -o /dev/null -w "%{http_code}" -X POST $URL \ + -F "name=Neptune" \ + -F "username=neptune" \ + -F "password=romangod123" \ + -F "email=neptune@example.com") + +if [ "$response_allowed" -eq 200 ]; then + echo "✅ Test Passed-Allowed Domain" +else + echo "❌ Test Failed-Allowed Domain (Got HTTP $response_allowed)" +fi + +echo +echo "---- Testing Disallowed Domain ----" +response_disallowed=$(curl -s -o /dev/null -w "%{http_code}" -X POST $URL \ + -F "name=Poseidon" \ + -F "username=poseidon" \ + -F "password=greekgodsbetter" \ + -F "email=poseidon@gmail.com") + +if [ "$response_disallowed" -eq 403 ]; then + echo "✅ Test Passed - Disallowed Domain" +else + echo "❌ Test Failed - Disallowed Domain (Got HTTP $response_disallowed)" +fi From e70ed011de631a1995556c06545126911b47e7d3 Mon Sep 17 00:00:00 2001 From: peopleig Date: Mon, 27 Oct 2025 12:49:53 +0530 Subject: [PATCH 10/11] Update example.config.toml to allow plugin initialization choice --- _examples/example.config.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/_examples/example.config.toml b/_examples/example.config.toml index 3be186a1..0279d65e 100644 --- a/_examples/example.config.toml +++ b/_examples/example.config.toml @@ -117,6 +117,14 @@ host = "localhost" port = "5432" sslmode = "prefer" +# Configuration related to plugin management in Beast +# This section lists all plugins that should be initialized at runtime +[plugins_config] + +# List of plugin names to be enabled. Only plugins listed here will be initialized +# For reference, Beast currently supports: "DummyPlugin", "EmailVerifyPlugin" +enabled_plugins = ["DummyPlugin", "EmailVerifyPlugin"] + #Allowed email ids for the Email Verify plugin [emailverify] allowed_domains = ["example.com", "organization.example.com"] From eaacea0b21bae95acc6d74c547913451b4c8e64b Mon Sep 17 00:00:00 2001 From: peopleig Date: Mon, 27 Oct 2025 12:56:58 +0530 Subject: [PATCH 11/11] Add choice functionality for plugins --- core/config/config.go | 11 ++++++++++- plugins/base.go | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/config/config.go b/core/config/config.go index 7018aa0a..271e1873 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -136,7 +136,12 @@ type BeastConfig struct { PidsLimit int64 `toml:"default_pids_limit"` // For SMTP Configuration - MailConfig MailConfig `toml:"mail_config"` + MailConfig MailConfig `toml:"mail_config"` + + // Plugins configuration + PluginsEnabled PluginsConfig `toml:"plugins_config"` + + // Email verification plugin configuration EmailVerify EmailVerifyConfig `toml:"emailverify"` } @@ -366,6 +371,10 @@ type MailConfig struct { SMTPPort string `toml:"smtpPort"` } +type PluginsConfig struct { + EnabledPlugins []string `toml:"enabled_plugins"` +} + type EmailVerifyConfig struct { AllowedDomains []string `toml:"allowed_domains"` } diff --git a/plugins/base.go b/plugins/base.go index 2c5f3061..c3a3fb30 100644 --- a/plugins/base.go +++ b/plugins/base.go @@ -1,6 +1,7 @@ package plugins import ( + "github.com/sdslabs/beastv4/core/config" log "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" @@ -23,9 +24,22 @@ func Register(p Plugin) { } -func InitPlugins(router *gin.Engine) { +func isPluginEnabled(pluginName string) bool { + enabledPlugins := config.Cfg.PluginsEnabled.EnabledPlugins + for _, name := range enabledPlugins { + if name == pluginName { + return true + } + } + return false +} +func InitPlugins(router *gin.Engine) { for _, p := range loadedPlugins { + if !isPluginEnabled(p.Name()) { + log.Warnf("%s is not enabled, skipping initialization", p.Name()) + continue + } log.Infof("Intializing plugin: %s", p.Name()) err := p.Init(router) if err != nil {