feat: Superadmins workspace (#1003)

* Add auto-invite superadmins workspace

* Revert "Add auto-invite superadmins workspace"

This reverts commit 768ae31857.

* Add Admin Workspace

* Show unable to delete info in UI

* Add Hub sync

* Update Migration

* Add Starter workspace

* Add workspace_key for admins workspace

* Remove schedule

* Update CI
This commit is contained in:
Kai Jellinghaus
2022-12-10 01:46:26 +01:00
committed by GitHub
parent 5702589954
commit b0baa07579
12 changed files with 220 additions and 140 deletions
+11 -11
View File
@@ -4094,7 +4094,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"argon2",
@@ -4145,7 +4145,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"base64",
"chrono",
@@ -4160,7 +4160,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"chrono",
"serde",
@@ -4173,7 +4173,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"axum",
@@ -4197,7 +4197,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"serde",
"serde_json",
@@ -4205,7 +4205,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"itertools",
@@ -4219,7 +4219,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"itertools",
@@ -4231,7 +4231,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"itertools",
@@ -4246,7 +4246,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"deno_core",
@@ -4260,7 +4260,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"chrono",
@@ -4283,7 +4283,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.54.0"
version = "1.55.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,9 @@
INSERT INTO workspace(id, name, owner) VALUES
('admins', 'Admins', 'admin@windmill.dev');
INSERT INTO workspace_settings (workspace_id) VALUES
('admins');
INSERT INTO workspace_key
(workspace_id, kind, key)
VALUES ('admins', 'cloud', md5(random()::text) || md5(random()::text))
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,20 @@
INSERT INTO script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'admins',
'system',
'import wmill from "https://deno.land/x/wmill@v1.55.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "starter", Deno.env.get("WM_BASE_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'Syncronize Hub Resource types with starter workspace',
'Basic administrative script to sync latest resource types from hub. Recommended to run at least once. On a schedule by default.',
'u/admin/hub_sync', -28028598712388162, 'deno', '');
+87 -81
View File
@@ -1980,87 +1980,6 @@
},
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)"
},
"71b953191b3175a32a22f803205a5fd3dedef1baa0993afad08109ab5bf62fe9": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "value",
"ordinal": 2,
"type_info": "Jsonb"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "resource_type",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "extra_perms",
"ordinal": 5,
"type_info": "Jsonb"
},
{
"name": "is_expired",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "refresh_error",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "is_linked",
"ordinal": 8,
"type_info": "Bool"
},
{
"name": "is_oauth?",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "account",
"ordinal": 10,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
true,
true,
false,
false,
null,
true,
null,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
},
"765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": {
"describe": {
"columns": [
@@ -3373,6 +3292,93 @@
},
"query": "\n UPDATE capture\n SET payload = $3\n WHERE workspace_id = $1\n AND path = $2\n "
},
"b9e968d522558b8ca2bd24d345768ba864a766e10dd79864b9d601254bb2d612": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "value",
"ordinal": 2,
"type_info": "Jsonb"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "resource_type",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "extra_perms",
"ordinal": 5,
"type_info": "Jsonb"
},
{
"name": "is_expired",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "is_refreshed",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "refresh_error",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "is_linked",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "is_oauth?",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "account",
"ordinal": 11,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
true,
true,
false,
false,
null,
null,
true,
null,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_token != '' as is_refreshed,\n account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
},
"bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": {
"describe": {
"columns": [
+9
View File
@@ -426,6 +426,15 @@ async fn delete_workspace(
Path(w_id): Path<String>,
Authed { is_admin, username, email, .. }: Authed,
) -> Result<String> {
let w_id = match w_id.as_str() {
"starter" => Err(Error::BadRequest(
"starter workspace cannot be deleted".to_string(),
)),
"admins" => Err(Error::BadRequest(
"admins workspace cannot be deleted".to_string(),
)),
_ => Ok(w_id),
}?;
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id)
@@ -1,5 +1,5 @@
<script lang="ts">
import { workspaceStore, usersWorkspaceStore } from '$lib/stores'
import { workspaceStore, userWorkspaces } from '$lib/stores'
import { classNames } from '$lib/utils'
import Icon from 'svelte-awesome'
import { Building } from 'lucide-svelte'
@@ -28,7 +28,7 @@
<div class="divide-y divide-gray-100" role="none">
<table class="w-full">
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
{#each $userWorkspaces as workspace}
<tr
class="text-xs cursor-pointer"
on:click={() => {
+69 -42
View File
@@ -1,55 +1,82 @@
import { browser } from '$app/env'
import { writable } from 'svelte/store'
import type { UserWorkspaceList } from '$lib/gen/models/UserWorkspaceList.js'
import { getUserExt } from './user'
import type { TokenResponse } from './gen'
import { browser } from "$app/env";
import { derived, type Readable, writable } from "svelte/store";
import type { UserWorkspaceList } from "$lib/gen/models/UserWorkspaceList.js";
import { getUserExt } from "./user";
import type { TokenResponse } from "./gen";
export interface UserExt {
email: string
username: string
is_admin: boolean
created_at: string
groups: string[]
pgroups: string[]
email: string;
username: string;
is_admin: boolean;
created_at: string;
groups: string[];
pgroups: string[];
}
let persistedWorkspace = browser && localStorage.getItem('workspace')
let persistedWorkspace = browser && localStorage.getItem("workspace");
export const oauthStore = writable<TokenResponse | undefined>(undefined)
export const userStore = writable<UserExt | undefined>(undefined)
export const oauthStore = writable<TokenResponse | undefined>(undefined);
export const userStore = writable<UserExt | undefined>(undefined);
export const workspaceStore = writable<string | undefined>(
persistedWorkspace ? String(persistedWorkspace) : undefined
)
export const starStore = writable(1)
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
export const superadmin = writable<String | false | undefined>(undefined)
persistedWorkspace ? String(persistedWorkspace) : undefined,
);
export const starStore = writable(1);
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(
undefined,
);
export const superadmin = writable<String | false | undefined>(undefined);
export const userWorkspaces: Readable<
Array<{
id: string;
name: string;
username: string;
}>
> = derived(
[usersWorkspaceStore, superadmin],
([store, superadmin]) => {
const originalWorkspaces = (store?.workspaces ?? []);
if (superadmin) {
return [...originalWorkspaces, {
id: "admins",
name: "Admins",
username: "superadmin",
}, {
id: "starter",
name: "Starter",
username: "superadmin",
}];
} else {
return originalWorkspaces;
}
},
);
export const hubScripts = writable<
| Array<{
path: string
summary: string
approved: boolean
kind: string
app: string
ask_id: number
}>
| undefined
>(undefined)
| Array<{
path: string;
summary: string;
approved: boolean;
kind: string;
app: string;
ask_id: number;
}>
| undefined
>(undefined);
if (browser) {
workspaceStore.subscribe(async (workspace) => {
if (workspace) {
localStorage.setItem('workspace', String(workspace))
userStore.set(await getUserExt(workspace))
} else {
userStore.set(undefined)
}
})
workspaceStore.subscribe(async (workspace) => {
if (workspace) {
localStorage.setItem("workspace", String(workspace));
userStore.set(await getUserExt(workspace));
} else {
userStore.set(undefined);
}
});
}
export function clearStores(): void {
localStorage.removeItem('workspace')
userStore.set(undefined)
workspaceStore.set(undefined)
usersWorkspaceStore.set(undefined)
superadmin.set(undefined)
localStorage.removeItem("workspace");
userStore.set(undefined);
workspaceStore.set(undefined);
usersWorkspaceStore.set(undefined);
superadmin.set(undefined);
}
+2 -2
View File
@@ -26,7 +26,7 @@
faFastForward
} from '@fortawesome/free-solid-svg-icons'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { superadmin, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
@@ -105,7 +105,7 @@
<h1 class="text-red-400 mt-6">Job {$page.params.run} not found in {workspace_id}</h1>
<h2>Are you in the right workspace?</h2>
<div class="flex flex-col gap-2">
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
{#each $userWorkspaces as workspace}
<div>
<Button variant="border" href="/run/{$page.params.run}?workspace={workspace.id}">
See in {workspace.name}
@@ -12,7 +12,7 @@
import { sendUserToast } from '$lib/utils'
import { logout, logoutWithRedirect } from '$lib/logout'
import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen'
import { superadmin, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { superadmin, usersWorkspaceStore, userWorkspaces, workspaceStore } from '$lib/stores'
import { faCrown, faUserCog } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Button } from '$lib/components/common'
@@ -60,7 +60,7 @@
if (list_all_as_super_admin) {
loadWorkspacesAsAdmin()
} else {
workspaces = $usersWorkspaceStore?.workspaces ?? []
workspaces = $userWorkspaces
}
}
@@ -277,8 +277,15 @@
<p class="italic text-xs">
The workspace will be archived for a short period of time and then permanently deleted
</p>
{#if $workspaceStore === 'admins' || $workspaceStore === 'starter'}
<p class="italic text-xs">
This workspace cannot be deleted as it has a special function. Consult the documentation for
more information.
</p>
{/if}
<Button
color="red"
disabled={$workspaceStore === 'admins' || $workspaceStore === 'starter'}
size="sm"
btnClasses="mt-2"
on:click={async () => {