Centralize rights management
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		@@ -29,7 +29,7 @@ pub struct AppConfig {
 | 
			
		||||
    #[arg(
 | 
			
		||||
        long,
 | 
			
		||||
        env,
 | 
			
		||||
        default_value = "http://localhost:9001/.well-known/openid-configuration"
 | 
			
		||||
        default_value = "http://localhost:9001/dex/.well-known/openid-configuration"
 | 
			
		||||
    )]
 | 
			
		||||
    pub oidc_configuration_url: String,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
use crate::virtweb_client;
 | 
			
		||||
use crate::virtweb_client::VMUuid;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
@@ -15,3 +17,59 @@ pub async fn config(auth: AuthExtractor) -> HttpResult {
 | 
			
		||||
        disable_auth: AppConfig::get().unsecure_disable_login,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug, serde::Serialize)]
 | 
			
		||||
pub struct Rights {
 | 
			
		||||
    vms: Vec<VMInfoAndCaps>,
 | 
			
		||||
    sys_info: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, serde::Serialize)]
 | 
			
		||||
pub struct VMInfoAndCaps {
 | 
			
		||||
    uiid: VMUuid,
 | 
			
		||||
    name: String,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    architecture: String,
 | 
			
		||||
    memory: usize,
 | 
			
		||||
    number_vcpu: usize,
 | 
			
		||||
    can_get_state: bool,
 | 
			
		||||
    can_start: bool,
 | 
			
		||||
    can_shutdown: bool,
 | 
			
		||||
    can_kill: bool,
 | 
			
		||||
    can_reset: bool,
 | 
			
		||||
    can_suspend: bool,
 | 
			
		||||
    can_resume: bool,
 | 
			
		||||
    can_screenshot: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn rights() -> HttpResult {
 | 
			
		||||
    let rights = virtweb_client::get_token_info().await?;
 | 
			
		||||
 | 
			
		||||
    let mut res = Rights {
 | 
			
		||||
        vms: vec![],
 | 
			
		||||
        sys_info: rights.can_retrieve_system_info(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    for v in rights.list_vm() {
 | 
			
		||||
        let vm_info = virtweb_client::vm_info(v).await?;
 | 
			
		||||
 | 
			
		||||
        res.vms.push(VMInfoAndCaps {
 | 
			
		||||
            uiid: vm_info.uuid,
 | 
			
		||||
            name: vm_info.name,
 | 
			
		||||
            description: vm_info.description.clone(),
 | 
			
		||||
            architecture: vm_info.architecture.to_string(),
 | 
			
		||||
            memory: vm_info.memory,
 | 
			
		||||
            number_vcpu: vm_info.number_vcpu,
 | 
			
		||||
            can_get_state: rights.is_route_allowed("GET", &v.route_state()),
 | 
			
		||||
            can_start: rights.is_route_allowed("GET", &v.route_start()),
 | 
			
		||||
            can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()),
 | 
			
		||||
            can_kill: rights.is_route_allowed("GET", &v.route_kill()),
 | 
			
		||||
            can_reset: rights.is_route_allowed("GET", &v.route_reset()),
 | 
			
		||||
            can_suspend: rights.is_route_allowed("GET", &v.route_suspend()),
 | 
			
		||||
            can_resume: rights.is_route_allowed("GET", &v.route_resume()),
 | 
			
		||||
            can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(res))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,20 +2,6 @@ use crate::controllers::HttpResult;
 | 
			
		||||
use crate::virtweb_client;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct SysInfoStatus {
 | 
			
		||||
    allowed: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Check if system info can be retrieved
 | 
			
		||||
pub async fn config() -> HttpResult {
 | 
			
		||||
    let info = virtweb_client::get_token_info().await?;
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(SysInfoStatus {
 | 
			
		||||
        allowed: info.can_retrieve_system_info(),
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get current system status
 | 
			
		||||
pub async fn status() -> HttpResult {
 | 
			
		||||
    Ok(HttpResponse::Ok().json(virtweb_client::get_server_info().await?))
 | 
			
		||||
 
 | 
			
		||||
@@ -5,54 +5,6 @@ use crate::virtweb_client;
 | 
			
		||||
use crate::virtweb_client::VMUuid;
 | 
			
		||||
use actix_web::{web, HttpResponse};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, serde::Serialize)]
 | 
			
		||||
pub struct VMInfoAndCaps {
 | 
			
		||||
    uiid: VMUuid,
 | 
			
		||||
    name: String,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    architecture: String,
 | 
			
		||||
    memory: usize,
 | 
			
		||||
    number_vcpu: usize,
 | 
			
		||||
    can_get_state: bool,
 | 
			
		||||
    can_start: bool,
 | 
			
		||||
    can_shutdown: bool,
 | 
			
		||||
    can_kill: bool,
 | 
			
		||||
    can_reset: bool,
 | 
			
		||||
    can_suspend: bool,
 | 
			
		||||
    can_resume: bool,
 | 
			
		||||
    can_screenshot: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get the list of VMs that can be controlled by VirtWeb remote
 | 
			
		||||
pub async fn list() -> HttpResult {
 | 
			
		||||
    let rights = virtweb_client::get_token_info().await?;
 | 
			
		||||
 | 
			
		||||
    let mut res = vec![];
 | 
			
		||||
 | 
			
		||||
    for v in rights.list_vm() {
 | 
			
		||||
        let vm_info = virtweb_client::vm_info(v).await?;
 | 
			
		||||
 | 
			
		||||
        res.push(VMInfoAndCaps {
 | 
			
		||||
            uiid: vm_info.uuid,
 | 
			
		||||
            name: vm_info.name,
 | 
			
		||||
            description: vm_info.description.clone(),
 | 
			
		||||
            architecture: vm_info.architecture.to_string(),
 | 
			
		||||
            memory: vm_info.memory,
 | 
			
		||||
            number_vcpu: vm_info.number_vcpu,
 | 
			
		||||
            can_get_state: rights.is_route_allowed("GET", &v.route_state()),
 | 
			
		||||
            can_start: rights.is_route_allowed("GET", &v.route_start()),
 | 
			
		||||
            can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()),
 | 
			
		||||
            can_kill: rights.is_route_allowed("GET", &v.route_kill()),
 | 
			
		||||
            can_reset: rights.is_route_allowed("GET", &v.route_reset()),
 | 
			
		||||
            can_suspend: rights.is_route_allowed("GET", &v.route_suspend()),
 | 
			
		||||
            can_resume: rights.is_route_allowed("GET", &v.route_resume()),
 | 
			
		||||
            can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(res))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
pub struct ReqPath {
 | 
			
		||||
    uid: VMUuid,
 | 
			
		||||
 
 | 
			
		||||
@@ -82,8 +82,11 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/auth/sign_out",
 | 
			
		||||
                web::get().to(auth_controller::sign_out),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/server/rights",
 | 
			
		||||
                web::get().to(server_controller::rights),
 | 
			
		||||
            )
 | 
			
		||||
            // VM routes
 | 
			
		||||
            .route("/api/vm/list", web::get().to(vm_controller::list))
 | 
			
		||||
            .route("/api/vm/{uid}/state", web::get().to(vm_controller::state))
 | 
			
		||||
            .route("/api/vm/{uid}/start", web::get().to(vm_controller::start))
 | 
			
		||||
            .route(
 | 
			
		||||
@@ -102,10 +105,6 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                web::get().to(vm_controller::screenshot),
 | 
			
		||||
            )
 | 
			
		||||
            // Sys info routes
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/sysinfo/config",
 | 
			
		||||
                web::get().to(sys_info_controller::config),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/sysinfo/status",
 | 
			
		||||
                web::get().to(sys_info_controller::status),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import {
 | 
			
		||||
  bundleIcon,
 | 
			
		||||
} from "@fluentui/react-icons";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { ServerApi } from "./api/ServerApi";
 | 
			
		||||
import { Rights, ServerApi } from "./api/ServerApi";
 | 
			
		||||
import { AuthRouteWidget } from "./routes/AuthRouteWidget";
 | 
			
		||||
import { AsyncWidget } from "./widgets/AsyncWidget";
 | 
			
		||||
import { MainMenu } from "./widgets/MainMenu";
 | 
			
		||||
@@ -40,12 +40,28 @@ export function App() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function AppInner(): React.ReactElement {
 | 
			
		||||
  const styles = useStyles();
 | 
			
		||||
  const [tab, setTab] = React.useState<"vm" | "info">("vm");
 | 
			
		||||
 | 
			
		||||
  if (!ServerApi.Config.authenticated && !ServerApi.Config.disable_auth)
 | 
			
		||||
    return <AuthRouteWidget />;
 | 
			
		||||
 | 
			
		||||
  return <AuthenticatedApp />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function AuthenticatedApp(): React.ReactElement {
 | 
			
		||||
  const styles = useStyles();
 | 
			
		||||
  const [tab, setTab] = React.useState<"vm" | "info">("vm");
 | 
			
		||||
 | 
			
		||||
  const [rights, setRights] = React.useState<Rights | undefined>();
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setRights(await ServerApi.GetRights());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AsyncWidget
 | 
			
		||||
      loadKey={1}
 | 
			
		||||
      load={load}
 | 
			
		||||
      errMsg="Failed to retrieve application rights!"
 | 
			
		||||
      build={() => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
@@ -66,10 +82,18 @@ function AppInner(): React.ReactElement {
 | 
			
		||||
                selectedValue={tab}
 | 
			
		||||
                onTabSelect={(_, d) => setTab(d.value as any)}
 | 
			
		||||
              >
 | 
			
		||||
          <Tab value="vm" icon={<DesktopIcon />}>
 | 
			
		||||
                <Tab
 | 
			
		||||
                  value="vm"
 | 
			
		||||
                  icon={<DesktopIcon />}
 | 
			
		||||
                  disabled={rights!.vms.length === 0}
 | 
			
		||||
                >
 | 
			
		||||
                  Virtual machines
 | 
			
		||||
                </Tab>
 | 
			
		||||
          <Tab value="info" icon={<InfoIcon />}>
 | 
			
		||||
                <Tab
 | 
			
		||||
                  value="info"
 | 
			
		||||
                  icon={<InfoIcon />}
 | 
			
		||||
                  disabled={!rights!.sys_info}
 | 
			
		||||
                >
 | 
			
		||||
                  System info
 | 
			
		||||
                </Tab>
 | 
			
		||||
              </TabList>
 | 
			
		||||
@@ -77,8 +101,11 @@ function AppInner(): React.ReactElement {
 | 
			
		||||
                <MainMenu />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
      {tab === "vm" && <VirtualMachinesWidget />}
 | 
			
		||||
            {tab === "vm" && <VirtualMachinesWidget rights={rights!} />}
 | 
			
		||||
            {tab === "info" && <SystemInfoWidget />}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,16 @@
 | 
			
		||||
import { APIClient } from "./ApiClient";
 | 
			
		||||
import { VMInfo } from "./VMApi";
 | 
			
		||||
 | 
			
		||||
export interface ServerConfig {
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  disable_auth: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Rights {
 | 
			
		||||
  vms: VMInfo[];
 | 
			
		||||
  sys_info: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let config: ServerConfig | null = null;
 | 
			
		||||
 | 
			
		||||
export class ServerApi {
 | 
			
		||||
@@ -27,4 +33,16 @@ export class ServerApi {
 | 
			
		||||
    if (config === null) throw new Error("Missing configuration!");
 | 
			
		||||
    return config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get application rights
 | 
			
		||||
   */
 | 
			
		||||
  static async GetRights(): Promise<Rights> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        uri: "/server/rights",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,5 @@
 | 
			
		||||
import { APIClient } from "./ApiClient";
 | 
			
		||||
 | 
			
		||||
export interface SysInfoConfig {
 | 
			
		||||
  allowed: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LoadAverage {
 | 
			
		||||
  one: number;
 | 
			
		||||
  five: number;
 | 
			
		||||
@@ -24,14 +20,6 @@ export interface SysInfoStatus {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SysInfoApi {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get system info configuration (ie. check if it allowed)
 | 
			
		||||
   */
 | 
			
		||||
  static async GetConfig(): Promise<SysInfoConfig> {
 | 
			
		||||
    return (await APIClient.exec({ method: "GET", uri: "/sysinfo/config" }))
 | 
			
		||||
      .data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get system status
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -29,13 +29,6 @@ export type VMState =
 | 
			
		||||
  | "Other";
 | 
			
		||||
 | 
			
		||||
export class VMApi {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the list of VM that can be managed by this console
 | 
			
		||||
   */
 | 
			
		||||
  static async GetList(): Promise<VMInfo[]> {
 | 
			
		||||
    return (await APIClient.exec({ method: "GET", uri: "/vm/list" })).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the state of a VM
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +1,13 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { SysInfoApi, SysInfoConfig, SysInfoStatus } from "../api/SysInfoApi";
 | 
			
		||||
import { AsyncWidget } from "./AsyncWidget";
 | 
			
		||||
import { SectionContainer } from "./SectionContainer";
 | 
			
		||||
import { Field, ProgressBar } from "@fluentui/react-components";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import { format_duration } from "../utils/time_utils";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { SysInfoApi, SysInfoStatus } from "../api/SysInfoApi";
 | 
			
		||||
import { useToast } from "../hooks/providers/ToastProvider";
 | 
			
		||||
import { format_duration } from "../utils/time_utils";
 | 
			
		||||
import { AsyncWidget } from "./AsyncWidget";
 | 
			
		||||
import { SectionContainer } from "./SectionContainer";
 | 
			
		||||
 | 
			
		||||
export function SystemInfoWidget(): React.ReactElement {
 | 
			
		||||
  const [config, setConfig] = React.useState<SysInfoConfig | undefined>();
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setConfig(await SysInfoApi.GetConfig());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SectionContainer>
 | 
			
		||||
      <AsyncWidget
 | 
			
		||||
        loadKey={1}
 | 
			
		||||
        load={load}
 | 
			
		||||
        errMsg="Failed to check system configuration!"
 | 
			
		||||
        loadingMessage="Checking server configuration..."
 | 
			
		||||
        build={() =>
 | 
			
		||||
          config?.allowed ? (
 | 
			
		||||
            <SystemInfoWidgetInner />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <SystemInfoWidgetUnavailable />
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </SectionContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SystemInfoWidgetUnavailable(): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <p style={{ textAlign: "center" }}>
 | 
			
		||||
      Unfortunatley, system information is available. (not enough privileges)
 | 
			
		||||
    </p>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SystemInfoWidgetInner(): React.ReactElement {
 | 
			
		||||
  const toast = useToast();
 | 
			
		||||
 | 
			
		||||
  const [status, setStatus] = React.useState<SysInfoStatus | undefined>();
 | 
			
		||||
@@ -63,6 +29,7 @@ function SystemInfoWidgetInner(): React.ReactElement {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SectionContainer>
 | 
			
		||||
      <AsyncWidget
 | 
			
		||||
        loadKey={1}
 | 
			
		||||
        load={load}
 | 
			
		||||
@@ -106,6 +73,7 @@ function SystemInfoWidgetInner(): React.ReactElement {
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
    </SectionContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,39 +21,23 @@ import {
 | 
			
		||||
} from "@fluentui/react-icons";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Rights } from "../api/ServerApi";
 | 
			
		||||
import { VMApi, VMInfo, VMState } from "../api/VMApi";
 | 
			
		||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
import { useToast } from "../hooks/providers/ToastProvider";
 | 
			
		||||
import { AsyncWidget } from "./AsyncWidget";
 | 
			
		||||
import { SectionContainer } from "./SectionContainer";
 | 
			
		||||
import { VMLiveScreenshot } from "./VMLiveScreenshot";
 | 
			
		||||
import { SectionContainer } from "./SectionContainer";
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles({
 | 
			
		||||
  body1Stronger: typographyStyles.body1Stronger,
 | 
			
		||||
  caption1: typographyStyles.caption1,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function VirtualMachinesWidget(): React.ReactElement {
 | 
			
		||||
  const [list, setList] = React.useState<VMInfo[] | undefined>();
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setList(await VMApi.GetList());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
export function VirtualMachinesWidget(p: {
 | 
			
		||||
  rights: Rights;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <SectionContainer>
 | 
			
		||||
      <AsyncWidget
 | 
			
		||||
        loadKey={1}
 | 
			
		||||
        load={load}
 | 
			
		||||
        loadingMessage="Loading the list virtual machines..."
 | 
			
		||||
        errMsg="Failed to load the list of virtual machines!"
 | 
			
		||||
        build={() => <VirtualMachinesWidgetInner list={list!} />}
 | 
			
		||||
      />
 | 
			
		||||
    </SectionContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function VirtualMachinesWidgetInner(p: { list: VMInfo[] }): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
@@ -62,10 +46,11 @@ function VirtualMachinesWidgetInner(p: { list: VMInfo[] }): React.ReactElement {
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
      {p.list.map((v, n) => (
 | 
			
		||||
        {p.rights.vms.map((v, n) => (
 | 
			
		||||
          <VMWidget key={n} vm={v} />
 | 
			
		||||
      ))}{" "}
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </SectionContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user