From dfc598c4be51fa49bbe46fecedc2bacf2978104a Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Sat, 30 Mar 2024 16:42:54 +0100
Subject: [PATCH] Added VM_ATTACH and VM_ATTACH_ANY capabilities. Previously,
 the VM_LIST VM capability allowed one to list the VM as well as attach to it.
 This capability was too coarse. Instead, VM_LIST should only allow one to
 list the VM and nothing more. The VM_ATTACH capability was added specifically
 to allow a user to attach to the VM.

---
 docs/README_client.md                |  28 +++---
 docs/README_server.md                |  26 +++---
 src/client/cmdVM/helper.go           | 126 ++++++++++++++++-----------
 src/client/cmdVM/vmAddAccess.go      |   2 +-
 src/client/cmdVM/vmAttachAsync.go    |   6 +-
 src/client/cmdVM/vmAttachSync.go     |   6 +-
 src/client/cmdVM/vmCreds2csv.go      |   4 +-
 src/client/cmdVM/vmCreds2pdf.go      |   4 +-
 src/client/cmdVM/vmDel.go            |   2 +-
 src/client/cmdVM/vmDelAccess.go      |   2 +-
 src/client/cmdVM/vmEdit.go           |   2 +-
 src/client/cmdVM/vmExportDir.go      |   2 +-
 src/client/cmdVM/vmImportDir.go      |   2 +-
 src/client/cmdVM/vmList.go           |  67 +++++++++++++-
 src/client/cmdVM/vmListSingle.go     |  10 +--
 src/client/cmdVM/vmReboot.go         |   2 +-
 src/client/cmdVM/vmShutdown.go       |   2 +-
 src/client/cmdVM/vmStart.go          |   2 +-
 src/client/cmdVM/vmStartWithCreds.go |   2 +-
 src/client/cmdVM/vmStop.go           |   2 +-
 src/client/version/version.go        |   2 +-
 src/common/caps/caps.go              |   4 +
 src/common/vm/vm.go                  |  10 ++-
 src/server/router/routerVMs.go       |  52 +++++++++--
 src/server/version/version.go        |   2 +-
 src/server/vms/vm.go                 |  21 +++--
 src/server/vms/vms.go                |  24 ++++-
 27 files changed, 290 insertions(+), 124 deletions(-)

diff --git a/docs/README_client.md b/docs/README_client.md
index ca7af11..27c62a2 100644
--- a/docs/README_client.md
+++ b/docs/README_client.md
@@ -376,9 +376,9 @@ Delete VMs matching the "exam ISC_433 PCO" pattern:
 vmdel "exam ISC_433 PCO"
 ```
 
-Set the VM access for VM `89649fe3-4940-4b77-929e-50903789cd87` with: `VM_LIST` and `VM_DESTROY` for user `student@nexus.org`:
+Set the VM access for VM `89649fe3-4940-4b77-929e-50903789cd87` with: `VM_LIST` and `VM_ATTACH` for user `student@nexus.org`:
 ```
-vmaddaccess 89649fe3-4940-4b77-929e-50903789cd87 student@nexus.org VM_LIST VM_DESTROY
+vmaddaccess 89649fe3-4940-4b77-929e-50903789cd87 student@nexus.org VM_LIST VM_ATTACH
 ```
 
 Set VM access for VMs matching the "alpine" pattern with: `VM_START` and `VM_STOP` for user `student@nexus.org`:
@@ -675,8 +675,8 @@ source .env.nexus
 Access control is implemented through capabilities.
 Capabilities define what users can or cannot do. They are divided into two categories:
 
-- **User capabilities**: define user access control; these are stored in the user metadata
-- **VM access capabilities**: define access to VMs; these are stored in the VM metadata
+- **User capabilities**: define user access control; these are stored in the user config
+- **VM access capabilities**: define access to VMs; these are stored in the VM config
 
 ### User capabilities
 
@@ -697,6 +697,7 @@ The table below lists all potential capabilities associated to a user:
 | VM_STOP_ANY       | Can kill/shutdown **ANY** VM                      |
 | VM_REBOOT_ANY     | Can Reboot **ANY** VM                             |
 | VM_LIST_ANY       | Can list **ANY** VM                               |
+| VM_ATTACH_ANY     | Can attach to **ANY** VM                          |
 | VM_READFS_ANY     | Can export files from **ANY** VM                  |
 | VM_WRITEFS_ANY    | Can import files into **ANY** VM                  |
 | VM_SET_ACCESS     | Can change (edit or delete) a VM's access         |
@@ -720,16 +721,17 @@ These capabilities are called "VM access capabilities":
 
 | Capability    | Description                                                        |
 |---            |---                                                                 |
-| VM_SET_ACCESS | User can add/change access to the VM                               |
+| VM_SET_ACCESS | User can add/change access to the (running or stopped) VM          |
 |               | VM_SET_ACCESS **must also be present** in the user's capabilities! |
-| VM_DESTROY    | User can destroy the VM                                            |
-| VM_EDIT       | User can edit the VM                                               |
-| VM_START      | User can start the VM                                              |
-| VM_STOP       | User can kill/shutdown the VM                                      |
-| VM_REBOOT     | User can reboot the VM                                             |
-| VM_LIST       | User can list or attach to the VM                                  |
-| VM_READFS     | User can export files from the VM                                  |
-| VM_WRITEFS    | User can import files into the VM                                  |
+| VM_DESTROY    | User can destroy the (stopped) VM                                  |
+| VM_EDIT       | User can edit the (running or stopped) VM                          |
+| VM_START      | User can start the (stopped) VM                                    |
+| VM_STOP       | User can kill/shutdown the (running) VM                            |
+| VM_REBOOT     | User can reboot the (running) VM                                   |
+| VM_LIST       | User can list the VM's meta-data                                   |
+| VM_ATTACH     | User can attach to the (running) VM                                |
+| VM_READFS     | User can export files from the (stopped) VM                        |
+| VM_WRITEFS    | User can import files into the (stopped) VM                        |
 
 ### IMPORTANT
 
diff --git a/docs/README_server.md b/docs/README_server.md
index 8b4fbc5..ced79b1 100644
--- a/docs/README_server.md
+++ b/docs/README_server.md
@@ -192,12 +192,14 @@ Here is the content of `vm.json`:
     "access": {
         "lukeskywalker@force.net" : {
             "VM_LIST":1,
+            "VM_ATTACH":1,
             "VM_START":1,
             "VM_STOP":1,
             "VM_REBOOT":1
         },
         "student@nexus.org" : {
-            "VM_LIST":1
+            "VM_LIST":1,
+            "VM_ATTACH":1
         }
     }
 }
@@ -269,6 +271,7 @@ The table below lists all potential capabilities associated to a user:
 | VM_STOP_ANY       | Can kill/shutdown **ANY** VM                      |
 | VM_REBOOT_ANY     | Can Reboot **ANY** VM                             |
 | VM_LIST_ANY       | Can list **ANY** VM                               |
+| VM_ATTACH_ANY     | Can attach to **ANY** VM                          |
 | VM_READFS_ANY     | Can export files from **ANY** VM                  |
 | VM_WRITEFS_ANY    | Can import files into **ANY** VM                  |
 | VM_SET_ACCESS     | Can change (edit or delete) a VM's access         |
@@ -292,16 +295,17 @@ These capabilities are called "VM access capabilities":
 
 | Capability    | Description                                                        |
 |---            |---                                                                 |
-| VM_SET_ACCESS | User can add/change access to the VM                               |
+| VM_SET_ACCESS | User can add/change access to the (running or stopped) VM          |
 |               | VM_SET_ACCESS **must also be present** in the user's capabilities! |
-| VM_DESTROY    | User can destroy the VM                                            |
-| VM_EDIT       | User can edit the VM                                               |
-| VM_START      | User can start the VM                                              |
-| VM_STOP       | User can kill/shutdown the VM                                      |
-| VM_REBOOT     | User can reboot the VM                                             |
-| VM_LIST       | User can list or attach to the VM                                  |
-| VM_READFS     | User can export files from the VM                                  |
-| VM_WRITEFS    | User can import files into the VM                                  |
+| VM_DESTROY    | User can destroy the (stopped) VM                                  |
+| VM_EDIT       | User can edit the (running or stopped) VM                          |
+| VM_START      | User can start the (stopped) VM                                    |
+| VM_STOP       | User can kill/shutdown the (running) VM                            |
+| VM_REBOOT     | User can reboot the (running) VM                                   |
+| VM_LIST       | User can list the VM's meta-data                                   |
+| VM_ATTACH     | User can attach to the (running) VM                                |
+| VM_READFS     | User can export files from the (stopped) VM                        |
+| VM_WRITEFS    | User can import files into the (stopped) VM                        |
 
 ### IMPORTANT
 
@@ -340,7 +344,7 @@ These capabilities are called "VM access capabilities":
 | `/vms`                   | returns VMs that can be listed                 | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |
 | `/vms/{id}`              | returns a VM                                   | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |
 | `/vms/start`             | returns VMs that can be started                | GET    |            | `VM_START_ANY`   | OR  | `VM_START`          |
-| `/vms/attach`            | returns VMs that can be attached to            | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |
+| `/vms/attach`            | returns VMs that can be attached to            | GET    |            | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         |
 | `/vms/stop`              | returns VMs that can be killed/shutdown        | GET    |            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
 | `/vms/reboot`            | returns VMs that can be rebooted               | GET    |            | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         |
 | `/vms/edit`              | returns VMs that can be edited                 | GET    |            | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |
diff --git a/src/client/cmdVM/helper.go b/src/client/cmdVM/helper.go
index 779c860..f36edfb 100644
--- a/src/client/cmdVM/helper.go
+++ b/src/client/cmdVM/helper.go
@@ -1,7 +1,6 @@
 package cmdVM
 
 import (
-    "fmt"
     "regexp"
     "errors"
     "strings"
@@ -33,69 +32,81 @@ Regex examples:
     u.PrintlnErr(usage)
 }
 
-// Prints a list of filtered VMs for a given route.
-// Return 0 if everything went well or 1 in case of failure.
-func printFilteredVMs(c cmd.Command, args []string, route string) int {
-    if len(args) < 1 {
-        c.PrintUsage()
-        return 1
+// Returns a list of filtered VMs for a given route.
+// Filters are based on patterns describing IDs or regexes.
+// Patterns can either be:
+// - any number of "VM IDs" (UUID)
+// - any number of "VM names"
+// - any combination of "VM IDs" and "VM names"
+// - any regular expression (it applies to the VM's name only)
+// Remark: the matching is case-insensitive
+// Regular expression examples:
+//   "."   -> matches everything
+//   "bla" -> matches any VM name containing "bla"
+func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized, error) {
+    if len(patterns) < 1 {
+        return nil, errors.New("At least one ID or regex must be specified")
     }
 
-    // Check if a "-l" argument is specified
-    foundLongOutputFlag := -1
-    for idx, arg := range args {
-        if arg == "-l" {
-            foundLongOutputFlag = idx
-        }
-    }
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
+    var ids []string
+    var regexes []string
 
-    if foundLongOutputFlag >= 0 {
-        args = u.RemoveArgAtIndex(args, foundLongOutputFlag)
+    for _, pattern := range patterns {
+        _, err := uuid.Parse(pattern)
+        if err != nil {
+            regexes = append(regexes, pattern)
+        } else {
+            ids = append(ids, pattern)
+        }
     }
 
-    vms, err := getFilteredVMs(route, args)
+    resp, err := client.R().Get(host+route)
     if err != nil {
-        u.PrintlnErr("Error: "+err.Error())
-        return 1
+        return nil, err
     }
 
-    if foundLongOutputFlag >= 0 {
-        for _, vm := range vms {
-            str, err := vm.String()
-            if err != nil {
-                u.PrintlnErr("Failed decoding VM "+vm.ID.String()+" to string. Skipped.")
-            } else {
-                u.Println(str)
-            }
+    vmsList := []vm.VMNetworkSerialized{}
+
+    if resp.IsSuccess() {
+        vms, err := deserializeVMs(resp)
+        if err != nil {
+            return nil, err
         }
-    } else {
-        // Compute the length of the longest name and state (used for padding columns)
-        nameLen := 0
-        idLen := 0
-        stateLen := 0
+
         for _, vm := range vms {
-            l := len(vm.Name)
-            if l > nameLen {
-                nameLen = l
+            found := false
+            for _, id := range ids {
+                if id == vm.ID.String() {
+                    vmsList = append(vmsList, vm)
+                    found = true
+                    break
+                }
             }
-            idLen = len(vm.ID.String())  // ID is a UUID of constant length
-            l = len(vm.State)
-            if l > stateLen {
-                stateLen = l
+            if found {
+                continue
+            }
+            for _, regex := range regexes {
+                match, err := regexp.MatchString(strings.ToLower(regex), strings.ToLower(vm.Name))
+                if err != nil {
+                    return nil, errors.New("Error matching \""+regex+"\": "+err.Error())
+                } else {
+                    if match {
+                        vmsList = append(vmsList, vm)
+                        break
+                    }
+                }
             }
         }
-
-        paddedFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%-%ds\n",nameLen, idLen, stateLen)
-
-        for _, vm := range vms {
-            fmt.Printf(paddedFmt, vm.Name, vm.ID, vm.State)
-        }
+        return vmsList, nil
+    } else {
+        return nil, errors.New("Error: "+resp.Status()+": "+resp.String())
     }
-
-    return 0
 }
 
-// Returns a list of filtered VMs for a given route.
+// Returns a list of filtered VM credentials for a given route.
 // Filters are based on patterns describing IDs or regexes.
 // Patterns can either be:
 // - any number of "VM IDs" (UUID)
@@ -106,7 +117,7 @@ func printFilteredVMs(c cmd.Command, args []string, route string) int {
 // Regular expression examples:
 //   "."   -> matches everything
 //   "bla" -> matches any VM name containing "bla"
-func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized, error) {
+func getFilteredVMCredentials(route string, patterns []string) ([]vm.VMCredentialsSerialized, error) {
     if len(patterns) < 1 {
         return nil, errors.New("At least one ID or regex must be specified")
     }
@@ -131,10 +142,10 @@ func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized,
         return nil, err
     }
 
-    vmsList := []vm.VMNetworkSerialized{}
+    vmsList := []vm.VMCredentialsSerialized{}
 
     if resp.IsSuccess() {
-        vms, err := deserializeVMs(resp)
+        vms, err := deserializeVMCredentials(resp)
         if err != nil {
             return nil, err
         }
@@ -169,7 +180,7 @@ func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized,
     }
 }
 
-// Deserialize a list of VMs from an http response (no filtering).
+// Deserialize a list of VMs from an http response.
 func deserializeVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
     vms := []vm.VMNetworkSerialized{}
     if err := json.Unmarshal(resp.Body(), &vms); err != nil {
@@ -178,6 +189,15 @@ func deserializeVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
     return vms, nil
 }
 
+// Deserialize a list of VM credentials from an http response.
+func deserializeVMCredentials(resp *resty.Response) ([]vm.VMCredentialsSerialized, error) {
+    vms := []vm.VMCredentialsSerialized{}
+    if err := json.Unmarshal(resp.Body(), &vms); err != nil {
+        return nil, err
+    }
+    return vms, nil
+}
+
 // Deserialize a single VM from an http response.
 func deserializeVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
     var vm *vm.VMNetworkSerialized = &vm.VMNetworkSerialized{}
@@ -185,4 +205,4 @@ func deserializeVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
         return vm, err
     }
     return vm, nil
-}
\ No newline at end of file
+}
diff --git a/src/client/cmdVM/vmAddAccess.go b/src/client/cmdVM/vmAddAccess.go
index d56bea5..ce53652 100644
--- a/src/client/cmdVM/vmAddAccess.go
+++ b/src/client/cmdVM/vmAddAccess.go
@@ -22,7 +22,7 @@ func (cmd *AddAccess)GetName() string {
 func (cmd *AddAccess)GetDesc() []string {
     return []string{
         "Adds a user's VM access in one or more VMs.",
-        "Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
+        "If not the VM's owner: requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
 }
 
 func (cmd *AddAccess)PrintUsage() {
diff --git a/src/client/cmdVM/vmAttachAsync.go b/src/client/cmdVM/vmAttachAsync.go
index f0bdb57..6fc0beb 100644
--- a/src/client/cmdVM/vmAttachAsync.go
+++ b/src/client/cmdVM/vmAttachAsync.go
@@ -19,7 +19,7 @@ func (cmd *AttachAsync)GetName() string {
 func (cmd *AttachAsync)GetDesc() []string {
     return []string{
         "Attaches to one or more VMs in order to use their desktop environment.",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
 func (cmd *AttachAsync)PrintUsage() {
@@ -42,7 +42,7 @@ func (cmd *AttachAsync)Run(args []string) int {
         return 1
     }
 
-    vms, err := getFilteredVMs("/vms/attach", args)
+    vms, err := getFilteredVMCredentials("/vms/attach", args)
     if err != nil {
         u.PrintlnErr(err.Error())
         return 1
@@ -56,7 +56,7 @@ func (cmd *AttachAsync)Run(args []string) int {
     statusCode := 0
 
     for _, v := range(vms) {
-        go func(v vm.VMNetworkSerialized) {
+        go func(v vm.VMCredentialsSerialized) {
             stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
             if err != nil {
                 u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
diff --git a/src/client/cmdVM/vmAttachSync.go b/src/client/cmdVM/vmAttachSync.go
index dcb7893..b824487 100644
--- a/src/client/cmdVM/vmAttachSync.go
+++ b/src/client/cmdVM/vmAttachSync.go
@@ -20,7 +20,7 @@ func (cmd *AttachSync)GetName() string {
 func (cmd *AttachSync)GetDesc() []string {
     return []string{
         "Attaches to one or more VMs in order to use their desktop environment.",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *AttachSync)PrintUsage() {
@@ -43,7 +43,7 @@ func (cmd *AttachSync)Run(args []string) int {
         return 1
     }
 
-    vms, err := getFilteredVMs("/vms/attach", args)
+    vms, err := getFilteredVMCredentials("/vms/attach", args)
     if err != nil {
         u.PrintlnErr(err.Error())
         return 1
@@ -61,7 +61,7 @@ func (cmd *AttachSync)Run(args []string) int {
     statusCode := 0
 
     for _, v := range(vms) {
-        go func(v vm.VMNetworkSerialized) {
+        go func(v vm.VMCredentialsSerialized) {
             stdoutStderr, err := exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
             if err != nil {
                 u.PrintlnErr("Failed attaching to VM ", v.ID, ": ", fmt.Sprintf("%s", stdoutStderr))
diff --git a/src/client/cmdVM/vmCreds2csv.go b/src/client/cmdVM/vmCreds2csv.go
index ee1f8b7..3033e48 100644
--- a/src/client/cmdVM/vmCreds2csv.go
+++ b/src/client/cmdVM/vmCreds2csv.go
@@ -20,7 +20,7 @@ func (cmd *Creds2csv)GetDesc() []string {
     return []string{
         "Creates a CSV file with the credentials required to attach to running VMs.",
         "The written CSV file contains 4 columns: VM ID;VM name;port;password",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
 func (cmd *Creds2csv)PrintUsage() {
@@ -42,7 +42,7 @@ func (cmd *Creds2csv)Run(args []string) int {
 
     csvFile := args[argc-1]
 
-    vms, err := getFilteredVMs("/vms/attach", args[:argc-1])
+    vms, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
     if err != nil {
         u.PrintlnErr("Error: "+err.Error())
         return 1
diff --git a/src/client/cmdVM/vmCreds2pdf.go b/src/client/cmdVM/vmCreds2pdf.go
index 51a1439..f551123 100644
--- a/src/client/cmdVM/vmCreds2pdf.go
+++ b/src/client/cmdVM/vmCreds2pdf.go
@@ -21,7 +21,7 @@ func (cmd *Creds2pdf)GetName() string {
 func (cmd *Creds2pdf)GetDesc() []string {
     return []string{
         "Creates a PDF with the credentials required to attach to running VMs.",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_ATTACH VM access capability or VM_ATTACH_ANY user capability."}
 }
 
 func (cmd *Creds2pdf)PrintUsage() {
@@ -43,7 +43,7 @@ func (cmd *Creds2pdf)Run(args []string) int {
 
     pdfFile := args[argc-1]
 
-    vms, err := getFilteredVMs("/vms/attach", args[:argc-1])
+    vms, err := getFilteredVMCredentials("/vms/attach", args[:argc-1])
     if err != nil {
         u.PrintlnErr("Error: "+err.Error())
         return 1
diff --git a/src/client/cmdVM/vmDel.go b/src/client/cmdVM/vmDel.go
index 6852b66..2fd8b14 100644
--- a/src/client/cmdVM/vmDel.go
+++ b/src/client/cmdVM/vmDel.go
@@ -16,7 +16,7 @@ func (cmd *Del)GetName() string {
 func (cmd *Del)GetDesc() []string {
     return []string{
         "Deletes one or more VMs.",
-        "Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability."}
+        "If not the VM's owner: requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability."}
 }
 
 func (cmd *Del)PrintUsage() {
diff --git a/src/client/cmdVM/vmDelAccess.go b/src/client/cmdVM/vmDelAccess.go
index 79db5ee..5843403 100644
--- a/src/client/cmdVM/vmDelAccess.go
+++ b/src/client/cmdVM/vmDelAccess.go
@@ -21,7 +21,7 @@ func (cmd *DelAccess)GetName() string {
 func (cmd *DelAccess)GetDesc() []string {
     return []string{
         "Removes a user's VM access in one or more VMs.",
-        "Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
+        "If not the VM's owner: requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
 }
 
 func (cmd *DelAccess)PrintUsage() {
diff --git a/src/client/cmdVM/vmEdit.go b/src/client/cmdVM/vmEdit.go
index 4710b2c..53b1792 100644
--- a/src/client/cmdVM/vmEdit.go
+++ b/src/client/cmdVM/vmEdit.go
@@ -21,7 +21,7 @@ func (cmd *Edit)GetName() string {
 func (cmd *Edit)GetDesc() []string {
     return []string{
         "Edits one or more VMs' properties: name, cpus, ram or nic.",
-        "Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability."}
+        "If not the VM's owner: requires VM_EDIT VM access capability or VM_EDIT_ANY user capability."}
 }
 
 func (cmd *Edit)PrintUsage() {
diff --git a/src/client/cmdVM/vmExportDir.go b/src/client/cmdVM/vmExportDir.go
index cfb0474..0a5a696 100644
--- a/src/client/cmdVM/vmExportDir.go
+++ b/src/client/cmdVM/vmExportDir.go
@@ -17,7 +17,7 @@ func (cmd *ExportDir)GetName() string {
 func (cmd *ExportDir)GetDesc() []string {
     return []string{
         "Exports one or more VMs' directory into one or more compressed archives.",
-        "Requires VM_READFS VM access capability or VM_READFS_ANY user capability."}
+        "If not the VM's owner: requires VM_READFS VM access capability or VM_READFS_ANY user capability."}
 }
 
 func (cmd *ExportDir)PrintUsage() {
diff --git a/src/client/cmdVM/vmImportDir.go b/src/client/cmdVM/vmImportDir.go
index cc5be51..36b4d28 100644
--- a/src/client/cmdVM/vmImportDir.go
+++ b/src/client/cmdVM/vmImportDir.go
@@ -17,7 +17,7 @@ func (cmd *ImportDir)GetName() string {
 func (cmd *ImportDir)GetDesc() []string {
     return []string{
         "Copies a local directory (or file) and all its content into one or more VMs.",
-        "Requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability."}
+        "If not the VM's owner: requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability."}
 }
 
 func (cmd *ImportDir)PrintUsage() {
diff --git a/src/client/cmdVM/vmList.go b/src/client/cmdVM/vmList.go
index 6a5af7d..d253b8d 100644
--- a/src/client/cmdVM/vmList.go
+++ b/src/client/cmdVM/vmList.go
@@ -1,6 +1,7 @@
 package cmdVM
 
 import (
+    "fmt"
     u "nexus-client/utils"
 )
 
@@ -15,7 +16,7 @@ func (cmd *List)GetName() string {
 func (cmd *List)GetDesc() []string {
     return []string{
         "Lists VMs.",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *List)PrintUsage() {
@@ -30,5 +31,67 @@ func (cmd *List)PrintUsage() {
 }
 
 func (cmd *List)Run(args []string) int {
-    return printFilteredVMs(cmd, args, "/vms")
+    return cmd.printFilteredVMs(args, "/vms")
+}
+
+// Prints a list of filtered VMs for a given route.
+// Return 0 if everything went well or 1 in case of failure.
+func (cmd *List)printFilteredVMs(args []string, route string) int {
+    if len(args) < 1 {
+        cmd.PrintUsage()
+        return 1
+    }
+
+    // Check if a "-l" argument is specified
+    foundLongOutputFlag := -1
+    for idx, arg := range args {
+        if arg == "-l" {
+            foundLongOutputFlag = idx
+        }
+    }
+
+    if foundLongOutputFlag >= 0 {
+        args = u.RemoveArgAtIndex(args, foundLongOutputFlag)
+    }
+
+    vms, err := getFilteredVMs(route, args)
+    if err != nil {
+        u.PrintlnErr("Error: "+err.Error())
+        return 1
+    }
+
+    if foundLongOutputFlag >= 0 {
+        for _, vm := range vms {
+            str, err := vm.String()
+            if err != nil {
+                u.PrintlnErr("Failed decoding VM "+vm.ID.String()+" to string. Skipped.")
+            } else {
+                u.Println(str)
+            }
+        }
+    } else {
+        // Compute the length of the longest name and state (used for padding columns)
+        nameLen := 0
+        idLen := 0
+        stateLen := 0
+        for _, vm := range vms {
+            l := len(vm.Name)
+            if l > nameLen {
+                nameLen = l
+            }
+            idLen = len(vm.ID.String())  // ID is a UUID of constant length
+            l = len(vm.State)
+            if l > stateLen {
+                stateLen = l
+            }
+        }
+
+        paddedFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%-%ds\n",nameLen, idLen, stateLen)
+
+        for _, vm := range vms {
+            fmt.Printf(paddedFmt, vm.Name, vm.ID, vm.State)
+        }
+    }
+
+    return 0
 }
diff --git a/src/client/cmdVM/vmListSingle.go b/src/client/cmdVM/vmListSingle.go
index 2c970eb..074a3ad 100644
--- a/src/client/cmdVM/vmListSingle.go
+++ b/src/client/cmdVM/vmListSingle.go
@@ -16,7 +16,7 @@ func (cmd *ListSingle)GetName() string {
 func (cmd *ListSingle)GetDesc() []string {
     return []string{
         "Lists a single VM.",
-        "Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
+        "If not the VM's owner: requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *ListSingle)PrintUsage() {
@@ -38,10 +38,10 @@ func (cmd *ListSingle)Run(args []string) int {
         return 1
     }
 
-    uuid := args[0]
-    resp, err := client.R().Get(host+"/vms/"+uuid)
+    vmID := args[0]
+    resp, err := client.R().Get(host+"/vms/"+vmID)
     if err != nil {
-        u.PrintlnErr("Failed retrieving VM \""+uuid+"\": "+err.Error())
+        u.PrintlnErr("Failed retrieving VM \""+vmID+"\": "+err.Error())
         return 1
     } else {
         if resp.IsSuccess() {
@@ -57,7 +57,7 @@ func (cmd *ListSingle)Run(args []string) int {
                 u.Println(str)
             }
         } else {
-            u.PrintlnErr("Failed retrieving VM \""+uuid+"\": "+resp.Status()+": "+resp.String())
+            u.PrintlnErr("Failed retrieving VM \""+vmID+"\": "+resp.Status()+": "+resp.String())
             return 1
         }
     }
diff --git a/src/client/cmdVM/vmReboot.go b/src/client/cmdVM/vmReboot.go
index 7bfd137..3e20b32 100644
--- a/src/client/cmdVM/vmReboot.go
+++ b/src/client/cmdVM/vmReboot.go
@@ -16,7 +16,7 @@ func (cmd *Reboot)GetName() string {
 func (cmd *Reboot)GetDesc() []string {
     return []string{
         "Gracefully reboots one or more VMs.",
-        "Requires VM_REBOOT VM access capability or VM_REBOOT_ANY user capability."}
+        "If not the VM's owner: requires VM_REBOOT VM access capability or VM_REBOOT_ANY user capability."}
 }
 
 func (cmd *Reboot)PrintUsage() {
diff --git a/src/client/cmdVM/vmShutdown.go b/src/client/cmdVM/vmShutdown.go
index 7ba1130..35fd4b9 100644
--- a/src/client/cmdVM/vmShutdown.go
+++ b/src/client/cmdVM/vmShutdown.go
@@ -16,7 +16,7 @@ func (cmd *Shutdown)GetName() string {
 func (cmd *Shutdown)GetDesc() []string {
     return []string{
         "Gracefully shutdowns one or more VMs.",
-        "Requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
+        "If not the VM's owner: requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
 }
 
 func (cmd *Shutdown)PrintUsage() {
diff --git a/src/client/cmdVM/vmStart.go b/src/client/cmdVM/vmStart.go
index 2656261..5963e21 100644
--- a/src/client/cmdVM/vmStart.go
+++ b/src/client/cmdVM/vmStart.go
@@ -16,7 +16,7 @@ func (cmd *Start)GetName() string {
 func (cmd *Start)GetDesc() []string {
     return []string{
         "Starts one or more VMs.",
-        "Requires VM_START VM access capability or VM_START_ANY user capability."}
+        "If not the VM's owner: requires VM_START VM access capability or VM_START_ANY user capability."}
 }
 
 func (cmd *Start)PrintUsage() {
diff --git a/src/client/cmdVM/vmStartWithCreds.go b/src/client/cmdVM/vmStartWithCreds.go
index e8bbbef..38a24a5 100644
--- a/src/client/cmdVM/vmStartWithCreds.go
+++ b/src/client/cmdVM/vmStartWithCreds.go
@@ -18,7 +18,7 @@ func (cmd *StartWithCreds)GetName() string {
 func (cmd *StartWithCreds)GetDesc() []string {
     return []string{
         "Starts one or more VMs with user-defined credentials.",
-        "Requires VM_START VM access capability or VM_START_ANY user capability."}
+        "If not the VM's owner: requires VM_START VM access capability or VM_START_ANY user capability."}
 }
 
 func (cmd *StartWithCreds)PrintUsage() {
diff --git a/src/client/cmdVM/vmStop.go b/src/client/cmdVM/vmStop.go
index f3d4651..74939fa 100644
--- a/src/client/cmdVM/vmStop.go
+++ b/src/client/cmdVM/vmStop.go
@@ -16,7 +16,7 @@ func (cmd *Stop)GetName() string {
 func (cmd *Stop)GetDesc() []string {
     return []string{
         "Kills one or more VMs.",
-        "Requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
+        "If not the VM's owner: requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
 }
 
 func (cmd *Stop)PrintUsage() {
diff --git a/src/client/version/version.go b/src/client/version/version.go
index 4877ef2..07b225e 100644
--- a/src/client/version/version.go
+++ b/src/client/version/version.go
@@ -8,7 +8,7 @@ import (
 const (
     major  = 1
     minor  = 9
-    bugfix = 0
+    bugfix = 1
 )
 
 type Version struct {
diff --git a/src/common/caps/caps.go b/src/common/caps/caps.go
index ffc6358..e29d721 100644
--- a/src/common/caps/caps.go
+++ b/src/common/caps/caps.go
@@ -15,6 +15,8 @@ const (
 
     CAP_VM_LIST              = "VM_LIST"
     CAP_VM_LIST_ANY          = "VM_LIST_ANY"
+    CAP_VM_ATTACH            = "VM_ATTACH"
+    CAP_VM_ATTACH_ANY        = "VM_ATTACH_ANY"
     CAP_VM_START             = "VM_START"
     CAP_VM_START_ANY         = "VM_START_ANY"
     CAP_VM_STOP              = "VM_STOP"
@@ -58,6 +60,7 @@ var userCaps = Capabilities {
     CAP_VM_STOP_ANY: 1,
     CAP_VM_REBOOT_ANY: 1,
     CAP_VM_LIST_ANY: 1,
+    CAP_VM_ATTACH_ANY: 1,
     CAP_VM_SET_ACCESS: 1,
     CAP_VM_SET_ACCESS_ANY: 1,
     CAP_VM_READFS_ANY: 1,
@@ -83,6 +86,7 @@ var VMAccessCaps = Capabilities {
     CAP_VM_STOP: 1,
     CAP_VM_REBOOT: 1,
     CAP_VM_LIST: 1,
+    CAP_VM_ATTACH: 1,
     CAP_VM_READFS: 1,
     CAP_VM_WRITEFS: 1,
 }
diff --git a/src/common/vm/vm.go b/src/common/vm/vm.go
index a99d1c1..10f62ac 100644
--- a/src/common/vm/vm.go
+++ b/src/common/vm/vm.go
@@ -33,11 +33,17 @@ type (
         Access map[string]caps.Capabilities `json:"access"`
 
         State VMState          `json:"state"`
-        Port int               `json:"port"`
-        Pwd string             `json:"pwd"`
         DiskBusy bool          `json:"diskBusy"`  // When true, the disk image must not be modified
     }
 
+    // VM id, name and credentials to be serialized over the network.
+    VMCredentialsSerialized struct {
+        ID uuid.UUID          `json:"id"         validate:"required"`
+        Name string           `json:"name"       validate:"required,min=2,max=256"`
+        Port int              `json:"port        validate:"required"`
+        Pwd string            `json:"pwd         validate:"required"`
+    }
+
     VMState string  // see stateXXX below
     NicType string  // see nicXXX below
 )
diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go
index 2d28876..b249dc1 100644
--- a/src/server/router/routerVMs.go
+++ b/src/server/router/routerVMs.go
@@ -63,16 +63,16 @@ func (r *RouterVMs)GetListableVM(c echo.Context) 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(), "    ")
+        return c.JSONPretty(http.StatusOK, vm.Serialize(), "    ")
     } else {
         if vm.IsOwner(user.Email) {
-            return c.JSONPretty(http.StatusOK, vm.SerializeToNetwork(), "    ")
+            return c.JSONPretty(http.StatusOK, vm.Serialize(), "    ")
         } else {
             capabilities, exists := vm.GetAccess()[user.Email]
             if exists {
                 _, found := capabilities[caps.CAP_VM_LIST]
                 if found {
-                    return c.JSONPretty(http.StatusOK, vm.SerializeToNetwork(), "    ")
+                    return c.JSONPretty(http.StatusOK, vm.Serialize(), "    ")
                 } else {
                     return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
                 }
@@ -85,11 +85,11 @@ func (r *RouterVMs)GetListableVM(c echo.Context) error {
 
 // Returns VMs that are running and that can be attached to.
 // Requires to be the VM's owner, or either capability:
-// User cap: CAP_VM_LIST_ANY: returns all running VMs.
-// VM access cap: CAP_VM_LIST: returns all running VMs with this cap for the logged user.
+// User cap: CAP_VM_ATTACH_ANY: returns all running VMs.
+// VM access cap: CAP_VM_ATTACH: returns all running VMs with this cap for the logged user.
 // curl --cacert ca.pem -X GET https://localhost:1077/vms/attach -H "Authorization: Bearer <AccessToken>"
 func (r *RouterVMs)GetAttachableVMs(c echo.Context) error {
-    return r.getFilteredVMs(c, caps.CAP_VM_LIST_ANY, caps.CAP_VM_LIST, func(vm *vms.VM) bool {
+    return r.getFilteredVMCredentials(c, caps.CAP_VM_ATTACH_ANY, caps.CAP_VM_ATTACH, func(vm *vms.VM) bool {
         return vm.IsRunning()
     })
 }
@@ -249,7 +249,7 @@ func (r *RouterVMs)CreateVM(c echo.Context) error {
         return echo.NewHTTPError(http.StatusBadRequest, err.Error())
     }
 
-    return c.JSONPretty(http.StatusCreated, vm.SerializeToNetwork(), "    ")
+    return c.JSONPretty(http.StatusCreated, vm.Serialize(), "    ")
 }
 
 // Deletes a VM based on its ID.
@@ -360,7 +360,7 @@ func (r *RouterVMs)EditVM(c echo.Context) error {
         if err := r.vms.EditVM(vm.GetID(), p); err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
-        return c.JSONPretty(http.StatusOK, vm.SerializeToNetwork(), "    ")
+        return c.JSONPretty(http.StatusOK, vm.Serialize(), "    ")
     })
 }
 
@@ -550,6 +550,42 @@ func (r *RouterVMs)getFilteredVMs(c echo.Context, userCapabilityAny, vmAccessCap
     }
 }
 
+// Helper function that returns a list of serialized VM credentials if either condition is true:
+// - the logged user has the userCapabilityAny capability.
+// - the logged user is the VM's owner.
+// - the VM access for the logged user matches the vmAccessCapability capability.
+// Additionally, VMs for which the cond function returns false are filtered out.
+func (r *RouterVMs)getFilteredVMCredentials(c echo.Context, userCapabilityAny, vmAccessCapability string, cond vms.VMKeeperFn) 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 the XX_ANY capability, returns all VMs.
+    if user.HasCapability(userCapabilityAny) {
+        // Returns all VMs that pass the condition.
+        return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMCredentials(cond), "    ")
+    } else {
+        // Returns all VMs:
+        // - owned by the user
+        // - for which the user has the specified capability (vmAccessCapability) in the VM's access
+        return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMCredentials(func(vm *vms.VM) bool {
+            if vm.IsOwner(user.Email) {
+                return cond(vm)
+            } else {
+                capabilities, exists := vm.GetAccess()[user.Email]
+                if exists {
+                    _, found := capabilities[vmAccessCapability]
+                    return found && cond(vm)
+                } else {
+                    return false
+                }
+            }
+        }), "    ")
+    }
+}
+
 // Helper function that executes an action on a VM if either condition is true:
 // - the logged user is the VM's owner (in this case, user has all VM access capabilities).
 // - the logged user has the userCapabilityAny capability.
diff --git a/src/server/version/version.go b/src/server/version/version.go
index a9c0dce..840b8bf 100644
--- a/src/server/version/version.go
+++ b/src/server/version/version.go
@@ -7,7 +7,7 @@ import (
 const (
     major  = 1
     minor  = 9
-    bugfix = 0
+    bugfix = 1
 )
 
 type Version struct {
diff --git a/src/server/vms/vm.go b/src/server/vms/vm.go
index ea8662f..5823661 100644
--- a/src/server/vms/vm.go
+++ b/src/server/vms/vm.go
@@ -84,9 +84,9 @@ func NewVM(creatorEmail string, name string, cpus, ram int, nic vmc.NicType, usb
     return vm, nil
 }
 
-// Returns a serialized version (to be written to disk) of the VM.
+// Returns a serialized version of the VM with fields that must be stored on disk.
 // Concurrency: safe
-func (vm *VM)SerializeToDisk() vmc.VMDiskSerialized {
+func (vm *VM)SerializeForDisk() vmc.VMDiskSerialized {
     return vmc.VMDiskSerialized {
         ID: vm.v.ID,
         Owner: vm.v.Owner,
@@ -100,9 +100,9 @@ func (vm *VM)SerializeToDisk() vmc.VMDiskSerialized {
     }
 }
 
-// Returns a serialized version (to be sent to the network) of the VM.
+// Returns a serialized version of the VM with most fields except credentials.
 // Concurrency: safe
-func (vm *VM)SerializeToNetwork() vmc.VMNetworkSerialized {
+func (vm *VM)Serialize() vmc.VMNetworkSerialized {
     return vmc.VMNetworkSerialized {
         ID: vm.v.ID,
         Owner: vm.v.Owner,
@@ -114,9 +114,18 @@ func (vm *VM)SerializeToNetwork() vmc.VMNetworkSerialized {
         TemplateID: vm.v.TemplateID,
         Access: vm.v.Access,
         State: vm.Run.State,
+        DiskBusy: vm.DiskBusy,
+    }
+}
+
+// Returns a serialized version of the VM with basic fields and credentials.
+// Concurrency: safe
+func (vm *VM)SerializeCredentials() vmc.VMCredentialsSerialized {
+    return vmc.VMCredentialsSerialized {
+        ID: vm.v.ID,
+        Name: vm.v.Name,
         Port: vm.Run.Port,
         Pwd: vm.Run.Pwd,
-        DiskBusy: vm.DiskBusy,
     }
 }
 
@@ -294,7 +303,7 @@ func (vm *VM)writeConfig() error {
     encoder := json.NewEncoder(file)
     encoder.SetIndent("", "    ")
 
-    if err = encoder.Encode(vm.SerializeToDisk()); err != nil {
+    if err = encoder.Encode(vm.SerializeForDisk()); err != nil {
         msg := "Failed encoding VM config file: "+err.Error()
         log.Error(msg)
         return errors.New(msg)
diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go
index dbba5a5..2844bdb 100644
--- a/src/server/vms/vms.go
+++ b/src/server/vms/vms.go
@@ -108,7 +108,29 @@ func (vms *VMs)GetNetworkSerializedVMs(keepFn VMKeeperFn) []vmc.VMNetworkSeriali
     for _, vm := range vms.m {
         vm.mutex.Lock()
         if keepFn(vm) {
-            list = append(list, vm.SerializeToNetwork())
+            list = append(list, vm.Serialize())
+        }
+        vm.mutex.Unlock()
+    }
+    vms.rwlock.RUnlock()
+
+    // Sort VMs by names
+    sort.Slice(list, func(i, j int) bool {
+        return list[i].Name < list[j].Name
+    })
+    return list
+}
+
+// Returns the list of serialized VM credentials for which VMKeeperFn is true.
+// Concurrency: safe
+func (vms *VMs)GetNetworkSerializedVMCredentials(keepFn VMKeeperFn) []vmc.VMCredentialsSerialized {
+    vms.rwlock.RLock()
+
+    list := []vmc.VMCredentialsSerialized{}
+    for _, vm := range vms.m {
+        vm.mutex.Lock()
+        if keepFn(vm) {
+            list = append(list, vm.SerializeCredentials())
         }
         vm.mutex.Unlock()
     }
-- 
GitLab