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