diff --git a/auth/auth.go b/auth/auth.go index f47f93a..8ea2ab0 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -48,28 +48,6 @@ func (con *AuthApi) signToken(token Claims) (string, error) { return t.SignedString([]byte(con.config.Signing.Key)) } -func AuthorizedTo(requiredPermissions models.Permission) gin.HandlerFunc { - return func(ctx *gin.Context) { - claimsPointer, exists := ctx.Get("claims") - if !exists { - log.Printf("LoggedIn was not called first") - ctx.AbortWithError(500, fmt.Errorf("Misconfigured method")) - return - } - - 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 *AuthApi) LoggedIn(ctx *gin.Context) { authCookie, err := ctx.Request.Cookie("auth") if err != nil { @@ -77,7 +55,7 @@ func (con *AuthApi) LoggedIn(ctx *gin.Context) { return } - token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (any, 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"]) @@ -216,6 +194,7 @@ func (con AuthApi) Verify(ctx *gin.Context) { ctx.Redirect(303, fmt.Sprintf("http://%s/login", con.config.Domain)) } + func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) gin.HandlerFunc { userAuthHandler, err := factories.GetUserPassAuthDbHandler(config.Authentication.UserPass) if err != nil { @@ -234,7 +213,7 @@ func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) gin.HandlerFu } group.POST("/signin", connection.signIn) - group.POST("/signup", connection.LoggedIn, AuthorizedTo(models.Admin), connection.signUp) + group.POST("/signup", connection.signUp) group.Any("/verify", connection.Verify) return connection.LoggedIn diff --git a/auth/utils.go b/auth/utils.go new file mode 100644 index 0000000..fad298d --- /dev/null +++ b/auth/utils.go @@ -0,0 +1,46 @@ +package auth + +import ( + "fmt" + + "git.acooldomain.co/server-manager/backend/models" + "github.com/gin-gonic/gin" +) + +const AuthorizedParam string = "authorized" + +func AuthorizedTo(requiredPermissions models.Permission) gin.HandlerFunc { + return func(ctx *gin.Context) { + claimsPointer, exists := ctx.Get("claims") + if !exists { + ctx.AbortWithError(500, fmt.Errorf("Did not call LoggedIn first")) + return + } + + claims, ok := claimsPointer.(*AuthClaims) + if !ok { + return + } + + if (requiredPermissions&claims.Permissions != requiredPermissions) && (models.Admin&claims.Permissions != models.Admin) { + return + } + + ctx.Set(AuthorizedParam, true) + } +} + +func AuthorizationEnforcer() gin.HandlerFunc { + return func(ctx *gin.Context) { + authorized, exists := ctx.Get(AuthorizedParam) + if !exists { + ctx.AbortWithStatus(403) + return + } + + if !authorized.(bool) { + ctx.AbortWithStatus(403) + } + + } +} diff --git a/dbhandler/mongo/servers.go b/dbhandler/mongo/servers.go index 0ea7ca1..80c960b 100644 --- a/dbhandler/mongo/servers.go +++ b/dbhandler/mongo/servers.go @@ -106,7 +106,7 @@ func (self *ServersDbHandler) ListServers(ctx context.Context) ([]dbhandler.Serv func (self *ServersDbHandler) GetServer(ctx context.Context, serverId string) (*dbhandler.Server, error) { var server Server - err := self.collection.FindOne(ctx, bson.M{"server_id": serverId}).Decode(&server) + err := self.collection.FindOne(ctx, bson.M{"id": serverId}).Decode(&server) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (self *ServersDbHandler) CreateServer(ctx context.Context, server dbhandler func (self *ServersDbHandler) DeleteServer(ctx context.Context, serverId string) error { _, err := self.collection.DeleteOne(ctx, bson.M{ - "server_id": serverId, + "id": serverId, }) return err @@ -163,7 +163,7 @@ func (self *ServersDbHandler) UpdateServer(ctx context.Context, serverId string, updateServerRequest["command"] = updateParams.Command } - _, err := self.collection.UpdateOne(ctx, bson.M{"server_id": serverId}, bson.M{"$set": updateServerRequest}) + _, err := self.collection.UpdateOne(ctx, bson.M{"id": serverId}, bson.M{"$set": updateServerRequest}) return err } diff --git a/dbhandler/mongo/user_pass_authentication.go b/dbhandler/mongo/user_pass_authentication.go index 078a321..c8edade 100644 --- a/dbhandler/mongo/user_pass_authentication.go +++ b/dbhandler/mongo/user_pass_authentication.go @@ -98,6 +98,14 @@ func (self *UserPassAuthenticationDbHandler) CreateUser( return err } +func (self *UserPassAuthenticationDbHandler) CountUsers(ctx context.Context) (uint, error) { + count, err := self.collection.CountDocuments(ctx, bson.M{}) + if err != nil { + return 0, err + } + return uint(count), nil +} + func (self *UserPassAuthenticationDbHandler) RemoveUser(ctx context.Context, username string) error { _, err := self.collection.DeleteOne( ctx, diff --git a/dbhandler/user_pass_auth.go b/dbhandler/user_pass_auth.go index 4cac147..74edd7e 100644 --- a/dbhandler/user_pass_auth.go +++ b/dbhandler/user_pass_auth.go @@ -28,6 +28,7 @@ type UserPassAuthanticationDbHandler interface { // Read Only AuthenticateUser(ctx context.Context, username string, password string) (*models.User, error) ListUsers(ctx context.Context) ([]models.User, error) + CountUsers(ctx context.Context) (uint, error) // Write CreateUser(ctx context.Context, username string, password string, permissions models.Permission, email string, maxOwnedServers uint) error diff --git a/factories/dbhandlers.go b/factories/dbhandlers.go index b5ef46c..c8e4c66 100644 --- a/factories/dbhandlers.go +++ b/factories/dbhandlers.go @@ -1,11 +1,14 @@ package factories import ( + "context" "errors" + "log" "sync" + "time" - "git.acooldomain.co/server-manager/backend/dbhandler/mongo" "git.acooldomain.co/server-manager/backend/dbhandler" + "git.acooldomain.co/server-manager/backend/dbhandler/mongo" "git.acooldomain.co/server-manager/backend/models" ) @@ -133,6 +136,21 @@ func GetUserPassAuthDbHandler(config models.UserPassAuthConfig) (dbhandler.UserP } userPassAuthDbHandlers[key] = handler + + ctx, cancel := context.WithTimeoutCause(context.Background(), 5*time.Second, errors.New("Timeout")) + defer cancel() + if config.InitialUser == nil { + return handler, nil + } + + count, _ := handler.CountUsers(ctx) + if count == 0 { + log.Printf("Trying to create user %#v\n", config.InitialUser) + err := handler.CreateUser(ctx, config.InitialUser.Username, config.InitialUser.Password, models.Admin, config.InitialUser.Email, 10) + if err != nil { + log.Printf("Failed to create initial user %e\n", err) + } + } return handler, nil } diff --git a/instancemanager/docker/instance_manager.go b/instancemanager/docker/instance_manager.go index b6f5be7..e1d4431 100644 --- a/instancemanager/docker/instance_manager.go +++ b/instancemanager/docker/instance_manager.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" + "maps" "net" instancemanager "git.acooldomain.co/server-manager/backend/instancemanager" "git.acooldomain.co/server-manager/backend/models" "github.com/buildkite/shellwords" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" @@ -22,11 +22,11 @@ import ( type InstanceManager struct { instancemanager.InstanceManager - client client.Client + client *client.Client config models.DockerInstanceManagerConfig } -func (self *InstanceManager) containerList(ctx context.Context, labels ContainerLabels, all bool) ([]types.Container, error) { +func (self *InstanceManager) containerList(ctx context.Context, labels ContainerLabels, all bool) ([]container.Summary, error) { filters, err := convertLabelsToFilter(labels) if err != nil { return nil, err @@ -61,12 +61,17 @@ func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*v // General // Read Only func (self *InstanceManager) GetImage(ctx context.Context, imageId string) (*instancemanager.Image, error) { - imageInspect, _, err := self.client.ImageInspectWithRaw(ctx, imageId) + imageInspect, err := self.client.ImageInspect(ctx, imageId) if err != nil { return nil, err } - if imageInspect.Config.Labels["type"] != "game" { + imageLabels, err := convertImageLabelsToStruct(imageInspect.Config.Labels) + if err != nil { + return nil, err + } + + if imageLabels.Type != Game { return nil, fmt.Errorf("Image not found") } @@ -89,7 +94,7 @@ func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager. images := make([]instancemanager.Image, len(rawImages)) for i, rawImage := range rawImages { - imageInspect, _, err := self.client.ImageInspectWithRaw(ctx, rawImage.ID) + imageInspect, err := self.client.ImageInspect(ctx, rawImage.ID) if err != nil { return nil, err } @@ -502,9 +507,7 @@ func (self *InstanceManager) StartFileBrowser(ctx context.Context, serverId stri return nil, err } - for key, value := range *containerConfig { - browserLabels[key] = value - } + maps.Copy(browserLabels, *containerConfig) command := self.config.FileBrowser.Command @@ -579,6 +582,6 @@ func NewInstanceManager(config models.DockerInstanceManagerConfig) (*InstanceMan return &InstanceManager{ config: config, - client: *apiClient, + client: apiClient, }, nil } diff --git a/instancemanager/docker/utils.go b/instancemanager/docker/utils.go index 2fbd403..c532172 100644 --- a/instancemanager/docker/utils.go +++ b/instancemanager/docker/utils.go @@ -8,8 +8,9 @@ import ( instancemanager "git.acooldomain.co/server-manager/backend/instancemanager" "git.acooldomain.co/server-manager/backend/models" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" "github.com/docker/go-connections/nat" ) @@ -54,15 +55,15 @@ func stringifyMap(m map[string]any) map[string]string { return stringifiedMap } -func convertContainerPortsToPorts(ports []types.Port) []models.Port { +func convertContainerPortsToPorts(ports []container.Port) []models.Port { containerPorts := make([]models.Port, len(ports)) logger := log.Default() for i, port := range ports { var portProtocol models.PortProtocol switch port.Type { - case "TCP": + case "tcp": portProtocol = models.TCP - case "UDP": + case "udp": portProtocol = models.UDP default: logger.Println(fmt.Sprintf("Unkown Port Protocol %s assuming TCP", port.Type)) @@ -90,10 +91,11 @@ func convertImageStringToModelsImage(image string) models.Image { } } -func convertImageInspectToInstanceImage(image types.ImageInspect) instancemanager.Image { +func convertImageInspectToInstanceImage(image image.InspectResponse) instancemanager.Image { modelsImage := convertImageStringToModelsImage(image.RepoTags[0]) ports := convertImagePortsToPorts(image.Config.ExposedPorts) + fmt.Printf("image: %#v\nconfig: %#v\nports: %#v\n", image, image.Config, ports) return instancemanager.Image{ Registry: modelsImage.Registry, @@ -112,7 +114,7 @@ func convertContainerLabelsToStruct(labels map[string]string) (*ContainerLabels, return nil, err } - err = json.Unmarshal(rawLabels, &labels) + err = json.Unmarshal(rawLabels, &containerLabels) if err != nil { return nil, err @@ -129,7 +131,7 @@ func convertVolumeLabelsToStruct(labels map[string]string) (*VolumeLabels, error return nil, err } - err = json.Unmarshal(rawLabels, &labels) + err = json.Unmarshal(rawLabels, &volumeLabels) if err != nil { return nil, err @@ -146,7 +148,7 @@ func convertImageLabelsToStruct(labels map[string]string) (*ImageLabels, error) return nil, err } - err = json.Unmarshal(rawLabels, &labels) + err = json.Unmarshal(rawLabels, &imageLabels) if err != nil { return nil, err @@ -161,9 +163,9 @@ func convertImagePortsToPorts(rawPorts nat.PortSet) []instancemanager.Port { portNumber := imagePort.Int() var protocol models.PortProtocol switch imagePort.Proto() { - case "TCP": + case "tcp": protocol = models.TCP - case "UDP": + case "udp": protocol = models.UDP default: log.Default().Println(fmt.Sprintf("Unknown port protocol %s using TCP", imagePort.Proto())) diff --git a/models/config.go b/models/config.go index ed33f9b..f6cb9b1 100644 --- a/models/config.go +++ b/models/config.go @@ -1,5 +1,11 @@ package models +type InitialUserConfig struct { + Email string `yaml:"email"` + Username string `yaml:"username"` + Password string `yaml:"password"` +} + type EmailConfig struct { FromEmail string `yaml:"from_email"` Username string `yaml:"username"` @@ -35,6 +41,7 @@ type UserPassAuthConfig struct { Type DatabaseType `yaml:"type"` Mongo *MongoDBConfig `yaml:"mongo"` InviteTokenDatabase InviteTokenDatabaseConfig `yaml:"invite_token_database"` + InitialUser *InitialUserConfig `yaml:"initial_user"` } type AuthenticationConfig struct { diff --git a/servers/auth_utils.go b/servers/auth_utils.go new file mode 100644 index 0000000..9095907 --- /dev/null +++ b/servers/auth_utils.go @@ -0,0 +1,36 @@ +package servers + +import ( + "git.acooldomain.co/server-manager/backend/auth" + "git.acooldomain.co/server-manager/backend/models" + "github.com/gin-gonic/gin" +) + +func (con ServersApi) ServerAuthorized(permissions models.Permission) func(*gin.Context) { + return func(ctx *gin.Context) { + claimsPointer, exists := ctx.Get("claims") + if !exists { + ctx.AbortWithStatus(403) + return + } + + claims := claimsPointer.(*auth.AuthClaims) + + serverId := ctx.Param("server_id") + if serverId == "" { + return + } + + userPermissions, err := con.ServerAuthorization.GetPermissions(ctx, claims.Username, serverId) + if err != nil { + return + } + + if userPermissions&permissions == permissions || userPermissions&models.Admin == models.Admin { + ctx.Set(auth.AuthorizedParam, true) + return + } + + return + } +} diff --git a/servers/browsers.go b/servers/browsers.go index 3c4fd5e..7c10e05 100644 --- a/servers/browsers.go +++ b/servers/browsers.go @@ -51,6 +51,6 @@ func LoadBrowsersGroup(group *gin.RouterGroup, config models.GlobalConfig) { InstanceManager: instanceManager, } - group.GET("", auth.AuthorizedTo(0), connection.GetBrowsers) - group.POST("/:server_id/stop", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Browse), connection.StopBrowser) + group.GET("", auth.AuthorizedTo(0), auth.AuthorizationEnforcer(), connection.GetBrowsers) + group.POST("/:server_id/stop", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Browse), auth.AuthorizationEnforcer(), connection.StopBrowser) } diff --git a/servers/images.go b/servers/images.go index 05acbbb..e195440 100644 --- a/servers/images.go +++ b/servers/images.go @@ -70,5 +70,5 @@ func LoadeImagesGroup(group *gin.RouterGroup, config models.GlobalConfig) { InstanceManager: instanceManager, } - group.GET("", auth.AuthorizedTo(0), connection.GetImages) + group.GET("", auth.AuthorizedTo(0), auth.AuthorizationEnforcer(), connection.GetImages) } diff --git a/servers/servers.go b/servers/servers.go index 01eaf11..dd6c73c 100644 --- a/servers/servers.go +++ b/servers/servers.go @@ -64,37 +64,6 @@ type CreateServerRequest struct { Nickname string `json:"Nickname"` } -func (con ServersApi) ServerAuthorized(permissions models.Permission) func(*gin.Context) { - return func(ctx *gin.Context) { - claimsPointer, exists := ctx.Get("claims") - if !exists { - ctx.AbortWithStatus(403) - return - } - - claims := claimsPointer.(*auth.AuthClaims) - - serverId := ctx.Param("server_id") - if serverId == "" { - ctx.AbortWithStatus(403) - return - } - - userPermissions, err := con.ServerAuthorization.GetPermissions(ctx, claims.Username, serverId) - if err != nil { - ctx.AbortWithError(500, err) - return - } - - if userPermissions&permissions == permissions || userPermissions&models.Admin == models.Admin { - return - } - - ctx.AbortWithStatus(403) - return - } -} - func (con ServersApi) CreateServer(ctx *gin.Context) { claims, exists := ctx.Get("claims") if !exists { @@ -168,6 +137,11 @@ func (con ServersApi) StartServer(ctx *gin.Context) { } instanceServer, err := con.InstanceManager.GetServer(ctx, serverId) + if err != nil { + ctx.AbortWithError(500, err) + return + } + if instanceServer.Running { ctx.Status(200) return @@ -176,6 +150,7 @@ func (con ServersApi) StartServer(ctx *gin.Context) { server, err := con.ServersDbHandler.GetServer(ctx, serverId) if err != nil { ctx.AbortWithError(500, err) + return } err = con.InstanceManager.StartServer( @@ -558,15 +533,15 @@ func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) { InstanceManager: instanceManager, } - group.POST("/:server_id/start", auth.AuthorizedTo(models.Start), connection.ServerAuthorized(models.Start), connection.StartServer) - group.POST("", auth.AuthorizedTo(models.Create), connection.CreateServer) - group.GET("", auth.AuthorizedTo(0), connection.GetServers) - group.POST("/:server_id/stop", auth.AuthorizedTo(models.Stop), connection.ServerAuthorized(models.Stop), connection.StopServer) - group.DELETE("/:server_id", auth.AuthorizedTo(models.Delete), connection.ServerAuthorized(models.Delete), connection.DeleteServer) - group.POST("/:server_id/run_command", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), connection.RunCommand) - group.GET("/:server_id/attach", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), connection.AttachServer) - group.PATCH("/:server_id", auth.AuthorizedTo(models.Admin), connection.ServerAuthorized(models.Admin), connection.UpdateServer) - group.POST("/:server_id/browse", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.BrowseServer) - group.GET("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.GetServerUserPermissions) - group.POST("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.SetServerUserPermissions) + group.POST("/:server_id/start", auth.AuthorizedTo(models.Start), connection.ServerAuthorized(models.Start), auth.AuthorizationEnforcer(), connection.StartServer) + group.POST("", auth.AuthorizedTo(models.Create), auth.AuthorizationEnforcer(), connection.CreateServer) + group.GET("", connection.GetServers) + group.POST("/:server_id/stop", auth.AuthorizedTo(models.Stop), connection.ServerAuthorized(models.Stop), auth.AuthorizationEnforcer(), connection.StopServer) + group.DELETE("/:server_id", auth.AuthorizedTo(models.Delete), connection.ServerAuthorized(models.Delete), auth.AuthorizationEnforcer(), connection.DeleteServer) + group.POST("/:server_id/run_command", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), auth.AuthorizationEnforcer(), connection.RunCommand) + group.GET("/:server_id/attach", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), auth.AuthorizationEnforcer(), connection.AttachServer) + group.PATCH("/:server_id", auth.AuthorizedTo(models.Admin), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.UpdateServer) + group.POST("/:server_id/browse", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.BrowseServer) + group.GET("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.GetServerUserPermissions) + group.POST("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.SetServerUserPermissions) } diff --git a/users/users.go b/users/users.go index 059322f..3454665 100644 --- a/users/users.go +++ b/users/users.go @@ -122,9 +122,9 @@ func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) { config: &config, } - group.GET("", auth.AuthorizedTo(0), connection.GetUsers) - group.GET("/@me", auth.AuthorizedTo(0), connection.GetUser) - group.POST("", auth.AuthorizedTo(models.Admin), connection.InviteUser) - group.DELETE("/:user_id", auth.AuthorizedTo(models.Admin), connection.DeleteUser) - group.PATCH("/:user_id/permissions", auth.AuthorizedTo(models.Admin), connection.SetUserPermissions) + group.GET("", auth.AuthorizedTo(0), auth.AuthorizationEnforcer(), connection.GetUsers) + group.GET("/@me", auth.AuthorizedTo(0), auth.AuthorizationEnforcer(), connection.GetUser) + group.POST("", auth.AuthorizedTo(models.Admin), auth.AuthorizationEnforcer(), connection.InviteUser) + group.DELETE("/:user_id", auth.AuthorizedTo(models.Admin), auth.AuthorizationEnforcer(), connection.DeleteUser) + group.PATCH("/:user_id/permissions", auth.AuthorizedTo(models.Admin), auth.AuthorizationEnforcer(), connection.SetUserPermissions) }