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