From fd5fe28a0190c67aed2c9272466085a69070a655 Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Fri, 29 Mar 2024 08:51:48 +0100
Subject: [PATCH] server: - added route to retrieve a single VM from its UUID -
 added route to retrieve a single template from its UUID

client:
- added vmlistsingle to test above route (but then commented out)
- added tpllistsingle to test above route (but then commented out)
---
 docs/README_server.md                | 17 +++++++-------
 src/client/cmdTemplate/helper.go     | 17 ++++++++++----
 src/client/cmdVM/helper.go           | 10 ++++----
 src/client/cmdVM/vmCreate.go         |  2 +-
 src/client/cmdVM/vmListSingle.go     |  2 +-
 src/client/nexush/nexush.go          |  3 ++-
 src/server/router/router.go          |  1 +
 src/server/router/routerTemplates.go | 35 ++++++++++++++++++++++++++++
 src/server/router/routerVMs.go       | 15 ++++++------
 9 files changed, 74 insertions(+), 28 deletions(-)

diff --git a/docs/README_server.md b/docs/README_server.md
index 814f0e5..8b4fbc5 100644
--- a/docs/README_server.md
+++ b/docs/README_server.md
@@ -366,14 +366,15 @@ These capabilities are called "VM access capabilities":
 
 ### Template management
 
-| Route                  | Description                | Method | Parameters       | Req. user cap.                     |
-|---                     |---                         |---     |---               |---                                 |
-| `/templates/vm`        | create a template          | POST   | vmID,name,access | `TPL_CREATE`                       |
-| `/templates/qcow`      | create a template          | POST   | qcow,name,access | `TPL_CREATE`                       |
-| `/templates/{id}`      | edit a template            | PUT    | name,access      | `TPL_EDIT_ANY` OR `TPL_EDIT`       |
-| `/templates/{id}`      | delete a template          | DELETE |                  | `TPL_DESTROY_ANY` OR `TPL_DESTROY` |
-| `/templates/{id}/disk` | download a template's disk | GET    |                  | `TPL_READFS_ANY` OR `TPL_READFS`   |
-| `/templates`           | list templates             | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |
+| Route                  | Description                          | Method | Parameters       | Req. user cap.                     |
+|---                     |---                                   |---     |---               |---                                 |
+| `/templates`           | returns templates that can be listed | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |
+| `/templates/{id}`      | returns a template                   | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |
+| `/templates/vm`        | create a template                    | POST   | vmID,name,access | `TPL_CREATE`                       |
+| `/templates/qcow`      | create a template                    | POST   | qcow,name,access | `TPL_CREATE`                       |
+| `/templates/{id}`      | edit a template                      | PUT    | name,access      | `TPL_EDIT_ANY` OR `TPL_EDIT`       |
+| `/templates/{id}`      | delete a template                    | DELETE |                  | `TPL_DESTROY_ANY` OR `TPL_DESTROY` |
+| `/templates/{id}/disk` | download a template's disk           | GET    |                  | `TPL_READFS_ANY` OR `TPL_READFS`   |
 
 Remarks:
 
diff --git a/src/client/cmdTemplate/helper.go b/src/client/cmdTemplate/helper.go
index 6f34648..2fa149b 100644
--- a/src/client/cmdTemplate/helper.go
+++ b/src/client/cmdTemplate/helper.go
@@ -141,7 +141,7 @@ func getFilteredTemplates(route string, patterns []string) ([]t.TemplateSerializ
     templatesList := []t.TemplateSerialized{}
 
     if resp.IsSuccess() {
-        templates, err := getTemplates(resp)
+        templates, err := deserializeTemplates(resp)
         if err != nil {
             return nil, err
         }
@@ -176,11 +176,20 @@ func getFilteredTemplates(route string, patterns []string) ([]t.TemplateSerializ
     }
 }
 
-// Retrieves all templates (no filtering).
-func getTemplates(resp *resty.Response) ([]t.TemplateSerialized, error) {
+// Deserialize a list of templates from an http response (no filtering).
+func deserializeTemplates(resp *resty.Response) ([]t.TemplateSerialized, error) {
     templates := []t.TemplateSerialized{}
     if err := json.Unmarshal(resp.Body(), &templates); err != nil {
         return nil, err
     }
     return templates, nil
-}
\ No newline at end of file
+}
+
+// Deserialize a single template from an http response.
+func deserializeTemplate(resp *resty.Response) (*t.TemplateSerialized, error) {
+    var tpl *t.TemplateSerialized = &t.TemplateSerialized{}
+    if err := json.Unmarshal(resp.Body(), &tpl); err != nil {
+        return tpl, err
+    }
+    return tpl, nil
+}
diff --git a/src/client/cmdVM/helper.go b/src/client/cmdVM/helper.go
index 306245a..779c860 100644
--- a/src/client/cmdVM/helper.go
+++ b/src/client/cmdVM/helper.go
@@ -134,7 +134,7 @@ func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized,
     vmsList := []vm.VMNetworkSerialized{}
 
     if resp.IsSuccess() {
-        vms, err := getVMs(resp)
+        vms, err := deserializeVMs(resp)
         if err != nil {
             return nil, err
         }
@@ -169,8 +169,8 @@ func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized,
     }
 }
 
-// Retrieves all VMs (no filtering).
-func getVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
+// Deserialize a list of VMs from an http response (no filtering).
+func deserializeVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
     vms := []vm.VMNetworkSerialized{}
     if err := json.Unmarshal(resp.Body(), &vms); err != nil {
         return nil, err
@@ -178,8 +178,8 @@ func getVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
     return vms, nil
 }
 
-// Retrieve a single VM.
-func getVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
+// Deserialize a single VM from an http response.
+func deserializeVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
     var vm *vm.VMNetworkSerialized = &vm.VMNetworkSerialized{}
     if err := json.Unmarshal(resp.Body(), vm); err != nil {
         return vm, err
diff --git a/src/client/cmdVM/vmCreate.go b/src/client/cmdVM/vmCreate.go
index a034f94..55c29ba 100644
--- a/src/client/cmdVM/vmCreate.go
+++ b/src/client/cmdVM/vmCreate.go
@@ -138,7 +138,7 @@ func (cmd *Create)Run(args []string) int {
             statusCode = 1
         } else {
             if resp.IsSuccess() {
-                vm, err := getVM(resp)
+                vm, err := deserializeVM(resp)
                 if err != nil {
                     u.PrintlnErr("Failed retrieving server's response: "+err.Error())
                     statusCode = 1
diff --git a/src/client/cmdVM/vmListSingle.go b/src/client/cmdVM/vmListSingle.go
index aa032c1..2c970eb 100644
--- a/src/client/cmdVM/vmListSingle.go
+++ b/src/client/cmdVM/vmListSingle.go
@@ -45,7 +45,7 @@ func (cmd *ListSingle)Run(args []string) int {
         return 1
     } else {
         if resp.IsSuccess() {
-            vm, err := getVM(resp)
+            vm, err := deserializeVM(resp)
             if err != nil {
                 u.PrintlnErr("Failed retrieving server's response: "+err.Error())
                 return 1
diff --git a/src/client/nexush/nexush.go b/src/client/nexush/nexush.go
index 5787b35..a6bb391 100644
--- a/src/client/nexush/nexush.go
+++ b/src/client/nexush/nexush.go
@@ -49,6 +49,7 @@ var cmdList = []cmd.Command {
     &cmdTemplate.Edit{"tpledit"},
     &cmdTemplate.ExportDisk{"tplexportdisk"},
     &cmdTemplate.List{"tpllist"},
+    // &cmdTemplate.ListSingle{"tpllistsingle"},   // for testing the route only
 
     &cmdMisc.HelpHeader{"!═════╡ VM commands ╞═════════════════════════════════════════════════════════════════"},
     &cmdVM.AddAccess{"vmaddaccess"},
@@ -63,7 +64,7 @@ var cmdList = []cmd.Command {
     &cmdVM.ImportDir{"vmimportdir"},
     &cmdVM.Stop{"vmkill"},
     &cmdVM.List{"vmlist"},
-    &cmdVM.ListSingle{"vmlistsingle"},
+    // &cmdVM.ListSingle{"vmlistsingle"},   // for testing the route only
     &cmdVM.Reboot{"vmreboot"},
     &cmdVM.Shutdown{"vmshutdown"},
     &cmdVM.Start{"vmstart"},
diff --git a/src/server/router/router.go b/src/server/router/router.go
index e35b910..103baca 100644
--- a/src/server/router/router.go
+++ b/src/server/router/router.go
@@ -99,6 +99,7 @@ func (router *Router)Start(port int) {
     // Template management.
     templatesGroup := router.echo.Group("/templates")
     templatesGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess()))
+    templatesGroup.GET("/:id", router.tpl.GetTemplate)
     templatesGroup.GET("", router.tpl.GetTemplates)
     templatesGroup.POST("/vm", router.tpl.CreateTemplateFromVM)
     templatesGroup.POST("/qcow", router.tpl.CreateTemplateFromQCOW)
diff --git a/src/server/router/routerTemplates.go b/src/server/router/routerTemplates.go
index 6c7f51f..ebeacd8 100644
--- a/src/server/router/routerTemplates.go
+++ b/src/server/router/routerTemplates.go
@@ -52,6 +52,41 @@ func (r *RouterTemplates)GetTemplates(c echo.Context) error {
     }
 }
 
+// Returns a template that can be listed based on its UUID.
+// Requires either capability:
+// CAP_TPL_LIST_ANY: any templates can be listed.
+// CAP_TPL_LIST: only templates owned by the user as well as public templates can be listed.
+// curl --cacert ca.pem -X GET https://localhost:1077/templates/62ae8791-c108-4235-a7d6-074e9b6a9017 -H "Authorization: Bearer <AccessToken>"
+func (r *RouterTemplates)GetTemplate(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
+    // Retrieves the template based on its UUID.
+    id, err := uuid.Parse(c.Param("id"))
+    if err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+    tpl, err := r.tpl.GetTemplate(id)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusNotFound, err.Error())
+    }
+
+    // If user has CAP_TPL_LIST_ANY, returns the template.
+    if user.HasCapability(caps.CAP_TPL_LIST_ANY) {
+        return c.JSONPretty(http.StatusOK, tpl.SerializeToNetwork(), "    ")
+    } else if user.HasCapability(caps.CAP_TPL_LIST) {
+        // Returns template if owned by the user or template is public.
+        if tpl.GetOwner() == user.Email || tpl.IsPublic() {
+            return c.JSONPretty(http.StatusOK, tpl.SerializeToNetwork(), "    ")
+        }
+    }
+
+    return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
+}
+
 // Creates a template from a VM.
 // Requires CAP_TPL_CREATE.
 // curl --cacert ca.pem -X POST https://localhost:1077/templates/vm -H 'Content-Type: application/json' -d '{"vmID":"e41f3556-ca24-4658-bd79-8c85bd6bff59","name":"Xubuntu_22.04","access":"public"}' -H "Authorization: Bearer <AccessToken>"
diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go
index d687db8..2d28876 100644
--- a/src/server/router/routerVMs.go
+++ b/src/server/router/routerVMs.go
@@ -3,7 +3,6 @@ package router
 import (
     "io"
     "os"
-    // "errors"
     "net/http"
     "path/filepath"
     "nexus-common/caps"
@@ -44,8 +43,14 @@ func (r *RouterVMs)GetListableVMs(c echo.Context) error {
 // Requires to be the VM's owner, or either capability:
 // User cap: CAP_VM_LIST_ANY: returns the VM.
 // VM access cap: CAP_VM_LIST: returns the VM with this cap for the logged user.
-// curl --cacert ca.pem -X GET https://localhost:1077/vm/62ae8791-c108-4235-a7d6-074e9b6a9017 -H "Authorization: Bearer <AccessToken>"
+// curl --cacert ca.pem -X GET https://localhost:1077/vms/62ae8791-c108-4235-a7d6-074e9b6a9017 -H "Authorization: Bearer <AccessToken>"
 func (r *RouterVMs)GetListableVM(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
     // Retrieves the VM based on its UUID.
     id, err := uuid.Parse(c.Param("id"))
     if err != nil {
@@ -56,12 +61,6 @@ func (r *RouterVMs)GetListableVM(c echo.Context) error {
         return echo.NewHTTPError(http.StatusNotFound, err.Error())
     }
 
-    // Retrieves logged user from context.
-    user, err := getLoggedUser(r.users, c)
-    if err != nil {
-        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
-    }
-
     // If user has CAP_VM_LIST_ANY capability, returns the VM.
     if user.HasCapability(caps.CAP_VM_LIST_ANY) {
         return c.JSONPretty(http.StatusOK, vm.SerializeToNetwork(), "    ")
-- 
GitLab