mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
tighten public apps security using triggerable policy
This commit is contained in:
+80
-59
@@ -317,6 +317,45 @@
|
||||
},
|
||||
"query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2"
|
||||
},
|
||||
"0f6af40e79a3f44ce84bd127a8af9216c82e71acf8bf4df12b078020c90d7d9d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "schema",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter' OR workspace_id = 'admins')"
|
||||
},
|
||||
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1839,6 +1878,27 @@
|
||||
},
|
||||
"query": "SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"5767e6a8f94f571a78bed0c0423882060c7d417fdabb30cff6c86cb1c5f46df9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter' OR workspace_id = 'admins'))"
|
||||
},
|
||||
"576b00c515ee7cbf628b1881596d8a03c1c506b309e39a0048a3a8fe18f37070": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1873,45 +1933,6 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)"
|
||||
},
|
||||
"58efbf34ba014b4853ef20ae400929b57c1d9de4262189274badf100d29e0649": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "schema",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')"
|
||||
},
|
||||
"59fc51efa01e823ec63f55a8081f282db0d7a40716c04f4043925eb84fe4429b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2108,26 +2129,6 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)"
|
||||
},
|
||||
"63c4b9320681fac84ea92c25c0f6da5c9ac154dfccf575cea8145692246205c4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name"
|
||||
},
|
||||
"63f330e98051ae9d0cf5617a553f769cc98b05fdc8643839b504023b26f38aab": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -5114,6 +5115,26 @@
|
||||
},
|
||||
"query": "INSERT INTO usr_to_group\n VALUES ($1, 'all', $2)"
|
||||
},
|
||||
"e3fbad80527771f63390fb1ab203bd3d0c058627b92f32f5c6f3e2856ab24e47": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') ORDER BY name"
|
||||
},
|
||||
"e5231634fbb37d45e5cf3e5dfb39cb829d224caad1c3ca1712d0fc4c495aa4fc": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -5399,6 +5399,8 @@ components:
|
||||
enum: [viewer, publisher, anonymous]
|
||||
on_behalf_of:
|
||||
type: string
|
||||
on_behalf_of_email:
|
||||
type: string
|
||||
|
||||
ListableApp:
|
||||
type: object
|
||||
|
||||
@@ -631,7 +631,7 @@ fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> {
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"on_behalf_of is missing in the app policy and is required for anonymous execution"
|
||||
"on_behalf_of_email is missing in the app policy and is required for anonymous execution"
|
||||
.to_string(),
|
||||
)
|
||||
})?
|
||||
|
||||
@@ -62,7 +62,6 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/completed/get/:id", get(get_completed_job))
|
||||
.route("/completed/get_result/:id", get(get_completed_job_result))
|
||||
.route("/completed/delete/:id", post(delete_completed_job))
|
||||
.route("/get/:id", get(get_job))
|
||||
.route("/flow/resume/:id", post(resume_suspended_flow_as_owner))
|
||||
.route("/getupdate/:id", get(get_job_update))
|
||||
.route(
|
||||
@@ -95,6 +94,7 @@ pub fn global_service() -> Router {
|
||||
"/get_flow/:job_id/:resume_id/:secret",
|
||||
get(get_suspended_job_flow),
|
||||
)
|
||||
.route("/get/:id", get(get_job))
|
||||
}
|
||||
|
||||
async fn get_result_by_id(
|
||||
|
||||
@@ -461,7 +461,7 @@ async fn list_resource_types_names(
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let rows = sqlx::query_scalar!(
|
||||
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') \
|
||||
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') \
|
||||
ORDER BY name",
|
||||
&w_id
|
||||
)
|
||||
@@ -481,7 +481,7 @@ async fn get_resource_type(
|
||||
let resource_type_o = sqlx::query_as!(
|
||||
ResourceType,
|
||||
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
'starter' OR workspace_id = 'admins')",
|
||||
&name,
|
||||
&w_id
|
||||
)
|
||||
@@ -498,7 +498,8 @@ async fn exists_resource_type(
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> JsonResult<bool> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)",
|
||||
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter' OR workspace_id = 'admins'))",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -108,11 +108,13 @@
|
||||
intervalId = undefined
|
||||
stopCurrentIteration = true
|
||||
if (isLoading && job) {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: workspace!,
|
||||
id: job.id,
|
||||
requestBody: {}
|
||||
})
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: workspace!,
|
||||
id: job.id,
|
||||
requestBody: {}
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
await clearIntervalAsync(interval)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import { fieldTypeToTsType, schemaToInputsSpec } from '../../utils'
|
||||
import InputValue from './InputValue.svelte'
|
||||
import MissingConnectionWarning from './MissingConnectionWarning.svelte'
|
||||
import RefreshButton from './RefreshButton.svelte'
|
||||
|
||||
// Component props
|
||||
@@ -23,7 +22,7 @@
|
||||
export let result: any = undefined
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
|
||||
const { worldStore, runnableComponents, workspace, appPath } =
|
||||
const { worldStore, runnableComponents, workspace, appPath, mode } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
onMount(() => {
|
||||
@@ -36,65 +35,19 @@
|
||||
})
|
||||
|
||||
let args: Record<string, any> = {}
|
||||
let debouncedArgs: Record<string, any> = args
|
||||
let testIsLoading = false
|
||||
let runnableInputValues: Record<string, any> = {}
|
||||
|
||||
let argsTimeout: NodeJS.Timeout | undefined
|
||||
let executeTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function setDebouncedArgs() {
|
||||
argsTimeout && clearTimeout(argsTimeout)
|
||||
argsTimeout = setTimeout(() => {
|
||||
Object.assign(args, debouncedArgs)
|
||||
args = args
|
||||
function setDebouncedExecute() {
|
||||
executeTimeout && clearTimeout(executeTimeout)
|
||||
executeTimeout = setTimeout(() => {
|
||||
executeComponent()
|
||||
}, 200)
|
||||
}
|
||||
$: debouncedArgs && setDebouncedArgs()
|
||||
|
||||
let previousStaticArgs: any = {}
|
||||
function setStaticInputsToArgs() {
|
||||
let nargs = {}
|
||||
Object.entries(fields ?? {}).forEach(([key, value]) => {
|
||||
if (value.type === 'static') {
|
||||
nargs[key] = value.value
|
||||
}
|
||||
})
|
||||
|
||||
if (JSON.stringify(previousStaticArgs) != JSON.stringify(nargs)) {
|
||||
previousStaticArgs = nargs
|
||||
Object.assign(args, nargs)
|
||||
args = args
|
||||
}
|
||||
}
|
||||
|
||||
$: fields && setStaticInputsToArgs()
|
||||
|
||||
function argMergedArgsValid(mergedArgs: Record<string, any>, testJobLoader) {
|
||||
if (!fields) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(fields).length !==
|
||||
Object.keys(mergedArgs).length - Object.keys(extraQueryParams).length
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const areAllArgsValid = Object.values(mergedArgs).every(
|
||||
(arg) => arg !== undefined && arg !== null
|
||||
)
|
||||
|
||||
if (areAllArgsValid && autoRefresh && testJobLoader) {
|
||||
executeComponent()
|
||||
}
|
||||
|
||||
return areAllArgsValid
|
||||
}
|
||||
|
||||
$: isValid =
|
||||
Object.keys(fields ?? {}).length == 0 ||
|
||||
argMergedArgsValid({ ...extraQueryParams, ...runnableInputValues, ...args }, testJobLoader)
|
||||
$: fields && runnableInputValues && args && autoRefresh && testJobLoader && setDebouncedExecute()
|
||||
|
||||
// Test job internal state
|
||||
let testJob: CompletedJob | undefined = undefined
|
||||
@@ -109,6 +62,8 @@
|
||||
outputs.loading.set(false, true)
|
||||
}
|
||||
|
||||
$: outputs?.loading?.set(testIsLoading)
|
||||
|
||||
$: runnable?.type === 'runnableByName' && loadSchemaAndInputsByName()
|
||||
|
||||
async function loadSchemaAndInputsByName() {
|
||||
@@ -132,10 +87,10 @@
|
||||
// When the schema is loaded, we need to update the inputs spec
|
||||
// in order to render the inputs the component panel
|
||||
function reloadInputs(schema: Schema) {
|
||||
let schemaWithoutExtraQueries: Schema = JSON.parse(JSON.stringify(schema))
|
||||
let schemaCopy: Schema = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
const result = {}
|
||||
const newInputs = schemaToInputsSpec(schemaWithoutExtraQueries)
|
||||
const newInputs = schemaToInputsSpec(schemaCopy)
|
||||
if (!fields) {
|
||||
return newInputs
|
||||
}
|
||||
@@ -207,9 +162,19 @@
|
||||
outputs?.loading?.set(true)
|
||||
|
||||
await testJobLoader?.abstractRun(() => {
|
||||
const nonStaticRunnableInputs = {}
|
||||
const staticRunnableInputs = {}
|
||||
Object.keys(fields ?? {}).forEach(([k, v]) => {
|
||||
let field = fields[k]
|
||||
if (field.type == 'static') {
|
||||
staticRunnableInputs[k] = field.value
|
||||
} else {
|
||||
nonStaticRunnableInputs[k] = runnableInputValues[k]
|
||||
}
|
||||
}, {})
|
||||
const requestBody = {
|
||||
args: { ...args, ...runnableInputValues },
|
||||
force_viewer_static_fields: {}
|
||||
args: { ...nonStaticRunnableInputs, ...args },
|
||||
force_viewer_static_fields: $mode == 'preview' ? undefined : staticRunnableInputs
|
||||
}
|
||||
|
||||
if (runnable?.type === 'runnableByName') {
|
||||
@@ -240,13 +205,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each Object.keys(fields ?? {}) as key}
|
||||
<InputValue
|
||||
{id}
|
||||
input={fields[key]}
|
||||
bind:value={runnableInputValues[key]}
|
||||
row={extraQueryParams['row'] ?? {}}
|
||||
/>
|
||||
{#each Object.entries(fields ?? {}) as [key, v]}
|
||||
{#if v.type != 'static' && v.type != 'user'}
|
||||
<InputValue
|
||||
{id}
|
||||
input={fields[key]}
|
||||
bind:value={runnableInputValues[key]}
|
||||
row={extraQueryParams['row'] ?? {}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<TestJobLoader
|
||||
@@ -254,7 +221,6 @@
|
||||
on:done={() => {
|
||||
if (testJob && outputs) {
|
||||
outputs.result?.set(testJob?.result)
|
||||
outputs.loading?.set(false)
|
||||
result = testJob.result
|
||||
}
|
||||
}}
|
||||
@@ -268,8 +234,7 @@
|
||||
<div class="px-2">
|
||||
<SchemaForm
|
||||
schema={schemaStripped}
|
||||
bind:args={debouncedArgs}
|
||||
{isValid}
|
||||
bind:args
|
||||
{disabledArgs}
|
||||
shouldHideNoInputs
|
||||
noVariablePicker
|
||||
@@ -286,19 +251,7 @@
|
||||
<RefreshButton componentId={id} />
|
||||
</div>
|
||||
|
||||
{#if isValid}
|
||||
<slot />
|
||||
{:else}
|
||||
<Alert type="info" size="xs" class="mt-2 px-1" title="Missing inputs">
|
||||
Please fill in all the inputs
|
||||
|
||||
{#each Object.keys(fields ?? {}) as key}
|
||||
{#if fields[key].type === 'connected'}
|
||||
<MissingConnectionWarning input={fields[key]} />
|
||||
{/if}
|
||||
{/each}
|
||||
</Alert>
|
||||
{/if}
|
||||
<slot />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
$: $appStore && saveDraft()
|
||||
|
||||
function saveDraft() {
|
||||
console.log('save')
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(() => localStorage.setItem('app', encodeState($appStore)), 500)
|
||||
}
|
||||
@@ -121,7 +120,9 @@
|
||||
<SplitPanesWrapper horizontal>
|
||||
<Pane size={70}>
|
||||
<div
|
||||
class="bg-gray-100 w-full h-full overflow-auto {app.fullscreen ? '' : 'max-w-6xl'}"
|
||||
class="bg-gray-100 relative w-full h-full overflow-auto {app.fullscreen
|
||||
? ''
|
||||
: 'max-w-6xl'}"
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('p-4 mx-auto', width)}>
|
||||
|
||||
@@ -12,13 +12,21 @@
|
||||
import { AppService, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { faClipboard, faExternalLink, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Eye, Laptop2, Pencil, PenTool, Smartphone } from 'lucide-svelte'
|
||||
import { Eye, Laptop2, Pencil, Smartphone } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { copyToClipboard, sendUserToast } from '../../../utils'
|
||||
import type { AppEditorContext, EditorBreakpoint, EditorMode } from '../types'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import AppExportButton from './AppExportButton.svelte'
|
||||
|
||||
async function hash(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
|
||||
return hashHex
|
||||
}
|
||||
|
||||
export let policy: Policy
|
||||
|
||||
const { app, summary, mode, breakpoint, appPath } =
|
||||
@@ -38,12 +46,35 @@
|
||||
saveDrawerOpen = false
|
||||
}
|
||||
|
||||
async function computeTriggerables() {
|
||||
const allTriggers = await Promise.all(
|
||||
$app.grid.map(async (x) => {
|
||||
let c = x.data as AppComponent
|
||||
if (c.componentInput?.type == 'runnable') {
|
||||
const staticInputs = Object.fromEntries(
|
||||
Object.entries(c.componentInput.fields ?? {})
|
||||
.filter(([k, v]) => v.type == 'static')
|
||||
.map(([k, v]) => {
|
||||
return [k, v['value']]
|
||||
})
|
||||
)
|
||||
if (c.componentInput.runnable?.type == 'runnableByName') {
|
||||
let hex = await hash(c.componentInput.runnable.inlineScript?.content)
|
||||
console.log(staticInputs)
|
||||
return [`rawscript/${hex}`, staticInputs]
|
||||
} else if (c.componentInput.runnable?.type == 'runnableByPath') {
|
||||
return [c.componentInput.runnable.path, staticInputs]
|
||||
}
|
||||
}
|
||||
return []
|
||||
})
|
||||
)
|
||||
policy.triggerables = Object.fromEntries(allTriggers)
|
||||
policy.on_behalf_of = `u/${$userStore?.username}`
|
||||
policy.on_behalf_of_email = $userStore?.email
|
||||
}
|
||||
async function createApp(path: string) {
|
||||
const policy = {
|
||||
triggerables: {},
|
||||
execution_mode: Policy.execution_mode.PUBLISHER,
|
||||
on_behalf_of: `u/${$userStore?.username}`
|
||||
}
|
||||
await computeTriggerables()
|
||||
try {
|
||||
const appId = await AppService.createApp({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -77,6 +108,7 @@
|
||||
path: appPath,
|
||||
requestBody: { policy }
|
||||
})
|
||||
console.log(policy)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -86,17 +118,14 @@
|
||||
return
|
||||
}
|
||||
loading.save = true
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: $summary,
|
||||
policy: {
|
||||
triggerables: {},
|
||||
execution_mode: Policy.execution_mode.PUBLISHER,
|
||||
on_behalf_of: `u/${$userStore?.username}`
|
||||
}
|
||||
policy
|
||||
}
|
||||
})
|
||||
loading.save = false
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
function getMinDimensionsByComponent(componentType: AppComponent['type'], column: number): Size {
|
||||
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
|
||||
const dimensions: Record<`${number}:${number}-${number}:${number}`, AppComponent['type'][]> = {
|
||||
'1:2-2:2': [
|
||||
'4:2-4:2': [
|
||||
'buttoncomponent',
|
||||
'textcomponent',
|
||||
'checkboxcomponent',
|
||||
@@ -29,7 +29,7 @@
|
||||
'passwordinputcomponent',
|
||||
'dateinputcomponent'
|
||||
],
|
||||
'2:12-4:12': ['barchartcomponent', 'piechartcomponent', 'formcomponent', 'displaycomponent'],
|
||||
'4:12-4:12': ['barchartcomponent', 'piechartcomponent', 'formcomponent', 'displaycomponent'],
|
||||
'3:10-6:12': ['tablecomponent']
|
||||
}
|
||||
// Finds the key that is associated with the component type and extracts the dimensions from it
|
||||
@@ -52,7 +52,7 @@
|
||||
'buttoncomponent'
|
||||
].includes(componentType)
|
||||
) {
|
||||
return { w: column, h: 1 }
|
||||
return { w: column, h: 2 }
|
||||
}
|
||||
return { w: column, h: 80 }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { clearStores } from './stores.js'
|
||||
import { sendUserToast } from './utils.js'
|
||||
|
||||
export async function logoutWithRedirect(rd?: string): Promise<void> {
|
||||
console.log('logoutWithRedirect', rd)
|
||||
|
||||
await clearUser()
|
||||
if (rd && rd?.split('?')[0] != '/user/login') {
|
||||
|
||||
@@ -184,9 +184,9 @@ export function removeItemAll<T>(arr: T[], value: T) {
|
||||
}
|
||||
|
||||
export async function isOwner(path: string, user: UserExt, workspace: string): Promise<boolean> {
|
||||
if (user.is_admin && (workspace != 'starter' || user.is_super_admin)) {
|
||||
if (user.is_admin && ((workspace == 'starter' || workspace == 'admin') && user.is_super_admin)) {
|
||||
return true
|
||||
} else if (workspace == 'starter') {
|
||||
} else if (workspace == 'starter' || workspace == 'admin') {
|
||||
return false
|
||||
} else {
|
||||
return await UserService.isOwnerOfPath({ path: path, workspace: workspace })
|
||||
|
||||
@@ -554,7 +554,7 @@
|
||||
{#if !canWrite}
|
||||
<Badge
|
||||
>Shared globally<Tooltip
|
||||
>This resource type is from the 'starter' workspace shared with all
|
||||
>This resource type is from the 'admins' workspace shared with all
|
||||
workspaces</Tooltip
|
||||
></Badge
|
||||
>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<tr class={enabled ? '' : 'bg-gray-50'}>
|
||||
<td class="max-w-sm"
|
||||
><button
|
||||
class="break-words text-sm text-blue-600 font-normal"
|
||||
class="break-words text-left text-sm text-blue-600 font-normal"
|
||||
on:click={() => scheduleEditor?.openEdit(path, is_flow)}>{path}</button
|
||||
>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
|
||||
Reference in New Issue
Block a user