]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/server/rest.rs
change index to templates using handlebars
[proxmox-backup.git] / src / server / rest.rs
index 8d82f855d421922921fc709536835d462d599179..95f731913a10bb86477b87261e5e3d1f94bcf009 100644 (file)
@@ -6,7 +6,7 @@ use std::pin::Pin;
 use std::sync::Arc;
 use std::task::{Context, Poll};
 
-use failure::*;
+use anyhow::{bail, format_err, Error};
 use futures::future::{self, FutureExt, TryFutureExt};
 use futures::stream::TryStreamExt;
 use hyper::header;
@@ -16,10 +16,11 @@ use serde_json::{json, Value};
 use tokio::fs::File;
 use tokio::time::Instant;
 use url::form_urlencoded;
+use handlebars::Handlebars;
 
-use proxmox::api::http_err;
+use proxmox::http_err;
 use proxmox::api::{ApiHandler, ApiMethod, HttpError};
-use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
+use proxmox::api::{RpcEnvironment, RpcEnvironmentType, check_api_permission};
 use proxmox::api::schema::{ObjectSchema, parse_simple_value, verify_json_object, parse_parameter_strings};
 
 use super::environment::RestEnvironment;
@@ -28,6 +29,7 @@ use super::ApiConfig;
 
 use crate::auth_helpers::*;
 use crate::tools;
+use crate::config::cached_user_info::CachedUserInfo;
 
 extern "C"  { fn tzset(); }
 
@@ -310,58 +312,43 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
     Ok(resp)
 }
 
-fn get_index(username: Option<String>, token: Option<String>) ->  Response<Body> {
+fn get_index(username: Option<String>, token: Option<String>, template: &Handlebars, parts: Parts) ->  Response<Body> {
 
     let nodename = proxmox::tools::nodename();
     let username = username.unwrap_or_else(|| String::from(""));
 
     let token = token.unwrap_or_else(|| String::from(""));
 
-    let setup = json!({
-        "Setup": { "auth_cookie_name": "PBSAuthCookie" },
+    let mut debug = false;
+
+    if let Some(query_str) = parts.uri.query() {
+        for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
+            if k == "debug" && v == "1" || v == "true" {
+                debug = true;
+            }
+        }
+    }
+
+    let data = json!({
         "NodeName": nodename,
         "UserName": username,
         "CSRFPreventionToken": token,
+        "debug": debug,
     });
 
-    let index = format!(r###"
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-    <title>Proxmox Backup Server</title>
-    <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
-    <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
-    <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
-    <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
-    <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
-    <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
-    <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
-    <script type="text/javascript" src="/extjs/charts-debug.js"></script>
-    <script type="text/javascript">
-      Proxmox = {};
-    </script>
-    <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
-    <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
-    <script type="text/javascript">
-      Ext.History.fieldid = 'x-history-field';
-    </script>
-    <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
-  </head>
-  <body>
-    <!-- Fields required for history management -->
-    <form id="history-form" class="x-hidden">
-      <input type="hidden" id="x-history-field"/>
-    </form>
-  </body>
-</html>
-"###, setup.to_string());
+    let mut ct = "text/html";
+
+    let index = match template.render("index", &data) {
+        Ok(index) => index,
+        Err(err) => {
+            ct = "text/plain";
+            format!("Error rendering template: {}", err.desc)
+        },
+    };
 
     Response::builder()
         .status(StatusCode::OK)
-        .header(header::CONTENT_TYPE, "text/html")
+        .header(header::CONTENT_TYPE, ct)
         .body(index.into())
         .unwrap()
 }
@@ -439,16 +426,15 @@ async fn chuncked_static_file_download(filename: PathBuf) -> Result<Response<Bod
 
 async fn handle_static_file_download(filename: PathBuf) ->  Result<Response<Body>, Error> {
 
-    tokio::fs::metadata(filename.clone())
+    let metadata = tokio::fs::metadata(filename.clone())
         .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
-        .and_then(|metadata| async move {
-            if metadata.len() < 1024*32 {
-                simple_static_file_download(filename).await
-            } else {
-                chuncked_static_file_download(filename).await
-            }
-        })
-        .await
+        .await?;
+
+    if metadata.len() < 1024*32 {
+        simple_static_file_download(filename).await
+    } else {
+        chuncked_static_file_download(filename).await
+    }
 }
 
 fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
@@ -468,7 +454,12 @@ fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<Strin
     (ticket, token)
 }
 
-fn check_auth(method: &hyper::Method, ticket: &Option<String>, token: &Option<String>) -> Result<String, Error> {
+fn check_auth(
+    method: &hyper::Method,
+    ticket: &Option<String>,
+    token: &Option<String>,
+    user_info: &CachedUserInfo,
+) -> Result<String, Error> {
 
     let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
 
@@ -481,6 +472,10 @@ fn check_auth(method: &hyper::Method, ticket: &Option<String>, token: &Option<St
         None => bail!("missing ticket"),
     };
 
+    if !user_info.is_active_user(&username) {
+        bail!("user account disabled or expired.");
+    }
+
     if method != hyper::Method::GET {
         if let Some(token) = token {
             println!("CSRF prevention token: {:?}", token);
@@ -508,7 +503,10 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
     let env_type = api.env_type();
     let mut rpcenv = RestEnvironment::new(env_type);
 
+    let user_info = CachedUserInfo::new()?;
+
     let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
+    let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500);
 
     if comp_len >= 1 && components[0] == "api2" {
 
@@ -524,20 +522,18 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
 
             let mut uri_param = HashMap::new();
 
-            if comp_len == 4 && components[2] == "access" && components[3] == "ticket" {
+            if comp_len == 4 && components[2] == "access" && (
+                (components[3] == "ticket" && method ==  hyper::Method::POST) ||
+                (components[3] == "domains" && method ==  hyper::Method::GET)
+            ) {
                 // explicitly allow those calls without auth
             } else {
                 let (ticket, token) = extract_auth_data(&parts.headers);
-                match check_auth(&method, &ticket, &token) {
-                    Ok(username) => {
-
-                        // fixme: check permissions
-
-                        rpcenv.set_user(Some(username));
-                    }
+                match check_auth(&method, &ticket, &token, &user_info) {
+                    Ok(username) => rpcenv.set_user(Some(username)),
                     Err(err) => {
                         // always delay unauthorized calls by 3 seconds (from start of request)
-                        let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err));
+                        let err = http_err!(UNAUTHORIZED, format!("authentication failed - {}", err));
                         tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
                         return Ok((formatter.format_error)(err));
                     }
@@ -550,13 +546,26 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
                     return Ok((formatter.format_error)(err));
                 }
                 Some(api_method) => {
-                    if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
-                        return proxy_protected_request(api_method, parts, body).await;
+                    let user = rpcenv.get_user();
+                    if !check_api_permission(api_method.access.permission, user.as_deref(), &uri_param, user_info.as_ref()) {
+                        let err = http_err!(FORBIDDEN, format!("permission check failed"));
+                        tokio::time::delay_until(Instant::from_std(access_forbidden_time)).await;
+                        return Ok((formatter.format_error)(err));
+                    }
+
+                    let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
+                        proxy_protected_request(api_method, parts, body).await
                     } else {
-                        return handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await;
+                        handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await
+                    };
+
+                    if let Err(err) = result {
+                        return Ok((formatter.format_error)(err));
                     }
+                    return result;
                 }
             }
+
         }
      } else {
         // not Auth required for accessing files!
@@ -568,18 +577,18 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
         if comp_len == 0 {
             let (ticket, token) = extract_auth_data(&parts.headers);
             if ticket != None {
-                match check_auth(&method, &ticket, &token) {
+                match check_auth(&method, &ticket, &token, &user_info) {
                     Ok(username) => {
                         let new_token = assemble_csrf_prevention_token(csrf_secret(), &username);
-                        return Ok(get_index(Some(username), Some(new_token)));
+                        return Ok(get_index(Some(username), Some(new_token), &api.templates, parts));
                     }
                     _ => {
                         tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
-                        return Ok(get_index(None, None));
+                        return Ok(get_index(None, None, &api.templates, parts));
                     }
                 }
             } else {
-                return Ok(get_index(None, None));
+                return Ok(get_index(None, None, &api.templates, parts));
             }
         } else {
             let filename = api.find_alias(&components);