Files
windmill/frontend/src/lib/components/AddUser.svelte
T
Ruben Fiszelandwindmill-internal-app[bot] b125eca762 feat(service-accounts): allow choosing role at creation time (#9307)
* [ee] feat(service-accounts): allow choosing role at creation time

Previously, service accounts were hardcoded to operator and could not be
used as the CLI sync user since they had no write access. They also only
counted as 0.5 seat each.

This change:
- Extends `NewServiceAccount` to accept optional `is_admin` / `operator`
  (defaults to `operator=true` for backward compatibility).
- Exposes a role picker in `AddUser.svelte` when creating a service
  account (Operator / Developer / Admin).
- Lets admins update a service account's role from the user list (it
  used to be locked to "Operator" with a tooltip).
- Updates the OpenAPI spec + regenerates the frontend client.

A developer/admin service account counts as 1 seat under the existing
seat-cap logic (operators stay at 0.5).

Companion PR on windmill-ee-private updates the `INSERT INTO usr` to
honour the chosen role.

Fixes WIN-1985

* [ee] feat(service-accounts): wm_deployers opt-in for Dev role

When creating a service account with role=Developer, surface a toggle
"Add to wm_deployers" (recommended). Members of wm_deployers can deploy
on behalf of other users — the typical setup when the service account is
used as the CLI sync / CI deploy identity.

- `NewServiceAccount` gains an optional `add_to_deployers` flag.
- Frontend defaults the toggle to on but only shows it under Developer
  (admins have it implicitly; operators can't deploy).
- Tooltip links to docs.windmill.dev "Run on behalf of".

Companion EE PR updates the handler to INSERT into usr_to_group for
wm_deployers when the flag is set.

Refs WIN-1985

* chore: update ee-repo-ref to 974ed42067d9f63acb42332b671b8c01ffd4b625

This commit updates the EE repository reference after PR #589 was merged in windmill-ee-private.

Previous ee-repo-ref: f7dbc3cc2ba21c396f4828881e3b9d9ab6f50c69

New ee-repo-ref: 974ed42067d9f63acb42332b671b8c01ffd4b625

Automated by sync-ee-ref workflow.

* [ee] fix(service-accounts): unhardcode role in superadmin user list

Two review issues from the merged #9307 / #589:

1. P1 — The global Users tab in #superadmin-settings still pinned every
   service account to "Operator". Now it shows the actual role
   (Admin / Operator / Developer), derived from the SA's usr row.

   - `list_users_as_super_admin`: replaced `true as operator_only` with
     the real `operator` value, and added `is_workspace_admin` from the
     row (NULL for password users since their admin status is
     per-workspace).
   - `global_whoami`: when the email belongs to a service account, look
     up its real `operator` / `is_admin` instead of pinning to operator.
   - `SuperadminSettingsInner.svelte`: drop the hardcoded "Operator"
     badge; render Admin / Operator / Developer using the new fields,
     matching the workspace-level view.

2. P2 — Regenerate the bundled `openapi-deref.{yaml,json}` so the
   `createServiceAccount` body (now exposing `is_admin`, `operator`,
   `add_to_deployers`) and the new `GlobalUserInfo.is_workspace_admin`
   field show up at runtime in `/api/openapi.{yaml,json}`.

Bumps `ee-repo-ref.txt` to the EE follow-up that adds the offline
seat-cap check on `create_service_account`.

Refs WIN-1985

* chore: update ee-repo-ref to b7a6068c1f3dc845e012959268b2426f0de4d697

This commit updates the EE repository reference after PR #590 was merged in windmill-ee-private.

Previous ee-repo-ref: 0b1307c21d1bfd6fb43a03c2ba39d2a8bf8e6470

New ee-repo-ref: b7a6068c1f3dc845e012959268b2426f0de4d697

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-25 16:18:38 +00:00

223 lines
7.2 KiB
Svelte

<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { globalEmailInvite, superadmin, workspaceStore, enterpriseLicense } from '$lib/stores'
import { SettingService, UserService, WorkspaceService } from '$lib/gen'
import { Button } from './common'
import Popover from './meltComponents/Popover.svelte'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import { goto } from '$lib/navigation'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
import { UserPlus } from 'lucide-svelte'
const dispatch = createEventDispatcher()
let email: string | undefined = $state()
let username: string | undefined = $state()
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
if (key === 'Enter') {
event.preventDefault()
addUser()
}
}
let automateUsernameCreation = $state(true)
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? true
}
getAutomateUsernameCreationSetting()
async function addUser() {
if (selected === 'service_account') {
if (!username) return
await WorkspaceService.createServiceAccount({
workspace: $workspaceStore!,
requestBody: {
username: username!,
is_admin: serviceAccountRole === 'admin',
operator: serviceAccountRole === 'operator',
add_to_deployers: serviceAccountRole === 'developer' && addToDeployers
}
})
sendUserToast(`Service account '${username}' created`)
} else {
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email: email!,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Added ${email}`)
if (!(await UserService.existsEmail({ email: email! }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email!
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
? `If not using SSO, ask an administrator to add ${email} to the instance`
: ''
}`,
true,
isSuperadmin
? [
{
label: 'Add user to the instance',
callback: () => {
$globalEmailInvite = emailCopy
goto('#superadmin-settings')
}
}
]
: []
)
}
}
}
dispatch('new')
}
type UserRole = 'operator' | 'developer' | 'admin' | 'service_account'
type ServiceAccountRole = 'operator' | 'developer' | 'admin'
let selected: UserRole = $state('developer' as UserRole)
let serviceAccountRole: ServiceAccountRole = $state('operator' as ServiceAccountRole)
let addToDeployers: boolean = $state(true)
let isServiceAccount = $derived(selected === 'service_account')
</script>
<Popover placement="bottom-end">
{#snippet trigger()}
<Button variant="accent" unifiedSize="md" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
Add new user
</Button>
{/snippet}
{#snippet content()}
<div class="flex flex-col w-[28rem] p-4">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
{#if isServiceAccount}
<span class="text-xs mb-1 leading-6">Username</span>
<input
type="text"
onkeyup={handleKeyUp}
placeholder="my_service_account"
autocomplete="off"
data-1p-ignore
bind:value={username}
/>
{:else}
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" onkeyup={handleKeyUp} placeholder="email" bind:value={email} />
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" onkeyup={handleKeyUp} placeholder="username" bind:value={username} />
{/if}
{/if}
<span class="text-xs mb-1 pt-6 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
<ToggleButton
value="service_account"
label={$enterpriseLicense ? 'Service Account' : 'Service Account (EE)'}
tooltip="A service account is a workspace-scoped identity for automation. It cannot log in directly and can be impersonated by admins."
disabled={!$enterpriseLicense}
{item}
/>
{/snippet}
</ToggleButtonGroup>
{#if isServiceAccount}
<span class="text-xs mb-1 leading-6">Service account role</span>
<ToggleButtonGroup bind:selected={serviceAccountRole} class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="Read/run only. Counts as 0.5 seat. Cannot be used for CLI sync or to author scripts/flows/apps."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="Can author and edit scripts/flows/apps within its path. Counts as 1 seat. Use this for CLI sync tokens."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="Full workspace admin. Counts as 1 seat. Grant only when the service account needs to manage workspace settings."
{item}
/>
{/snippet}
</ToggleButtonGroup>
{#if serviceAccountRole === 'developer'}
<div class="flex items-center gap-2 mb-4">
<Toggle bind:checked={addToDeployers} size="xs" />
<span class="text-xs leading-6">
Add to <code>wm_deployers</code>
<Tooltip>
Recommended when this service account will be used as a <code>wmill sync push</code>
/ CI deploy identity. Members of <code>wm_deployers</code> can deploy on behalf of
other users in the target workspace.
<a
href="https://www.windmill.dev/docs/core_concepts/staging_prod#run-on-behalf-of"
target="_blank"
rel="noopener noreferrer"
class="underline">Learn more</a
>.
</Tooltip>
</span>
</div>
{/if}
{/if}
<Button
variant="accent"
size="sm"
on:click={() => {
addUser().then(() => {
// @ts-ignore
email = undefined
// @ts-ignore
username = undefined
})
}}
disabled={isServiceAccount
? username === undefined || username === ''
: email === undefined || (!automateUsernameCreation && username === undefined)}
>
Add
</Button>
</div>
{/snippet}
</Popover>