diff --git a/README.md b/README.md index 95e4fe817f49f59a1f05b68700ea50359718dc99..d8c6b641646cf0b55d822df6cbfe645282cc636f 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Here is an example of `users.json` defining user Jane Doe: "pwd": "$2a$10$3MfOrdLLAEJFu0rZkGxGW.bHGiMC/uRD3B5igg5bvdwUedToRqOdO", "caps": { "TPL_CREATE": 1, + "TPL_EDIT": 1, "TPL_DESTROY": 1, "TPL_LIST": 1, "USER_CREATE": 1, @@ -135,14 +136,15 @@ A template directory contains the following files: - `template.json` is the template's configuration - `disk.qcow` is the template's disk image in QCOW2 format -The `template.json` file defines the ID of the template, its name, its owner, and the access type which can either be `public` or `private`: +The `template.json` file defines the ID of the template, its name, its owner, the access type which can either be `public` or `private`, and its creation time: ```{.json} { "id": "12e064f2-2092-428e-9685-f3e573c18802", "name": "Xubuntu 22.04 with gcc toolchain and vscodium editor", "owner": "janedoe@nexus.org", - "access": "public" + "access": "public", + "creationTime": "2022-07-31T16:42:17+01:00" } ``` @@ -289,6 +291,8 @@ The table below lists all potential capabilities associated to a user: | VM_WRITEFS_ANY | Can import files into **ANY** VM | | VM_SET_ACCESS | Can change (edit or delete) a VM's access | | TPL_CREATE | Can create a template | +| TPL_EDIT | Can edit a template | +| TPL_EDIT_ANY | Can edit **ANY** template | | TPL_DESTROY | Can destroy a template | | TPL_DESTROY_ANY | Can destroy **ANY** template | | TPL_LIST | Can list public or owned templates | @@ -387,6 +391,7 @@ These capabilities are called "VM access capabilities": |--- |--- |--- |--- |--- | | `/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` | diff --git a/conf/users.json b/conf/users.json index a407f1f5d1509234d407b079c1591f77d50db215..c9ef8cf4616e6ea7ee7589f612d44b101e43ac77 100644 --- a/conf/users.json +++ b/conf/users.json @@ -20,6 +20,7 @@ "VM_WRITEFS_ANY":1, "VM_SET_ACCESS":1, "TPL_CREATE":1, + "TPL_EDIT":1, "TPL_DESTROY":1, "TPL_DESTROY_ANY":1, "TPL_LIST":1, diff --git a/src/caps/caps.go b/src/caps/caps.go index 2a5c3380a60865d0d5e6063da1e4245d4450644d..8f60b55b4d5cbbfe300a256bb57cb4243f312858 100644 --- a/src/caps/caps.go +++ b/src/caps/caps.go @@ -29,6 +29,8 @@ const ( CAP_VM_WRITEFS_ANY = "VM_WRITEFS_ANY" CAP_TPL_CREATE = "TPL_CREATE" + CAP_TPL_EDIT = "TPL_EDIT" + CAP_TPL_EDIT_ANY = "TPL_EIDT_ANY" CAP_TPL_LIST = "TPL_LIST" CAP_TPL_LIST_ANY = "TPL_LIST_ANY" CAP_TPL_DESTROY = "TPL_DESTROY" @@ -56,6 +58,8 @@ var userCaps = Capabilities { CAP_VM_WRITEFS_ANY: 1, CAP_TPL_CREATE: 1, + CAP_TPL_EDIT: 1, + CAP_TPL_EDIT_ANY: 1, CAP_TPL_DESTROY: 1, CAP_TPL_DESTROY_ANY: 1, CAP_TPL_LIST: 1, diff --git a/src/router/router.go b/src/router/router.go index 89454ff2f1617eac0d68d8b104bd2444070cce88..c2b9f6e486d7fc2fa34e9257b9a7c650043aa4db 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -102,6 +102,7 @@ func (router *Router)Start(port int) { templatesGroup.POST("/qcow", router.tpl.CreateTemplateFromQCOW) templatesGroup.GET("/:id/disk", router.tpl.ExportDisk) templatesGroup.DELETE("/:id", router.tpl.DeleteTemplateByID) + templatesGroup.PUT("/:id", router.tpl.EditTemplateByID) // Starts server in a dedicated goroutine. go func() { diff --git a/src/router/routerTemplates.go b/src/router/routerTemplates.go index c820aeee3d42dd093a154d2eca013a665f6eab07..3545d1a78007d7e1920535319418a51aff22947f 100644 --- a/src/router/routerTemplates.go +++ b/src/router/routerTemplates.go @@ -221,6 +221,59 @@ func (r *RouterTemplates)DeleteTemplateByID(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) } +// Edit a template based on its ID. +// Requires either of: +// CAP_TPL_EDIT_ANY: any template can be edited. +// CAP_TPL_EDIT: only a template owned by the user can be edited. +// curl --cacert ca.pem -X PUT https://localhost:1077/templates/4913a2bb-edfe-4dfe-af53-38197a44523b -H "Authorization: Bearer <AccessToken>" +func (r *RouterTemplates)EditTemplateByID(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 template ID. + tplID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Deserializes and validates the client's parameters. + // Given these parameters are optional, we can't use a validator on them. + // Validation is performed in templates.EditTemplate() instead. + type Parameters struct { + Name string `json:"name"` + Access string `json:"access"` + } + p := new(Parameters) + + if user.HasCapability(caps.CAP_TPL_EDIT_ANY) { + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } else if user.HasCapability(caps.CAP_TPL_EDIT) { + template, err := r.tpl.GetTemplate(tplID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + if template.Owner == user.Email { + if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } + } + + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) +} + // Exports a template's disk image. // Requires either of: // CAP_TPL_READFS_ANY: any template can have its disk exported. diff --git a/src/vms/templates.go b/src/vms/templates.go index a54c31fe1f1a0a37200f8c7a17e6791502bd4890..ad1002711cabea53693f82212e7968d10882471f 100644 --- a/src/vms/templates.go +++ b/src/vms/templates.go @@ -91,6 +91,7 @@ func (templates *Templates)GetTemplates(keep TemplateKeeperFn) []Template { func (templates *Templates)GetTemplate(tplID uuid.UUID) (Template, error) { templates.rwlock.RLock() defer templates.rwlock.RUnlock() + template, exists := templates.m[tplID.String()] if !exists { return dummyTemplate, errors.New("Template not found") @@ -142,3 +143,34 @@ func (templates *Templates)AddTemplate(template *Template) error { func (templates *Templates)getDir() string { return templates.dir } + +// Edit a template' specs: name, access +func (templates *Templates)EditTemplate(tplID uuid.UUID, name, access string) error { + tpl, err := templates.GetTemplate(tplID) + if err != nil { + return err + } + + // Only updates fields that have changed. + if name != "" { + tpl.Name = name + } + if access != "" { + tpl.Access = access + } + + if err = tpl.validate(); err != nil { + return err + } + + err = tpl.writeConfig() + if err != nil { + return err + } + + key := tpl.ID.String() + delete(templates.m, key) + templates.m[key] = tpl + + return nil +} diff --git a/src/vms/vms.go b/src/vms/vms.go index 278fc0bfeec1c089e9b0541824f327c77b9b4451..fa28eb8d823b39a43a17ee6e4bb885c3969b6210 100644 --- a/src/vms/vms.go +++ b/src/vms/vms.go @@ -137,7 +137,7 @@ func (vms *VMs)DeleteVM(vmID uuid.UUID) error { } vm.mutex.Unlock() // Removes the VM from the map. - delete(vms.m, vm.ID.String()) + delete(vms.m, vmID.String()) return nil } @@ -314,7 +314,7 @@ func (vms *VMs)EditVM(vmID uuid.UUID, name string, cpus, ram int, nic NicType) e defer vms.rwlock.Unlock() if err = vms.updateVM(&vm); err != nil { - return errors.New("Failed updating VM") + return err } return nil @@ -350,7 +350,7 @@ func (vms *VMs)SetVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string, ne vm.Access[userEmail] = newAccess if err = vms.updateVM(&vm); err != nil { - return errors.New("Failed updating VM") + return err } return nil @@ -383,7 +383,7 @@ func (vms *VMs)DeleteVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string) delete(vm.Access, userEmail) if err = vms.updateVM(&vm); err != nil { - return errors.New("Failed updating VM") + return err } return nil