diff --git a/auth/auth.go b/auth/auth.go index 013af4c..4ebbb95 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,7 +1,6 @@ package auth import ( - "context" "encoding/json" "fmt" "log" @@ -9,27 +8,20 @@ import ( "strings" "time" + "git.acooldomain.co/server-manager/backend-kubernetes-go/db_handler/mongo" "git.acooldomain.co/server-manager/backend-kubernetes-go/dbhandler" "git.acooldomain.co/server-manager/backend-kubernetes-go/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "golang.org/x/crypto/bcrypt" ) -var secret []byte -var method string -var DOMAIN string - type Connection struct { - usersDbHandler dbhandler.UsersDBHandler - authorizationDbHandler dbhandler.AuthorizationDbHandler + config models.GlobalConfig - authMode models.AuthMode - userAuthDbHandler *dbhandler.UserPassAuthanticationDbHandler - OidcAuthDbHandler *dbhandler.OidcAuthenticationDbHandler + tokenHandler dbhandler.InviteTokenDbHandler + userAuthDbHandler dbhandler.UserPassAuthanticationDbHandler + serverAuthDbHandler dbhandler.ServersAuthorizationDbHandler + OidcAuthDbHandler dbhandler.OidcAuthenticationDbHandler } type Claims struct { @@ -43,14 +35,8 @@ type AuthClaims struct { Claims } -type InviteToken struct { - Email string `bson:"Email"` - Permissions models.Permission `bson:"Permissions"` - Token string `bson:"Token"` -} - -func signToken(token Claims) (string, error) { - t := jwt.New(jwt.GetSigningMethod(method)) +func (con *Connection) signToken(token Claims) (string, error) { + t := jwt.New(jwt.GetSigningMethod(con.config.Signing.Algorithm)) t.Claims = &AuthClaims{ &jwt.StandardClaims{ @@ -59,98 +45,99 @@ func signToken(token Claims) (string, error) { token, } - return t.SignedString(secret) + return t.SignedString(con.config.Signing.Key) } -func AuthorizedTo(requiredPermissions models.Permission, overwriters ...func(*gin.Context) bool) gin.HandlerFunc { +func AuthorizedTo(requiredPermissions models.Permission) gin.HandlerFunc { return func(ctx *gin.Context) { - authCookie, err := ctx.Request.Cookie("auth") - if err != nil { - ctx.AbortWithError(403, err) + claimsPointer, exists := ctx.Get("claims") + if !exists { + log.Printf("LoggedIn was not called first") + ctx.AbortWithError(500, fmt.Errorf("Misconfigured method")) return } - token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - - // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") - return secret, nil - }) - if err != nil { - ctx.AbortWithError(403, err) - return - } - if claims, ok := token.Claims.(*AuthClaims); ok && token.Valid { - ctx.Set("claims", claims) - if (requiredPermissions&claims.Permissions != requiredPermissions) && (models.Admin&claims.Permissions != models.Admin) { - for _, overwrite := range overwriters { - if overwrite(ctx) { - return - } - } - ctx.AbortWithStatusJSON(403, "matching permissions were not found") - return - } - } else { + claims, ok := claimsPointer.(*AuthClaims) + if !ok { ctx.AbortWithStatus(500) + return + } + + if (requiredPermissions&claims.Permissions != requiredPermissions) && (models.Admin&claims.Permissions != models.Admin) { + ctx.AbortWithStatusJSON(403, "matching permissions were not found") + return } } } +func (con *Connection) LoggedIn(ctx *gin.Context) { + authCookie, err := ctx.Request.Cookie("auth") + if err != nil { + ctx.AbortWithError(403, err) + return + } + + token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if token.Method.Alg() != con.config.Signing.Algorithm { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") + return con.config.Signing.Key, nil + }) + if err != nil { + ctx.AbortWithError(403, err) + return + } + + if !token.Valid { + ctx.AbortWithStatus(403) + return + } + claims, ok := token.Claims.(*AuthClaims) + if !ok { + ctx.AbortWithStatus(500) + return + } + + ctx.Set("claims", claims) +} + type SignUpRequest struct { Token string Username string Password string } -func (con Connection) signUp(c *gin.Context) { +func (con Connection) signUp(ctx *gin.Context) { var request SignUpRequest - err := json.NewDecoder(c.Request.Body).Decode(&request) + err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { - c.AbortWithError(500, err) - } - - var token InviteToken - - err = con.DatabaseConnection.Database("Backend").Collection("Tokens").FindOne( - context.TODO(), - bson.D{{}}, - options.FindOne(), - ).Decode(&token) - - if err != nil { - c.AbortWithError(500, err) + ctx.AbortWithError(500, err) return } + + token, err := con.tokenHandler.GetInviteToken(ctx, request.Token) + if err != nil { + ctx.AbortWithError(500, err) + return + } + if token.Token == "" { - c.AbortWithStatusJSON(403, "PermissionDenied") + ctx.AbortWithStatusJSON(403, "PermissionDenied") return } - hashedPass, err := hashPassword(request.Password) - if err != nil { - c.AbortWithError(500, err) - return - } - - _, err = con.DatabaseConnection.Database("Backend").Collection("Users").InsertOne(context.TODO(), &models.User{ - Username: request.Username, - HashedPass: hashedPass, - Permissions: token.Permissions, - MaxOwnedServers: 5, - Email: token.Email, - }, &options.InsertOneOptions{}) + err = con.userAuthDbHandler.CreateUser(ctx, request.Username, request.Password, token.Permissions, token.Email, con.config.Users.DefaultMaxOwnedServers) if err != nil { - c.AbortWithError(500, err) + ctx.AbortWithError(500, err) return } - con.signIn(c) + con.signIn(ctx) } type SignInRequest struct { @@ -158,24 +145,18 @@ type SignInRequest struct { Password string } -func (con Connection) signIn(c *gin.Context) { +func (con Connection) signIn(ctx *gin.Context) { var request SignInRequest - err := json.NewDecoder(c.Request.Body).Decode(&request) + err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { - c.AbortWithError(500, err) + ctx.AbortWithError(500, err) return } - var userItem models.User - err = con.DatabaseConnection.Database("Backend").Collection("Users").FindOne(context.TODO(), bson.D{{Key: "Username", Value: request.Username}}).Decode(&userItem) + userItem, err := con.userAuthDbHandler.AuthenticateUser(ctx, request.Username, request.Password) if err != nil { - c.AbortWithError(403, err) - return - } - - if bcrypt.CompareHashAndPassword([]byte(userItem.HashedPass), []byte(request.Password)) != nil { - c.AbortWithStatus(403) + ctx.AbortWithError(403, err) return } @@ -184,76 +165,89 @@ func (con Connection) signIn(c *gin.Context) { Permissions: userItem.Permissions, } - signedToken, err := signToken(token) + signedToken, err := con.signToken(token) if err != nil { - c.AbortWithError(500, err) + ctx.AbortWithError(500, err) return } - c.SetCookie("auth", signedToken, int(time.Hour)*24*30, "", ".games.acooldomain.co", true, false) - c.IndentedJSON(http.StatusOK, signedToken) + ctx.SetCookie("auth", signedToken, int(time.Hour)*24*30, "", "."+con.config.Domain, true, false) + ctx.IndentedJSON(http.StatusOK, signedToken) } -func (con Connection) verify(c *gin.Context) { - authCookie, err := c.Request.Cookie("auth") - if err != nil { - c.Redirect(303, fmt.Sprintf("http://%s/", DOMAIN)) +func (con Connection) Verify(ctx *gin.Context) { + claimsPointer, exists := ctx.Get("claims") + if !exists { + ctx.Status(403) return } - token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } + claims := claimsPointer.(*AuthClaims) - // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") - return secret, nil - }) - - if err != nil { - c.AbortWithError(403, err) - return - } - claims := token.Claims - - forwarded_host := c.Request.Header.Get("x-forwarded-host") + forwarded_host := ctx.Request.Header.Get("x-forwarded-host") log.Printf("Checking auth of %s", forwarded_host) domainSegments := strings.Split(forwarded_host, ".") serverId, service := domainSegments[0], domainSegments[1] - c.AddParam("server_id", serverId) - if service == "browsers" { - if claims.(*AuthClaims).Permissions&models.Browse == models.Browse || claims.(*AuthClaims).Permissions&models.Admin == models.Admin || con.ServerAuthorized(models.Browse)(c) { - c.Header("X-Username", claims.(*AuthClaims).Username) - log.Printf("Set header X-Username %s", claims.(*AuthClaims).Username) - c.Status(200) + switch service { + case "browsers": + serverPermissions, err := con.serverAuthDbHandler.GetPermissions(ctx, claims.Username, serverId) + if err != nil { + ctx.AbortWithError(500, err) return } - - } - - if service == "cloud" { - if claims.(*AuthClaims).Permissions&models.Cloud == models.Cloud || claims.(*AuthClaims).Permissions&models.Admin == models.Admin { - log.Printf("Set header X-Username %s", claims.(*AuthClaims).Username) - c.Header("X-Username", claims.(*AuthClaims).Username) - c.Status(200) + if (claims.Permissions|serverPermissions)&models.Admin == models.Admin { + ctx.Header("X-Username", claims.Username) + log.Printf("Set header X-Username %s", claims.Username) + ctx.Status(200) + return + } + case "cloud": + if claims.Permissions&models.Cloud == models.Cloud || claims.Permissions&models.Admin == models.Admin { + log.Printf("Set header X-Username %s", claims.Username) + ctx.Header("X-Username", claims.Username) + ctx.Status(200) return } } - c.Redirect(303, fmt.Sprintf("http://%s/login", DOMAIN)) + ctx.Redirect(303, fmt.Sprintf("http://%s/login", con.config.Domain)) } func LoadGroup(group *gin.RouterGroup, client *mongo.Client, config models.GlobalConfig) { - connection := Connection{DatabaseConnection: client} + var userAuthHandler dbhandler.UserPassAuthanticationDbHandler + var inviteHandler dbhandler.InviteTokenDbHandler + var serverAuthHandler dbhandler.ServersAuthorizationDbHandler - secret = []byte(config.Key) - method = config.Algorithm - DOMAIN = config.Domain + var err error + + if config.Authentication.UserPass.Type == models.MONGO { + userAuthHandler, err = mongo.NewUserPassAuthHandler(*config.Authentication.UserPass.Mongo) + if err != nil { + panic(err) + } + } + + if config.Authentication.UserPass.InviteTokenDatabase.Type == models.MONGO { + inviteHandler, err = mongo.NewInviteTokenDbHandler(*config.Authentication.UserPass.Mongo) + if err != nil { + panic(err) + } + } + + mailClient = *mail.NewMailClient(config.Email) + + connection := Connection{ + userPassAuthHandler: userAuthHandler, + tokenHandler: inviteHandler, + mailClient: mailClient, + config: &config, + } + + connection := Connection{DatabaseConnection: client} group.POST("/signin", connection.signIn) group.POST("/signup", AuthorizedTo(models.Admin), connection.signUp) - group.Any("/verify", connection.verify) + group.Any("/verify", connection.Verify) } diff --git a/db_handler/mongo/servers_authorization.go b/db_handler/mongo/servers_authorization.go index d551151..1865db9 100644 --- a/db_handler/mongo/servers_authorization.go +++ b/db_handler/mongo/servers_authorization.go @@ -18,12 +18,12 @@ type ServerPermissions struct { Permissions models.Permission `bson:"permissions"` } -type AuthorizationDbHandler struct { +type ServersAuthorizationDbHandler struct { dbhandler.ServersAuthorizationDbHandler collection *mongo.Collection } -func (self *AuthorizationDbHandler) RemoveUser(ctx context.Context, username string) error { +func (self *ServersAuthorizationDbHandler) RemoveUser(ctx context.Context, username string) error { _, err := self.collection.DeleteMany( ctx, bson.M{ @@ -34,7 +34,7 @@ func (self *AuthorizationDbHandler) RemoveUser(ctx context.Context, username str return err } -func (self *AuthorizationDbHandler) RemoveServer(ctx context.Context, serverId string) error { +func (self *ServersAuthorizationDbHandler) RemoveServer(ctx context.Context, serverId string) error { _, err := self.collection.DeleteMany( ctx, bson.M{ @@ -45,7 +45,7 @@ func (self *AuthorizationDbHandler) RemoveServer(ctx context.Context, serverId s return err } -func (self *AuthorizationDbHandler) AddPermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { +func (self *ServersAuthorizationDbHandler) AddPermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { var serverPermissions ServerPermissions err := self.collection.FindOne( ctx, @@ -76,7 +76,7 @@ func (self *AuthorizationDbHandler) AddPermissions(ctx context.Context, username return err } -func (self *AuthorizationDbHandler) RemovePermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { +func (self *ServersAuthorizationDbHandler) RemovePermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { var serverPermissions ServerPermissions err := self.collection.FindOne( ctx, @@ -107,7 +107,7 @@ func (self *AuthorizationDbHandler) RemovePermissions(ctx context.Context, usern return err } -func (self *AuthorizationDbHandler) SetPermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { +func (self *ServersAuthorizationDbHandler) SetPermissions(ctx context.Context, username string, serverId string, permissions models.Permission) error { _, err := self.collection.UpdateOne( ctx, bson.M{ @@ -123,7 +123,7 @@ func (self *AuthorizationDbHandler) SetPermissions(ctx context.Context, username return err } -func (self *AuthorizationDbHandler) GetPermissions(ctx context.Context, username string, serverId string) (models.Permission, error) { +func (self *ServersAuthorizationDbHandler) GetPermissions(ctx context.Context, username string, serverId string) (models.Permission, error) { var serverPermissions ServerPermissions err := self.collection.FindOne( ctx, @@ -140,7 +140,7 @@ func (self *AuthorizationDbHandler) GetPermissions(ctx context.Context, username return serverPermissions.Permissions, nil } -func NewAuthorizationHandler(config models.MongoDBConfig) (*AuthorizationDbHandler, error) { +func NewServersAuthorizationHandler(config models.MongoDBConfig) (*ServersAuthorizationDbHandler, error) { clientOptions := options.Client().ApplyURI(config.Url).SetAuth(options.Credential{ Username: config.Username, Password: config.Password, @@ -155,7 +155,7 @@ func NewAuthorizationHandler(config models.MongoDBConfig) (*AuthorizationDbHandl return nil, err } - return &AuthorizationDbHandler{ + return &ServersAuthorizationDbHandler{ collection: client.Database(config.Database).Collection(config.Collection), }, nil } diff --git a/db_handler/mongo/user_pass_authentication.go b/db_handler/mongo/user_pass_authentication.go index 63bfef7..f31c1a2 100644 --- a/db_handler/mongo/user_pass_authentication.go +++ b/db_handler/mongo/user_pass_authentication.go @@ -41,34 +41,42 @@ func (self *UserPassAuthenticationDbHandler) ListUsers(ctx context.Context) ([]m modelUsers := make([]models.User, len(authUsers)) for i, authUser := range authUsers { modelUsers[i] = models.User{ - Username: authUser.Username, - Nickname: authUser.Nickname, - Email: authUser.Email, + Username: authUser.Username, + Nickname: authUser.Nickname, + Email: authUser.Email, + MaxOwnedServers: authUser.MaxOwnedSevers, + Permissions: authUser.Permissions, } } return modelUsers, nil } -func (self *UserPassAuthenticationDbHandler) AuthenticateUser(ctx context.Context, username string, password string) (models.Permission, error) { +func (self *UserPassAuthenticationDbHandler) AuthenticateUser(ctx context.Context, username string, password string) (*models.User, error) { var user AuthUser err := self.collection.FindOne(ctx, bson.M{"username": username}).Decode(&user) if err != nil { - return 0, err + return nil, err } hashedPassword, err := dbhandler.HashPassword(password) if err != nil { - return 0, err + return nil, err } if user.HashedPassword != hashedPassword { - return 0, fmt.Errorf("Incorrect Password") + return nil, fmt.Errorf("Incorrect Password") } - return user.Permissions, nil + return &models.User{ + Username: user.Username, + Nickname: user.Nickname, + Email: user.Email, + MaxOwnedServers: user.MaxOwnedSevers, + Permissions: user.Permissions, + }, nil } func (self *UserPassAuthenticationDbHandler) CreateUser( diff --git a/db_handler/user_pass_auth.go b/db_handler/user_pass_auth.go index 570c677..d1fcb13 100644 --- a/db_handler/user_pass_auth.go +++ b/db_handler/user_pass_auth.go @@ -26,7 +26,7 @@ type UserSignupRequest struct { type UserPassAuthanticationDbHandler interface { // Read Only - AuthenticateUser(ctx context.Context, username string, password string) (models.Permission, error) + AuthenticateUser(ctx context.Context, username string, password string) (*models.User, error) ListUsers(ctx context.Context) ([]models.User, error) // Write diff --git a/go.work b/go.work index eb97c15..25bea09 100644 --- a/go.work +++ b/go.work @@ -12,4 +12,5 @@ use ( ./instance_manager/kubernetes ./servers ./users + ./utils ) diff --git a/models/config.go b/models/config.go index 33de1b3..587c9e7 100644 --- a/models/config.go +++ b/models/config.go @@ -76,6 +76,10 @@ type ServersAuthorizationDatabaseConfig struct { Mongo *MongoDBConfig `yaml:"mongo"` } +type UsersConfig struct { + DefaultMaxOwnedServers uint `yaml:"default_max_owned_servers"` +} + type GlobalConfig struct { // Features Configs Email EmailConfig `yaml:"email"` @@ -83,6 +87,7 @@ type GlobalConfig struct { Signing SigningConfig `yaml:"signing"` Authentication AuthenticationConfig `yaml:"authentication"` InstanceManager InstanceManagerConfig `yaml:"instance_manager"` + Users UsersConfig `yaml:"users"` // Database Configs ServersDatabase ServersDatabaseConfig `yaml:"servers_database"` diff --git a/models/user.go b/models/user.go index f8d2ba2..97136a6 100644 --- a/models/user.go +++ b/models/user.go @@ -1,8 +1,9 @@ package models type User struct { - Username string `json:"username"` - Nickname string `json:"nickname"` - Email string `json:"email"` - MaxOwnedServers int `json:"maxed_owned_servers"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Email string `json:"email"` + MaxOwnedServers uint `json:"maxed_owned_servers"` + Permissions Permission `json:"permissions"` } diff --git a/utils/constructors.go b/utils/constructors.go new file mode 100644 index 0000000..69d2514 --- /dev/null +++ b/utils/constructors.go @@ -0,0 +1,41 @@ +package dbhandler + +import ( + "fmt" + "sync" + + "git.acooldomain.co/server-manager/backend-kubernetes-go/db_handler/mongo" + "git.acooldomain.co/server-manager/backend-kubernetes-go/dbhandler" + "git.acooldomain.co/server-manager/backend-kubernetes-go/models" +) + +var ( + mu sync.Mutex + existingServerAuthHandlers = make(map[string]dbhandler.ServersAuthorizationDbHandler) +) + +func configToKey(config any) (string, error) { + switch c := config.(type) { + case models.MongoDBConfig: + return fmt.Sprintf("mongo:%s:%s:%s:%s:%s", c.Url, c.Database, c.Collection, c.Username, c.Password), nil + default: + return "", fmt.Errorf("unknown config format") + } +} + +func GetServersAuthDbHandler(config any) (dbhandler.ServersAuthorizationDbHandler, error) { + mu.Lock() + defer mu.Unlock() + key, err := configToKey(config) + if handler, exists := existingServerAuthHandlers[key]; err != nil && exists { + return handler, nil + } + + switch c := config.(type) { + case models.MongoDBConfig: + handler, err := mongo.NewServersAuthorizationHandler(c) + return handler, err + } + + return nil, fmt.Errorf("Unknown config format") +} diff --git a/utils/go.mod b/utils/go.mod new file mode 100644 index 0000000..6d8d73d --- /dev/null +++ b/utils/go.mod @@ -0,0 +1,3 @@ +module git.acooldomain.co/server-manager/backend-kubernetes-go/utils + +go 1.22.0