add resume urls at the api level

This commit is contained in:
Ruben Fiszel
2022-12-02 22:19:15 +01:00
parent 5ae0e5da19
commit cc037458ca
7 changed files with 145 additions and 76 deletions
+7
View File
@@ -3829,6 +3829,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "uuid"
version = "1.2.1"
@@ -4120,6 +4126,7 @@ dependencies = [
"tower-http",
"tracing",
"tracing-subscriber",
"urlencoding",
"windmill-audit",
"windmill-common",
"windmill-parser",
+2 -1
View File
@@ -64,4 +64,5 @@ tokio-util.workspace = true
tokio-tar.workspace = true
hmac.workspace = true
cookie.workspace = true
sha2.workspace = true
sha2.workspace = true
urlencoding.workspace = true
+37
View File
@@ -2887,6 +2887,43 @@ paths:
schema:
type: string
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- name: resume_id
in: path
required: true
schema:
type: integer
- name: approver
in: query
schema:
type: string
responses:
"200":
description: url endpoints
content:
application/json:
schema:
type: object
properties:
approvalPage:
type: string
resume:
type: string
cancel:
type: string
required:
- approvalPage
- resume
- cancel
/w/{workspace}/jobs/resume/{id}/{resume_id}/{signature}:
get:
summary: resume a job for a suspended flow
+65 -2
View File
@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use anyhow::Context;
use axum::{
extract::{FromRequest, Path, Query},
@@ -18,6 +20,7 @@ use hyper::StatusCode;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sql_builder::{prelude::*, quote, SqlBuilder};
use sqlx::{query_scalar, types::Uuid, Postgres, Transaction};
use urlencoding::encode;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error},
@@ -34,6 +37,7 @@ use crate::{
db::{UserDB, DB},
users::Authed,
variables::get_workspace_key,
BaseUrl,
};
pub fn workspaced_service() -> Router {
@@ -64,6 +68,7 @@ pub fn workspaced_service() -> Router {
"/job_signature/:job_id/:resume_id",
get(create_job_signature),
)
.route("/resume_urls/:job_id/:resume_id", get(get_resume_urls))
.route("/result_by_id/:job_id/:node_id", get(get_result_by_id))
}
@@ -662,16 +667,74 @@ pub async fn create_job_signature(
Query(approver): Query<QueryApprover>,
) -> error::Result<String> {
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
create_signature(key, job_id, resume_id, approver.approver)
}
fn create_signature(
key: String,
job_id: Uuid,
resume_id: u32,
approver: Option<String>,
) -> Result<String, Error> {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?;
mac.update(job_id.as_bytes());
mac.update(resume_id.to_be_bytes().as_ref());
tracing::info!("approver: {:?}", approver.approver);
if let Some(approver) = approver.approver {
if let Some(approver) = approver {
mac.update(approver.as_bytes());
}
Ok(hex::encode(mac.finalize().into_bytes()))
}
#[allow(non_snake_case)]
#[derive(Serialize)]
pub struct ResumeUrls {
approvalPage: String,
cancel: String,
resume: String,
}
fn build_resume_url(
op: &str,
w_id: &str,
job_id: &Uuid,
resume_id: &u32,
signature: &str,
approver: &str,
base_url: &str,
) -> String {
format!("{base_url}/api/w/{w_id}/jobs/{op}/{job_id}/{resume_id}/{signature}{approver}")
}
pub async fn get_resume_urls(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> error::JsonResult<ResumeUrls> {
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?;
let base_url = base_url.0.clone();
let approver = approver
.approver
.as_ref()
.map(|x| format!("?approver={}", encode(x)))
.unwrap_or_else(String::new);
let res = ResumeUrls {
approvalPage: format!(
"{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}"
),
cancel: build_resume_url(
"cancel", &w_id, &job_id, &resume_id, &signature, &approver, &base_url,
),
resume: build_resume_url(
"resume", &w_id, &job_id, &resume_id, &signature, &approver, &base_url,
),
};
Ok(Json(res))
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum Job {
+1 -1
View File
@@ -46,7 +46,7 @@ mod workspaces;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
struct BaseUrl(String);
pub struct BaseUrl(String);
pub struct IsSecure(bool);
pub struct CookieDomain(Option<String>);
pub struct CloudHosted(bool);
+15 -72
View File
@@ -1,4 +1,4 @@
import { ResourceService, VariableService } from './windmill-api/index.ts'
import { JobService, ResourceService, VariableService } from './windmill-api/index.ts'
import { OpenAPI } from './windmill-api/index.ts'
export {
@@ -168,87 +168,30 @@ export async function databaseUrlFromResource(path: string): Promise<string> {
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`
}
export interface NonceAndHmac {
nonce: number;
signature: string;
}
/**
* Get HMAC and nonce needed for approval script
* @param workspace workspace name
* @param jobId
* Get URLs needed for resuming a flow after this step
* @param approver approver name
* @returns HMAC and nonce needed to authorize approval script actions
* @returns approval page UI URL, resume and cancel API URLs for resumeing the flow
*/
export async function genNounceAndHmac(workspace: string, jobId: string, approver?: string): Promise<NonceAndHmac> {
const nonce = Math.floor(Math.random() * 4294967295);
const u = new URL(
`/api/w/${workspace}/jobs/job_signature/${jobId}/${nonce}`,
Deno.env.get("WM_BASE_URL"),
);
u.searchParams.append('token', Deno.env.get("WM_TOKEN") ?? '');
if (approver) {
u.searchParams.append('approver', approver);
}
const sig = await fetch(u.toString());
return {
nonce,
signature: await sig.text()
};
}
export interface ResumeEndpoints {
export async function getResumeUrls(approver?: string): Promise<{
approvalPage: string;
resume: string;
cancel: string;
}> {
const nonce = Math.floor(Math.random() * 4294967295);
const workspace = getWorkspace()
return await JobService.getResumeUrls({ workspace, resumeId: nonce, approver, id: Deno.env.get("WM_JOB_ID") ?? 'NO_JOB_ID' })
}
/**
* Get URLs needed for approval script
* @param approver approver name
* @returns approval page UI URL, resume and cancel API URLs for approval script
* @deprecated use getResumeUrls instead
*/
export async function getResumeEndpoints(approver?: string): Promise<ResumeEndpoints> {
const workspace = getWorkspace()
const { nonce, signature } = await genNounceAndHmac(
workspace,
Deno.env.get("WM_JOB_ID") ?? "no_job_id",
approver
);
function getResumeUrl(op: string): string {
const u = new URL(
`${op}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`,
Deno.env.get("WM_BASE_URL") + `/api/w/${workspace}/jobs/`,
);
if (approver) {
u.searchParams.append('approver', approver);
}
return u.toString();
}
function getApprovalPage() {
const u = new URL(
`/approve/${workspace}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`,
Deno.env.get("WM_BASE_URL"),
);
if (approver) {
u.searchParams.append('approver', approver);
}
return u.toString();
}
return {
approvalPage: getApprovalPage(),
resume: getResumeUrl("resume"),
cancel: getResumeUrl("cancel"),
};
export function getResumeEndpoints(approver?: string): Promise<{
approvalPage: string;
resume: string;
cancel: string;
}> {
return getResumeUrls(approver)
}
export function base64ToUint8Array(data: string): Uint8Array {
+18
View File
@@ -327,6 +327,24 @@ def get_state_path() -> str:
return state_path
def get_resume_urls(approver: str | None = None) -> Dict:
from windmill_api.api.job import get_resume_urls as get_resume_urls_api
workspace = get_workspace()
client = create_client()
job_id = os.environ.get("WM_JOB_ID") or "NO_ID"
import random
nonce = random.randint(0, 1000000000)
res = get_resume_urls_api.sync_detailed(
workspace, job_id, nonce, client=client, approver=approver
)
if res.parsed is not None:
return res.parsed.to_dict()
else:
raise Exception("Failed to get resume urls")
def _transform_leaves(d: Dict[str, Any]) -> Dict[str, Any]:
return {k: _transform_leaf(v) for k, v in d.items()}