@datarails-ai/dr-cli 0.0.1-locked → 1.6.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +608 -0
- package/dist/chunk-DWMCBGQH.js +242 -0
- package/dist/cli.js +64468 -0
- package/dist/kb-6UWI7Q32.js +21 -0
- package/dist/scopes-map.json +4412 -0
- package/dist/skills/connector-sandbox/SKILL.md +95 -0
- package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
- package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
- package/dist/skills/dr-cli/SKILL.md +659 -0
- package/dist/skills/dr-cli/reference/admin.md +796 -0
- package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
- package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
- package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
- package/dist/skills/dr-cli/reference/cash.md +61 -0
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
- package/dist/skills/dr-cli/reference/datamappers.md +150 -0
- package/dist/skills/dr-cli/reference/desk.md +472 -0
- package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
- package/dist/skills/dr-cli/reference/forecast.md +51 -0
- package/dist/skills/dr-cli/reference/integrations.md +57 -0
- package/dist/skills/dr-cli/reference/lookups.md +168 -0
- package/dist/skills/dr-cli/reference/metrics.md +512 -0
- package/dist/skills/dr-cli/reference/multi-product.md +180 -0
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
- package/dist/skills/dr-cli/reference/rh-config.md +84 -0
- package/dist/skills/dr-cli/reference/safety.md +143 -0
- package/dist/skills/dr-cli/reference/spend-control.md +206 -0
- package/dist/skills/dr-cli/reference/templates.md +163 -0
- package/dist/skills/dr-cli/reference/workflows.md +137 -0
- package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
- package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
- package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
- package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
- package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
- package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
- package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
- package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
- package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
- package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
- package/dist/skills/dr-switch-user/SKILL.md +395 -0
- package/dist/skills/dr-validate/SKILL.md +392 -0
- package/dist/skills/dr-validate/report-template.html +950 -0
- package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
- package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
- package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
- package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
- package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
- package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
- package/dist/skills/fx-onboard/SKILL.md +325 -0
- package/dist/skills/mec-table-validation/SKILL.md +383 -0
- package/dist/skills/plugins.json +24 -0
- package/dist/skills/validate-workato-migration/SKILL.md +167 -0
- package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
- package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
- package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
- package/dist/zendeskKb-JPSHVOPH.js +243 -0
- package/package.json +56 -9
- package/scripts/postinstall.mjs +91 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
# IT / admin / misc
|
|
2
|
+
|
|
3
|
+
## Auth / impersonation
|
|
4
|
+
|
|
5
|
+
- `login --server <key>` — browser device flow; the operator's own login.
|
|
6
|
+
- `login-as <email> --server <key>` — impersonate a user with **JWT only**
|
|
7
|
+
(no browser). Requires an operator `login` first. Resolves the email to a
|
|
8
|
+
user id via `can_login_as` (or pass `--user-id <n>`), mints an impersonation
|
|
9
|
+
JWT from the monolith (`POST /jwt/api/loginas/`) authorized by the operator,
|
|
10
|
+
and stores it as a switchable identity keyed by `(server, email)`. Target it
|
|
11
|
+
afterward with `--account <email>`. `--as <operator-email>` selects the
|
|
12
|
+
operator when several are stored. The token refreshes silently and
|
|
13
|
+
auto-remints from the operator if its refresh dies — no re-prompt until the
|
|
14
|
+
operator's own login expires. Denials surface as clear messages
|
|
15
|
+
(not permitted / inactive / unavailable). `whoami --account <email>` shows
|
|
16
|
+
`Impersonated by:`.
|
|
17
|
+
- **Skipping the email lookup.** The email→id resolution is an extra
|
|
18
|
+
round-trip against the user directory before the mint. If it errors or times
|
|
19
|
+
out, get the id once and bypass it:
|
|
20
|
+
`dr users can-login-as --search <email> --json` → `--user-id <n>`. Same for
|
|
21
|
+
scripted runs over many users: resolve the ids up front, then pass
|
|
22
|
+
`--user-id` so a directory hiccup can't fail the batch mid-way.
|
|
23
|
+
- **Requires dr ≥ 1.3.24.** `login-as` shipped in 1.3.24; older installs
|
|
24
|
+
answer `unknown command 'login-as'`. If the browser Login-As works but the
|
|
25
|
+
CLI "can't", check `dr --version` first and `dr update` — that, not a
|
|
26
|
+
permission block, is the most common cause of "the CLI won't let me".
|
|
27
|
+
- **The product `SUPER_ADMIN` role is NOT Django `is_superuser`.** The gate
|
|
28
|
+
(`IT.helpers.it_loginas`, wired as Django's `CAN_LOGIN_AS`; the JWT view
|
|
29
|
+
resolves it via `_loginas_permitted` — same gate as the browser Login-As)
|
|
30
|
+
blocks only genuine Django **`is_superuser`**, sync-service, or application
|
|
31
|
+
targets. A customer/demo user whose *product* role is `SUPER_ADMIN` is
|
|
32
|
+
still impersonable. **Verified on DEMO:** `login-as superadmin@demo.<org>`
|
|
33
|
+
(product role SUPER_ADMIN, `is_admin: true`) minted successfully, same as a
|
|
34
|
+
CONTRIBUTOR. So a `can_login_as` hit is generally reliable; there is **no**
|
|
35
|
+
"equal-or-higher privilege" rule.
|
|
36
|
+
- **When `not_permitted` really does fire:** the target is a true Django
|
|
37
|
+
superuser / sync-service / application user, **or** you (the operator)
|
|
38
|
+
lack scope — a Django superuser operator can impersonate any non-superuser;
|
|
39
|
+
a supporting-manager operator only users in their supported orgs
|
|
40
|
+
(`profile.get_supporting_login_as_organization_users()`). Note the CLI
|
|
41
|
+
`users get` payload exposes `profile.role` (product role) but **not**
|
|
42
|
+
`is_superuser`, so you can't confirm the Django flag from the CLI.
|
|
43
|
+
|
|
44
|
+
## Users
|
|
45
|
+
|
|
46
|
+
- `users list`, `users get`, `users me`.
|
|
47
|
+
- `users can-login-as` — users the operator can impersonate; `--search`,
|
|
48
|
+
`--limit`, `--all`, `--json`. Feeds the email→id lookup for `login-as`.
|
|
49
|
+
It can list a genuine Django superuser / sync-service / application account
|
|
50
|
+
that the mint will still refuse (the gate is applied at mint time), but for
|
|
51
|
+
ordinary customer accounts — including product `SUPER_ADMIN`-role users — a
|
|
52
|
+
hit is reliable. See the gotcha above.
|
|
53
|
+
- `users set-password <user_id>` — set a new password on a **sync-service
|
|
54
|
+
account only** (`profile.is_sync_service === true`). Refuses every other user
|
|
55
|
+
up-front; regular humans authenticate via Descope/SSO and must use the
|
|
56
|
+
product's "Send a reset password link by email" flow instead. The new value
|
|
57
|
+
is read from an interactive hidden TTY prompt — there is no `--password` flag
|
|
58
|
+
and stdin is required (refuses non-TTY to avoid silent hangs); paste works.
|
|
59
|
+
Text mode prints the target's email + org for sanity-check before the prompt;
|
|
60
|
+
`--json` skips that preamble and emits `{"id":…,"email":"…","ok":true}` on
|
|
61
|
+
success. Server-side complexity rejections (HTTP 400 with
|
|
62
|
+
`{password:[…]}`) are surfaced verbatim as bullet lines so the operator sees
|
|
63
|
+
the exact policy that failed. PUTs `/api/it/users/<id>/set_password`,
|
|
64
|
+
mirroring the FE `dialog-user-set-password.component.ts` path.
|
|
65
|
+
|
|
66
|
+
### `users bulk-create` — invite many users from a CSV
|
|
67
|
+
|
|
68
|
+
`users bulk-create --file <csv> [--organization <id>] [--no-register-identity] [--confirm] [--json] [--timeout <seconds>]`
|
|
69
|
+
automates the UI's **Invite Users → Set Permissions** flow for a whole list:
|
|
70
|
+
one backend call to create each user (`POST /api/it/users`), then one to scope
|
|
71
|
+
their data access (`POST /api/it/users/<id>/permission_groups`). Turns an
|
|
72
|
+
hours-long manual job into seconds.
|
|
73
|
+
|
|
74
|
+
**No passwords, SSO as normal.** This never sets or sends a password. The
|
|
75
|
+
server creates each user with a random throwaway password it never surfaces and
|
|
76
|
+
sends the standard invite (`send_invite_mail_or_ciam` — the Descope/CIAM path),
|
|
77
|
+
exactly like a UI invite. Invited users onboard through the org's **default
|
|
78
|
+
SSO/Descope login** — identical to inviting them one-by-one. There is no
|
|
79
|
+
`--password` here (that's `users set-password`, sync-service accounts only).
|
|
80
|
+
|
|
81
|
+
**CSV format** (header is exact; comma-delimited, so multi-group cells use `;`):
|
|
82
|
+
|
|
83
|
+
```csv
|
|
84
|
+
email,first_name,last_name,role,permission_groups,department,job_role,register_identity
|
|
85
|
+
jane.doe@acme.com,Jane,Doe,VIEWER,US Sales;Marketing Reports,FINANCE_FPA,ANALYST_OR_SPECIALIST,true
|
|
86
|
+
john.roe@acme.com,John,Roe,VIEWER,EMEA Sales,SALES,MANAGER,
|
|
87
|
+
sam.lee@acme.com,Sam,Lee,VIEWER,US Sales;EMEA Sales;Exec Dashboards,,,false
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Column | Required | Notes |
|
|
91
|
+
|--------|----------|-------|
|
|
92
|
+
| `email` | ✅ | Unique per file. The only truly required field. |
|
|
93
|
+
| `first_name` / `last_name` | – | Optional. |
|
|
94
|
+
| `role` | – | Blank ⇒ `VIEWER`. Accepts `VIEWER`, `COLLABORATOR`, `CONTRIBUTOR`, `ADMIN` (the privileged `SUPER_ADMIN`/`DR_CONNECT_ADMIN` are rejected on purpose). |
|
|
95
|
+
| `permission_groups` | – | `;`-separated group **names** (not ids), resolved case-insensitively against `dr organizations permission-groups`. This is the data-access column; blank = invited but sees nothing until scoped. |
|
|
96
|
+
| `department` / `job_role` | – | Optional enum names (e.g. `FINANCE_FPA`, `MANAGER`); validated server-side. |
|
|
97
|
+
| `register_identity` | – | **Optional 8th column.** Per-row control over identity-provider registration + invite email. `true`/blank ⇒ register + invite on create (the default). `false` ⇒ defer — create the user without IdP registration / invite (register later with `register-identity`). **Omit the column entirely and every row defaults to `true`** (back-compat). `--no-register-identity` forces `false` for the whole batch regardless of this column. |
|
|
98
|
+
|
|
99
|
+
> **`bulk-create` does not create groups — the name must already exist.** Any name
|
|
100
|
+
> in the `permission_groups` column is resolved against existing groups; an unknown
|
|
101
|
+
> name aborts the run (fail-fast) before any user is created. Pre-create groups
|
|
102
|
+
> with **`dr organizations create-group`** (see Organizations below) or in the web
|
|
103
|
+
> UI's Permission Groups settings. On a brand-new org with no groups yet, leave the
|
|
104
|
+
> column blank (users are invited unscoped) and pass `--organization <id>` since the
|
|
105
|
+
> target org can't be inferred without groups.
|
|
106
|
+
|
|
107
|
+
Ready-to-edit templates ship at `docs/examples/users-bulk-create.csv` (with
|
|
108
|
+
permission groups) and `docs/examples/users-bulk-create-no-groups.csv` (blank
|
|
109
|
+
`permission_groups` — invite unscoped, e.g. into a fresh org).
|
|
110
|
+
|
|
111
|
+
**Big CSVs finish in one run — do not chunk them.** The whole-command budget is
|
|
112
|
+
sized to the work: 30s of base, plus 5s per **round-trip** (not per row — one row
|
|
113
|
+
that fans out to N calls is sized as N), capped at 30 minutes. A 140-row CSV gets
|
|
114
|
+
~12 minutes, which is far more than the ~5 minutes it actually needs.
|
|
115
|
+
|
|
116
|
+
`--timeout <seconds>` raises that budget for something unusually large or a very
|
|
117
|
+
slow tenant. It is a **floor, not a cap**: auto-sizing may still widen it, so a
|
|
118
|
+
value smaller than what the plan needs costs you nothing. Max 86400 (24h). Past
|
|
119
|
+
~354 round-trips the 30-minute ceiling starts biting and the per-call allowance
|
|
120
|
+
decays — the command warns on stderr when that happens, and `--timeout` is the
|
|
121
|
+
answer if the run then aborts part-way.
|
|
122
|
+
|
|
123
|
+
> **If a run ever stops partway and the CLI says `session expired; re-login
|
|
124
|
+
> required`, do not conclude the JWT expired.** dr-cli auto-refreshes the JWT
|
|
125
|
+
> (proactively at a 60s buffer, and again on any 401), so a mid-batch expiry is
|
|
126
|
+
> not a thing you need to work around. That message was historically what a
|
|
127
|
+
> whole-command *timeout* looked like when the abort happened to land inside the
|
|
128
|
+
> auth pre-flight. Re-logging-in and re-running in smaller chunks "worked" only
|
|
129
|
+
> because each run restarted the clock. The fix is the sized budget above, or
|
|
130
|
+
> `--timeout`, not chunking and not a re-login.
|
|
131
|
+
|
|
132
|
+
**Deferring registration — create quietly, invite later.** By default each
|
|
133
|
+
create registers the user in the identity provider (Descope) and sends the
|
|
134
|
+
invite email — on prod, registration *is* the invite email (one atomic Descope
|
|
135
|
+
call). Two ways to defer that:
|
|
136
|
+
- **Per row:** set the `register_identity` column to `false` on the rows you
|
|
137
|
+
want deferred (blank/absent ⇒ `true`). Lets one CSV mix invited and deferred
|
|
138
|
+
users.
|
|
139
|
+
- **Whole batch:** pass **`--no-register-identity`**, which forces every row to
|
|
140
|
+
`false` regardless of the column.
|
|
141
|
+
Deferred users still get the row, async data-model provisioning, and seat-limit
|
|
142
|
+
alert — they just can't authenticate until registered. Stand up an environment
|
|
143
|
+
silently, then fire the invites with **`users register-identity --file <same
|
|
144
|
+
csv>`** (below). The command sends `register_identity: false` only for deferred
|
|
145
|
+
rows; a fully-invited run's create body is unchanged. See DR-52009.
|
|
146
|
+
|
|
147
|
+
**Safety model:**
|
|
148
|
+
- **Dry-run by default.** Without `--confirm` the command parses the CSV,
|
|
149
|
+
resolves every group name + the target org, prints the plan, and makes **zero
|
|
150
|
+
mutating calls**. Nothing is created (and no invite emails go out) until you
|
|
151
|
+
re-run with `--confirm`.
|
|
152
|
+
- **Fail-fast before any write.** A typo'd/unknown group name, an ambiguous
|
|
153
|
+
name, or a duplicate email aborts the whole run *before* the first user is
|
|
154
|
+
created — you never get a half-built batch.
|
|
155
|
+
- **Target org.** Auto-resolved from the tenant's permission groups (the create
|
|
156
|
+
endpoint requires an org id and the profile endpoint doesn't expose one).
|
|
157
|
+
Pass `--organization <id>` to be explicit, or when the tenant has no groups /
|
|
158
|
+
groups spanning multiple orgs (the command asks for it in those cases). The
|
|
159
|
+
resolved org **name** is printed so you can eyeball the target before
|
|
160
|
+
confirming — the guard against creating users on the wrong tenant.
|
|
161
|
+
- **Per-row isolation on execute.** An already-existing email is **not created
|
|
162
|
+
again**, but if the row lists permission groups those groups are **assigned to
|
|
163
|
+
the existing user** (reported as `re-scoped`). This makes re-runs idempotent
|
|
164
|
+
*and* self-healing: a row that created a user but then failed to scope them
|
|
165
|
+
(see below) is repaired on the next `--confirm` run. Assignment is **additive**
|
|
166
|
+
— it only adds groups, never removes existing access. A row with no groups on
|
|
167
|
+
an existing email is simply `skipped`. A hard failure is recorded and the batch
|
|
168
|
+
keeps going; any failure makes the command exit non-zero, with a
|
|
169
|
+
created/re-scoped/skipped/failed summary.
|
|
170
|
+
- **Partial-failure remediation (`failed` rows).** A row that reports
|
|
171
|
+
`created (id N) but group assignment failed` means the user exists but is
|
|
172
|
+
**unscoped** — fix the group name if it was rejected, then re-run the same CSV
|
|
173
|
+
with `--confirm`: the create step finds the user already exists and re-scopes
|
|
174
|
+
it. A row reporting `already exists but could not resolve its user id` means
|
|
175
|
+
the operator can't see that user via `/api/it/users` (usually a different org)
|
|
176
|
+
— assign the groups in the UI or check `--organization`.
|
|
177
|
+
- Requires an **org-admin or supporting-manager** login (same gate as the UI
|
|
178
|
+
invite); a support/CSM account with those rights on the tenant works. Note the
|
|
179
|
+
re-scoping behavior means this command can **grant data access to users that
|
|
180
|
+
already existed**, not just ones it creates — the printed org name + `--confirm`
|
|
181
|
+
gate are your guard against doing that on the wrong tenant.
|
|
182
|
+
|
|
183
|
+
### `users set-email` — change one user's login email
|
|
184
|
+
|
|
185
|
+
`users set-email <user_id> <new_email> [--confirm] [--json]` changes a user's
|
|
186
|
+
**login email** through `PUT /api/it/users/<id>` — the same endpoint the IT
|
|
187
|
+
admin edit dialog uses. The PUT (not a PATCH) is deliberate: it lets the
|
|
188
|
+
server's SSO/CIAM email-sync run, and it preserves the user's
|
|
189
|
+
`excel_online_email` rather than blanking it.
|
|
190
|
+
|
|
191
|
+
- **Dry-run by default.** Without `--confirm` the command GETs the user and
|
|
192
|
+
prints `current_email -> new_email` (or the same as JSON) and makes **zero
|
|
193
|
+
mutating calls**. Re-run with `--confirm` to apply.
|
|
194
|
+
- **Validation.** The `<user_id>` must be positive and `<new_email>` must
|
|
195
|
+
contain `@`, both checked before any network call. A target already taken by
|
|
196
|
+
another user fails with `email already in use: <email>. Pick a different
|
|
197
|
+
address.` (server `EmailInUseError`).
|
|
198
|
+
- On success `--json` emits `{id, previous_email, email}`.
|
|
199
|
+
|
|
200
|
+
### `users bulk-set-email` — rename many users' emails from a CSV
|
|
201
|
+
|
|
202
|
+
`users bulk-set-email --file <csv> [--confirm] [--json] [--timeout <seconds>]` applies many login-email
|
|
203
|
+
renames in one run, each row reusing the same `PUT /api/it/users/<id>` path as
|
|
204
|
+
single-user `set-email`. Same shape as `bulk-create` / `bulk-permissions`:
|
|
205
|
+
resolve + validate the whole file first, then execute with per-row isolation.
|
|
206
|
+
|
|
207
|
+
**CSV format** (header is exact, in order):
|
|
208
|
+
|
|
209
|
+
```csv
|
|
210
|
+
source,target
|
|
211
|
+
old.jane@acme.com,jane.doe@acme.com
|
|
212
|
+
old.john@acme.com,john.roe@acme.com
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Column | Required | Notes |
|
|
216
|
+
|--------|----------|-------|
|
|
217
|
+
| `source` | ✅ | The user's **current** email; must contain `@`; matched case-insensitively against tenant users; unique per file. |
|
|
218
|
+
| `target` | ✅ | The **new** email; must contain `@`; unique per file; a row whose `source` == `target` is rejected. |
|
|
219
|
+
|
|
220
|
+
A ready-to-edit template ships at `docs/examples/users-bulk-set-email.csv`.
|
|
221
|
+
|
|
222
|
+
**Safety model:**
|
|
223
|
+
- **Fail-fast before any write.** `planEmailRenames` aborts the whole run
|
|
224
|
+
*before* the first PUT when: a `source` matches no tenant user; the batch
|
|
225
|
+
spans **more than one organization** (all sources must belong to a single org
|
|
226
|
+
— the org name + id are printed so you can eyeball the target); a source user
|
|
227
|
+
has **no organization** (can't verify the same-org rule); or a `target` is
|
|
228
|
+
already in use by an existing user. If that existing user is itself a source
|
|
229
|
+
in the batch, it's a **swap/chain**, which is unsupported (renames apply
|
|
230
|
+
sequentially, not atomically) and also aborts.
|
|
231
|
+
- **Dry-run by default.** Without `--confirm` it prints the resolved plan
|
|
232
|
+
(`source -> target (id N)` per row) and changes nothing.
|
|
233
|
+
- **Per-row isolation on execute.** With `--confirm` each rename runs in order;
|
|
234
|
+
a row failure (e.g. a race where the target got taken) is recorded as `failed`
|
|
235
|
+
and the batch continues. A non-empty failure set makes the command exit
|
|
236
|
+
non-zero after reporting every row's `changed`/`failed` status. `--json`
|
|
237
|
+
emits `{organization, results, attempted, planned}` even if the run aborts
|
|
238
|
+
mid-way.
|
|
239
|
+
- Requires the same **org-admin / supporting-manager** login as the other IT
|
|
240
|
+
user commands.
|
|
241
|
+
|
|
242
|
+
### `users set-email-notifications` — turn one user's notification email on/off
|
|
243
|
+
|
|
244
|
+
`users set-email-notifications <user_id> --enable|--disable [--confirm] [--json]`
|
|
245
|
+
is the CLI equivalent of the **"Enable notifications"** Yes/No field in the
|
|
246
|
+
admin user-edit form. It writes `disable_emails` through the same
|
|
247
|
+
`PUT /api/it/users/<id>` the dialog uses. Exactly one of `--enable` /
|
|
248
|
+
`--disable` is required.
|
|
249
|
+
|
|
250
|
+
- `--enable` → the user **receives** notification mail (`disable_emails=false`).
|
|
251
|
+
- `--disable` → the user receives none (`disable_emails=true`).
|
|
252
|
+
- **Dry-run by default.** Without `--confirm` it prints
|
|
253
|
+
`notifications off -> on` and makes zero mutating calls; it also says so
|
|
254
|
+
when the user is already in the requested state.
|
|
255
|
+
- **Only `disable_emails` changes.** The write body is deliberately minimal —
|
|
256
|
+
`{disable_emails, email, excel_online_email}` — with `email` echoed back
|
|
257
|
+
**unchanged** from the GET. Sending no `email` would make the server compare
|
|
258
|
+
the stored address against `None` and run its SSO email-sync for a change
|
|
259
|
+
that isn't happening; `excel_online_email` is echoed because the server
|
|
260
|
+
clears it when the key is absent. Role, department, job role, organization
|
|
261
|
+
and block status are all left untouched.
|
|
262
|
+
|
|
263
|
+
**Sync-service and support accounts are refused**, with a message naming why —
|
|
264
|
+
they are provisioned with notifications off and the platform does not send them
|
|
265
|
+
notification mail. The bulk command skips them for the same reason, so the two
|
|
266
|
+
verbs never disagree about the same user. (The server would actually accept the
|
|
267
|
+
write: its sync-service override only fires when `is_sync_service` is in the
|
|
268
|
+
payload, which this minimal body never sends.)
|
|
269
|
+
|
|
270
|
+
**Accounts with a malformed stored email can't be toggled.** Because the write
|
|
271
|
+
echoes the current address back, the server re-runs its email validation over
|
|
272
|
+
it. A user whose stored address isn't a valid one (no TLD, legacy seed data)
|
|
273
|
+
fails with a message naming the address and pointing at
|
|
274
|
+
`users set-email <id> <valid@address>` as the prerequisite. The admin UI has
|
|
275
|
+
the same limitation — its edit payload echoes `email` too — so this is a
|
|
276
|
+
property of the endpoint, not of the CLI.
|
|
277
|
+
|
|
278
|
+
**Blocked users are the one surprise.** The server reports notification state
|
|
279
|
+
as `disable_emails OR is_blocked`, so enabling a blocked user succeeds but
|
|
280
|
+
`users get <id>` keeps showing notifications off until the user is unblocked.
|
|
281
|
+
The command prints an explicit note saying the write landed and no retry is
|
|
282
|
+
needed — don't re-run it.
|
|
283
|
+
|
|
284
|
+
**An unreported current state stays `unknown`.** The server omits `profile`
|
|
285
|
+
entirely for some accounts, and `disable_emails` is optional even when
|
|
286
|
+
`profile` is present. Where the field is missing the preview prints
|
|
287
|
+
`notifications unknown -> on`, never claims "already in the requested
|
|
288
|
+
state", and `--json` reports `current_email_notifications: null` with
|
|
289
|
+
`no_change: null` (and `previous_email_notifications: null` after the write).
|
|
290
|
+
The requested write still goes ahead — you asked for a specific user — but
|
|
291
|
+
nothing in the output asserts a starting state we have no evidence for. The
|
|
292
|
+
bulk command, which was not asked about any one user, skips those users
|
|
293
|
+
instead.
|
|
294
|
+
|
|
295
|
+
To find who is affected on a tenant, `users list` now renders an
|
|
296
|
+
`EMAIL_NOTIF` column, and `users list --email-notifications off
|
|
297
|
+
[--organization <id>]` filters directly to the users who receive nothing.
|
|
298
|
+
|
|
299
|
+
### `users bulk-set-email-notifications` — same toggle, whole organization
|
|
300
|
+
|
|
301
|
+
`users bulk-set-email-notifications --organization <id> --enable|--disable
|
|
302
|
+
[--only-changed] [--confirm] [--json] [--timeout <seconds>]` applies the toggle to every user in one
|
|
303
|
+
organization. There is **no org-level notification setting** in the data model —
|
|
304
|
+
the flag lives on each user — so this is a per-user write loop, shaped like
|
|
305
|
+
`bulk-set-email`: plan first, dry-run by default, per-user isolation on execute.
|
|
306
|
+
|
|
307
|
+
**Always skipped, without being asked:**
|
|
308
|
+
|
|
309
|
+
| Skipped | Why |
|
|
310
|
+
|---------|-----|
|
|
311
|
+
| sync-service users | Provisioned with notifications off; the platform sends them no notification mail, so the setting has no effect. The single-user command refuses them for the same reason. |
|
|
312
|
+
| support users | Provisioned with notifications off by design. |
|
|
313
|
+
| users whose state the server didn't report | State unknown; writing blind could flip someone you didn't mean to touch. |
|
|
314
|
+
| users already in the requested state | Only with `--only-changed`, and **never** for a blocked user — see below. |
|
|
315
|
+
|
|
316
|
+
**Safety model:**
|
|
317
|
+
- **Fail-fast.** An `--organization` matching no users aborts before any write
|
|
318
|
+
rather than reporting an empty plan — the usual cause is a wrong org id.
|
|
319
|
+
- **Dry-run by default.** Without `--confirm` it prints the full plan
|
|
320
|
+
(`~ email (id N) off -> on`, `[blocked]` where relevant) plus every skipped
|
|
321
|
+
user and its reason, and changes nothing.
|
|
322
|
+
- **Per-user isolation on execute.** A failed user is recorded and the batch
|
|
323
|
+
continues; a non-empty failure set exits non-zero, names the failed ids, and
|
|
324
|
+
points at `--only-changed` to retry just the remainder. `--json` emits
|
|
325
|
+
`{organization, email_notifications, results, skipped, attempted, planned}`
|
|
326
|
+
even if the run aborts mid-way.
|
|
327
|
+
- **`--only-changed` never skips a blocked user.** Their read state is
|
|
328
|
+
`_disable_emails OR is_blocked`, so it always says "off" and reveals nothing
|
|
329
|
+
about the stored column. Skipping on it would pass over a blocked user still
|
|
330
|
+
stored as enabled during `--disable --only-changed`, and they would start
|
|
331
|
+
receiving mail the moment they were unblocked. Blocked users are always
|
|
332
|
+
written.
|
|
333
|
+
- Blocked users in the batch get one summary note, for the same read-back
|
|
334
|
+
reason as the single-user command.
|
|
335
|
+
|
|
336
|
+
### `users register-identity` — send the deferred invites for a `--no-register-identity` batch
|
|
337
|
+
|
|
338
|
+
`users register-identity --file <csv> [--confirm] [--json] [--timeout <seconds>]` is the second half
|
|
339
|
+
of the `bulk-create --no-register-identity` flow (DR-52009): it registers the deferred
|
|
340
|
+
users in the identity provider and sends the invite email that create skipped.
|
|
341
|
+
**Feed it the SAME CSV** you bulk-created from — it only reads the `email`
|
|
342
|
+
column (all other columns are ignored), resolves each email to its tenant user
|
|
343
|
+
id via `GET /api/it/users`, and calls `PUT /api/it/users/<id>/register_identity`
|
|
344
|
+
per row. That endpoint uses the *same* invite path as create, so the resulting
|
|
345
|
+
email is identical to an immediate invite (**not** a password-reset).
|
|
346
|
+
|
|
347
|
+
**Safety model:**
|
|
348
|
+
- **Dry-run by default.** Without `--confirm` it resolves every email and prints
|
|
349
|
+
the plan — `~ email (id N)` for resolved rows, `! email (NOT FOUND …)` for
|
|
350
|
+
emails not in the tenant — and makes **zero** mutating calls. Re-run with
|
|
351
|
+
`--confirm` to register + invite.
|
|
352
|
+
- **Honest per-row status** (from the server's contract): `registered` (200
|
|
353
|
+
`{"sent":true}` — newly provisioned + emailed), `already_exists` (200
|
|
354
|
+
`{"sent":false}` — the identity was already in the IdP, a safe no-op), or
|
|
355
|
+
`failed` (a 502 `provider_error` from the IdP, an email not found in the
|
|
356
|
+
tenant, or any other error). The run prints a `registered / already_exists /
|
|
357
|
+
failed` summary.
|
|
358
|
+
- **Per-row isolation + idempotent re-runs.** A failed row is recorded and the
|
|
359
|
+
batch keeps going; **only a non-empty `failed` set makes the command exit
|
|
360
|
+
non-zero**. Because an already-registered user reports `already_exists` (not
|
|
361
|
+
`failed`), re-running the same CSV after a partial batch only retries the ones
|
|
362
|
+
that didn't land. `--json` emits `{results}` with per-row `{line, email,
|
|
363
|
+
status, user_id, detail?}`.
|
|
364
|
+
- Requires the same **org-admin / supporting-manager** login as the other IT
|
|
365
|
+
user commands (server enforces `IsAuthenticatedOriginalITAdmin`).
|
|
366
|
+
|
|
367
|
+
> **Note (adjacent monolith bug DR-52121):** the deferred invite emails because
|
|
368
|
+
> a monolith comparison bug forces `send_mail=True`. That's the desired behavior
|
|
369
|
+
> here, but a future DR-52121 fix must keep passing `send_mail=True` or deferred
|
|
370
|
+
> (and inline) invites will silently stop emailing.
|
|
371
|
+
|
|
372
|
+
### `users permissions` — the Permission Manager (data tables + elements)
|
|
373
|
+
|
|
374
|
+
The CLI surface of the web **Permission Manager** (`/#/it/users/:id/permissions`).
|
|
375
|
+
**Requires the `use_new_table_perms` product flag on the tenant** — verify with
|
|
376
|
+
`dr features orgs list <org_id>`; the new grant/revoke endpoints this uses are
|
|
377
|
+
only active when it's on. All *write* subcommands are **dry-run unless
|
|
378
|
+
`--confirm`** (like `bulk-create`), and support `--json`.
|
|
379
|
+
|
|
380
|
+
```console
|
|
381
|
+
users permissions get <user_id>
|
|
382
|
+
users permissions set-role <user_id> --role <ROLE> [--confirm]
|
|
383
|
+
users permissions grant-table --email <e> --table <id[,id]> --role <ROLE> [--confirm]
|
|
384
|
+
users permissions revoke-table --email <e> --table <id[,id]> [--confirm]
|
|
385
|
+
users permissions grant-element --type <T> --email <e> --id <id[,id]> --role <ROLE> [--confirm]
|
|
386
|
+
users permissions revoke-element --type <T> --email <e> --id <id[,id]> [--confirm]
|
|
387
|
+
users permissions set-filters <user_id> --file <json> [--confirm]
|
|
388
|
+
users permissions remove-group <user_id> --group <id> [--confirm]
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
- `get` prints identity, org role, permission-group membership, and the user's
|
|
392
|
+
**per-table roles** (`GET /api/templates?user_to_check_role_id=<id>` — only
|
|
393
|
+
tables with a direct personal role are listed; group-derived access is
|
|
394
|
+
annotated), and the user's **row-level data-access filters**
|
|
395
|
+
(`GET /api/it/users/<id>/permission_filters?include_inherited=true` — direct +
|
|
396
|
+
group-inherited, shown as `Table.Field in/not in [values]`). A **Super Admin**
|
|
397
|
+
shows a full-access note instead of grants. Per-**element** role read-back and
|
|
398
|
+
filter *authoring* (`POST …/permission_filters`) are **not** assembled yet
|
|
399
|
+
(DR-51917 follow-ups).
|
|
400
|
+
- **Common trap — "the user has no table permissions".** Do **not** answer
|
|
401
|
+
"what tables can this user see?" from the raw `GET /api/it/users/<id>` object's
|
|
402
|
+
`permissions` field. That field is Django **global model-level** perm codenames
|
|
403
|
+
(`user.user_permissions`) and is **empty for essentially every VIEWER** — it
|
|
404
|
+
never reflects table access. Real table access lives as **guardian object-level
|
|
405
|
+
roles per table**, visible only via the `user_to_check_role_id` sweep that this
|
|
406
|
+
`get` runs. Always use `users permissions get <id>` (or the sweep directly); an
|
|
407
|
+
empty top-level `permissions: []` is **not** evidence of no access.
|
|
408
|
+
- **`CUSTOM` role in a read/export.** The sweep can report a role of
|
|
409
|
+
`{id: -1, name: "CUSTOM", label: "Custom"}`. This is a **server sentinel**, not
|
|
410
|
+
a catalog role — `BackOffice/serializers.py` returns it whenever the user's
|
|
411
|
+
perms on that object don't map to a named preset role. On a plain VIEWER it
|
|
412
|
+
corresponds to the web UI's **"No access"**; treat/label it as no access (or a
|
|
413
|
+
bespoke perm set), never as a grantable role.
|
|
414
|
+
- `set-role` changes the **org role** via `PUT /api/it/users/<id>/permissions`
|
|
415
|
+
`{role: <NAME>}` — `VIEWER | CONTRIBUTOR | COLLABORATOR | ADMIN | SUPER_ADMIN`
|
|
416
|
+
(the `UserProfileRole` PK is the name). Server-side guards (SUPER_ADMIN
|
|
417
|
+
promotion needs a super-admin operator, `COLLABORATOR` needs `use_new_table_perms`,
|
|
418
|
+
seat limits) surface verbatim.
|
|
419
|
+
- `grant-table` / `revoke-table` drive `POST /api/templates/users/grant_permissions_bulk`
|
|
420
|
+
(`role: null` revokes). Note the path is `/api/templates/…`, not `/api/tables/…`
|
|
421
|
+
— the monolith mounts the Tables app under `/api/templates/` (a `/api/tables/…`
|
|
422
|
+
POST 404s). The `--role` name is resolved case-insensitively against
|
|
423
|
+
`GET /api/permissions/roles/table` — role names are the full enum keys, e.g.
|
|
424
|
+
`OWNER_TABLE_ROLE`, `VIEWER_TABLE_DATA_ROLE`.
|
|
425
|
+
- `grant-element` / `revoke-element` cover **every category in the web Permission
|
|
426
|
+
Manager's Elements pane**. `--type` is one of
|
|
427
|
+
`dashboard | storyboard | filebox | lookup | model | report | datasource`, each
|
|
428
|
+
mapped to its app + role catalog:
|
|
429
|
+
|
|
430
|
+
| `--type` | web category | grant endpoint | role catalog |
|
|
431
|
+
|---|---|---|---|
|
|
432
|
+
| `dashboard` | Dashboards | `POST /api/dashboards/users/grant_permissions_bulk` | `roles/dashboard` |
|
|
433
|
+
| `storyboard` | Storyboards | `POST /api/storytelling/users/grant_permissions_bulk` | `roles/story` |
|
|
434
|
+
| `filebox` | Input fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
|
|
435
|
+
| `lookup` | Lookup fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
|
|
436
|
+
| `model` | Model fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
|
|
437
|
+
| `report` | Reports | `POST /api/filebox_reports/users/grant_permissions_bulk` | `roles/filebox_reports` |
|
|
438
|
+
| `datasource` | Data sources | `POST /api/v3/data_integrations/data-sources/users/grant_permissions_bulk` | `roles/data_source` |
|
|
439
|
+
|
|
440
|
+
`--id` is the entity id (dashboard/story/document/report-space/data-source id),
|
|
441
|
+
repeatable or comma-separated. `filebox`, `lookup`, and `model` share the
|
|
442
|
+
Documents surface — they differ only by which documents the web UI lists; grant
|
|
443
|
+
by id is identical. **`datasource` caveat:** `roles/data_source` is **empty
|
|
444
|
+
unless the tenant has the `new_data_sources_perms_and_roles` flag on**, so
|
|
445
|
+
`--role` resolution fails with an empty valid-role list when it's off.
|
|
446
|
+
- **`report` caveat:** grants at the report-*space* level only; the web UI also
|
|
447
|
+
cascades to the report's nested documents via a second `/api/documents/...` call
|
|
448
|
+
— not yet mirrored here (DR-51917).
|
|
449
|
+
- `set-filters` replaces the user's **row-level data-access filters** via
|
|
450
|
+
`POST /api/it/users/<id>/permission_filters`. **This is a FULL REPLACE** — the
|
|
451
|
+
server deletes every existing own filter and recreates from the file, so pass
|
|
452
|
+
the complete desired set (an empty array clears them all). `--file` is a JSON
|
|
453
|
+
array (or `{"new_filters":[…]}`) of `{field:<FlatTableField id>, values:[…],
|
|
454
|
+
is_excluded?:bool}`. `is_excluded:true` makes it a deny-list. Dry-run prints
|
|
455
|
+
the intended set and the replace warning; `--confirm` applies. (Filter *reads*
|
|
456
|
+
are shown by `get`.)
|
|
457
|
+
- `remove-group` deletes one membership (`DELETE /api/it/users/<id>/permission_groups/<gid>`);
|
|
458
|
+
the additive *add* is done by `bulk-create`'s `permission_groups` column.
|
|
459
|
+
- Every write subcommand echoes a `Target: account <server>, operator <email>`
|
|
460
|
+
line first (text mode only) so a mis-set `--account`/`--server` is visible
|
|
461
|
+
before — and after — a mutation lands. Under `--json` the line is suppressed so
|
|
462
|
+
output stays a single parseable object.
|
|
463
|
+
- With `--json`, every write subcommand emits structured JSON on **both** paths:
|
|
464
|
+
the dry-run object carries `"dry_run": true`; the confirmed object omits it. So
|
|
465
|
+
a script can diff the two without parsing prose.
|
|
466
|
+
|
|
467
|
+
### `users bulk-permissions` — many Permission-Manager grants from a CSV
|
|
468
|
+
|
|
469
|
+
`users bulk-permissions --file <csv> [--confirm] [--json] [--timeout <seconds>]`
|
|
470
|
+
|
|
471
|
+
The bulk counterpart to `users permissions` — applies the four grant kinds above
|
|
472
|
+
from one CS-authorable CSV instead of one flag-driven call at a time. Same
|
|
473
|
+
`use_new_table_perms` requirement, same **dry-run unless `--confirm`** / `--json`
|
|
474
|
+
model. Header (exact, seven columns): `email,resource,type,ids,role,filter_values,filter_excluded`.
|
|
475
|
+
One row = one grant, discriminated by `resource`:
|
|
476
|
+
|
|
477
|
+
| `resource` | maps to | row shape |
|
|
478
|
+
|---|---|---|
|
|
479
|
+
| `table` | `grant-table` | `ids` = `;`-sep table ids, `role` required |
|
|
480
|
+
| `element` | `grant-element` | `type` = filebox/lookup/model/report/dashboard/storyboard/datasource, `ids` = `;`-sep, `role` required |
|
|
481
|
+
| `role` | `set-role` | `role` = org role; `ids`/`type`/`filter_*` blank |
|
|
482
|
+
| `filter` | `set-filters` | `ids` = one FlatTableField id, `filter_values` = `;`-sep, `filter_excluded` blank/`true`/`false` |
|
|
483
|
+
|
|
484
|
+
- **Resolves everything first and fails loud before any write:** every `email`
|
|
485
|
+
must exist on the tenant, and each `table`/`element` `role` must resolve against
|
|
486
|
+
that surface's role catalog (org roles and filter field ids are validated
|
|
487
|
+
server-side, per row). A typo aborts the whole run before a single grant lands.
|
|
488
|
+
- **`filter` rows are full-replace, aggregated per user:** all `filter` rows for
|
|
489
|
+
one email are collected into a single `POST …/permission_filters`, so the CSV
|
|
490
|
+
must list that user's *complete* desired filter set (re-run idempotency depends
|
|
491
|
+
on it). Grants (`table`/`element`/`role`) are additive.
|
|
492
|
+
- **Per-row isolation on apply:** a failed row is reported, the batch continues,
|
|
493
|
+
and any failure exits non-zero. A corrected re-run is safe for `table`,
|
|
494
|
+
`element`, and `role` rows (additive); a `filter` re-run must still carry each
|
|
495
|
+
user's *complete* set, since the full replace deletes any omitted filter.
|
|
496
|
+
- Requires an org-admin / supporting-manager / contributor operator (same as the
|
|
497
|
+
per-item grants). Ready template: `docs/examples/users-bulk-permissions.csv`.
|
|
498
|
+
|
|
499
|
+
## Organizations
|
|
500
|
+
|
|
501
|
+
Alias `orgs`.
|
|
502
|
+
|
|
503
|
+
- `organizations list [--mini]`, `organizations get`.
|
|
504
|
+
- `organizations contacts` — per-tenant user directory (the source for
|
|
505
|
+
share-with-user pickers); `role` is `ADMIN|CONTRIBUTOR|VIEWER`.
|
|
506
|
+
- `organizations permission-groups` — **lists** named user bundles (same role
|
|
507
|
+
enum + `members[]`). Read-only (`GET /api/organizations/permission_groups`).
|
|
508
|
+
- `organizations create-group --org <id> --name <n> [--role <VIEWER|CONTRIBUTOR|ADMIN>]
|
|
509
|
+
[--member <email> ...]` — **creates** a permission group via
|
|
510
|
+
`POST /api/it/permission_groups` (the IT app — a different endpoint from the
|
|
511
|
+
list above). `--org` is sent in the body and must be one of your accessible
|
|
512
|
+
tenants (else 403); `--name` is unique per org. `--role` is upper-cased;
|
|
513
|
+
`--member` is repeatable and each must be accessible to you, in the target org,
|
|
514
|
+
and not outrank `--role`. **Dry-run unless `--confirm`**; `--json` on both
|
|
515
|
+
paths. Requires an org-admin / supporting-manager / contributor operator. This
|
|
516
|
+
is the group-creation the web UI's Permission Groups settings does — and the
|
|
517
|
+
way to pre-create groups that `users bulk-create` then references.
|
|
518
|
+
- `organizations update-group <id> --name <n>` — **renames** a group
|
|
519
|
+
(`PUT /api/it/permission_groups/<id>`). The IT update endpoint is **name-only**:
|
|
520
|
+
role is immutable and members are managed separately, so this changes the name
|
|
521
|
+
and nothing else. **Dry-run unless `--confirm`**; `--json` too. The server 403s
|
|
522
|
+
a rename of a default/system group (`is_editable:false`).
|
|
523
|
+
- `organizations delete-group <id>` — **deletes** a group
|
|
524
|
+
(`DELETE /api/it/permission_groups/<id>`), behind the **two-step confirm-token**
|
|
525
|
+
flow like every other `delete`: the first run prints the group's
|
|
526
|
+
name/role/member count and a token; re-run with `--confirm-token <tok>`. Hard
|
|
527
|
+
delete that also removes the group's access to shared assets (members
|
|
528
|
+
themselves remain in Datarails). Prod-gate **Tier A**.
|
|
529
|
+
- `organizations bulk-groups --file <csv> [--organization <id>] [--timeout <seconds>]` — **provisions
|
|
530
|
+
many groups + membership from one CSV** (header `group_name,role,members`,
|
|
531
|
+
exactly those three columns; `members` is `;`-separated emails — a stray comma
|
|
532
|
+
is rejected, not silently truncated, and duplicate emails are deduped
|
|
533
|
+
case-insensitively). Each row is classified against the tenant's existing
|
|
534
|
+
groups: an unknown name is **created** (`create-group`), a known name is
|
|
535
|
+
**augmented** — its listed members are added additively
|
|
536
|
+
(`POST /api/it/users/<id>/permission_groups`, a no-op for existing members).
|
|
537
|
+
`role` applies **only on create** (it's immutable on an existing group); a
|
|
538
|
+
blank `role` lets the server assign its default on create, and on augment
|
|
539
|
+
leaves the existing role untouched. Resolves everything first and **fails loud
|
|
540
|
+
before any write**: a member email not on the tenant, an ambiguous existing
|
|
541
|
+
group name (matches more than one group id), or a `role` cell that disagrees
|
|
542
|
+
with an existing group's role, aborts the whole run. **Dry-run unless
|
|
543
|
+
`--confirm`** — the sole write gate: this is additive + reversible, so unlike
|
|
544
|
+
`delete-group` it is *not* behind the two-step confirm-token flow. `--json` on
|
|
545
|
+
both paths; per-row isolation on apply (a failed row is reported, the batch
|
|
546
|
+
continues, non-zero exit if any failed). `--organization` is only needed to
|
|
547
|
+
place *created* groups, and only when it can't be derived from the tenant's
|
|
548
|
+
existing groups (none exist, or they span multiple orgs); an augment-only run
|
|
549
|
+
never requires it. Requires an org-admin / supporting-manager / contributor
|
|
550
|
+
operator (same as `create-group`). Ready template:
|
|
551
|
+
`docs/examples/organizations-bulk-groups.csv`.
|
|
552
|
+
- `organizations push-targets` — the organizations this account may push a
|
|
553
|
+
dashboard or filebox into. **Run this before `dashboards push` / `filebox
|
|
554
|
+
push`**; those commands reject any destination not on this list.
|
|
555
|
+
- Requires admin or supporting-manager rights; a 403 surfaces verbatim.
|
|
556
|
+
- What you get back depends on the signed-in org: a **template** org
|
|
557
|
+
(`use_as_template=true`) sees its sibling orgs and their descendants; any
|
|
558
|
+
other org sees its own descendants. Both are full subtrees, not just direct
|
|
559
|
+
children. An empty list means there is nowhere to push.
|
|
560
|
+
- The list does **not** carry customer status, so it can include an inactive
|
|
561
|
+
org. `filebox push` to an `Opt-Out`/`Churn`/`On Hold` org is rejected by the
|
|
562
|
+
server with that reason.
|
|
563
|
+
- `organizations add --domain <d> [--name <n>] [--license-type <l>]
|
|
564
|
+
[--category <c>] [--from-template <org_id>] [--use-as-template]
|
|
565
|
+
[--include-input-versions] [--payload-file <p>]` — a 403 (insufficient
|
|
566
|
+
permission) is surfaced verbatim; re-impersonate as needed.
|
|
567
|
+
- `--from-template <org_id>` clones the new tenant from an existing org; the
|
|
568
|
+
source must already have `use_as_template=true` on its row (else 400). There
|
|
569
|
+
is no CLI surface to flip that flag.
|
|
570
|
+
- `--include-input-versions` clones input file versions too (requires
|
|
571
|
+
`--from-template`; the lone flag is rejected client-side).
|
|
572
|
+
- The clone runs **asynchronously** — the POST returns 201 with a sparse org
|
|
573
|
+
row immediately; the new tenant's data lands a few minutes later.
|
|
574
|
+
- `--use-as-template` flags the **new** org as a template (off by default).
|
|
575
|
+
|
|
576
|
+
`support-manager list` (alias `sm`).
|
|
577
|
+
|
|
578
|
+
## Organization export / import (full-org migration)
|
|
579
|
+
|
|
580
|
+
Move or duplicate a whole organization — same server under a new domain, or
|
|
581
|
+
across servers (e.g. US → EU). Wraps `POST|DELETE /api/organizations/export`
|
|
582
|
+
and `POST|DELETE /api/organizations/import` (the org clone-over-blob
|
|
583
|
+
pipeline). **Requires a datarails.com admin or supporting-manager identity on
|
|
584
|
+
every server involved** — a 403 means the active account isn't one (check
|
|
585
|
+
`dr whoami`).
|
|
586
|
+
|
|
587
|
+
**The completion contract (read this first).** Both POSTs return 200
|
|
588
|
+
immediately and run server-side as background chains. **Completion is
|
|
589
|
+
signaled ONLY by email** to the triggering operator
|
|
590
|
+
(`EXPORT/IMPORT_ORGANIZATION_SUCCESS/WARNING/FAILED`) — there is **no status
|
|
591
|
+
API to poll**, so there is no `--wait`. Do not chain export → import in one
|
|
592
|
+
breath: trigger the export, stop, and only import after the export email
|
|
593
|
+
confirms success. The imported org's row appears immediately, but the tenant
|
|
594
|
+
is **not usable** until its email arrives — a "finished" import may still be
|
|
595
|
+
partial (per-phase failures don't abort the run; the WARNING email lists
|
|
596
|
+
what was skipped).
|
|
597
|
+
|
|
598
|
+
The flow:
|
|
599
|
+
|
|
600
|
+
1. **Export (on the source server):**
|
|
601
|
+
`organizations export create --domain <src> --new-domain <dest>
|
|
602
|
+
[--keep-user-passwords] [--allow-foreign-server] [--exported-before]`
|
|
603
|
+
- `--new-domain` is the full identity of the copy — it becomes both the
|
|
604
|
+
new org's domain AND its display name, and names the blob folder the
|
|
605
|
+
import reads. Omit only when re-importing under the same domain.
|
|
606
|
+
- **Passwords are hands-off.** A renamed export automatically randomizes
|
|
607
|
+
EVERY user password in the imported org — a fresh cryptographic secret
|
|
608
|
+
per org, sent to the server and recorded nowhere. The copy is simply not
|
|
609
|
+
reachable with knowable credentials; Datarails staff access it via
|
|
610
|
+
impersonation. For a REAL migration (users must keep logging in), pass
|
|
611
|
+
`--keep-user-passwords` to preserve source-org credentials.
|
|
612
|
+
- `--allow-foreign-server` is **required when the source server is not US**
|
|
613
|
+
and whenever the export will be imported on another server.
|
|
614
|
+
- A repeat export is rejected until you either pass `--exported-before`
|
|
615
|
+
(resume) or `export delete` the stale one — the error says which.
|
|
616
|
+
2. **Wait for the export email.** No email = still running (or the chain
|
|
617
|
+
broke — a hard failure can die without any email; check with the RnD team
|
|
618
|
+
or Groundcover if it's been hours).
|
|
619
|
+
3. **Import (on the target server):**
|
|
620
|
+
`organizations import run --domain <dest> [--temporary] [--no-skip-models]
|
|
621
|
+
[--imported-before]`
|
|
622
|
+
- `--domain` is always the **domain chosen at export** (the `--new-domain`
|
|
623
|
+
from step 1), NOT the source domain — it names the blob folder.
|
|
624
|
+
- Imports are **permanent by default**. `--temporary` opts into a
|
|
625
|
+
throwaway copy marked for auto-deletion by the server GC.
|
|
626
|
+
- Unknown model types in the blob are **skipped by default** (typical
|
|
627
|
+
version skew between servers; the completion email lists anything
|
|
628
|
+
skipped). `--no-skip-models` makes the import fail instead when the
|
|
629
|
+
blob carries model types this server cannot import.
|
|
630
|
+
4. **Wait for the import email**, verify the tenant, then — **back on the
|
|
631
|
+
source server** — clean up:
|
|
632
|
+
`organizations export delete --domain <src> [--new-domain <dest>] --server <source>`
|
|
633
|
+
— removes the export record + blob only (org data untouched; two-step
|
|
634
|
+
confirm, Tier B). The source org must still exist on the server you call,
|
|
635
|
+
so clean up the export **before** ever deleting the source org (otherwise
|
|
636
|
+
the record is unreachable via this API and only the ~60-day server GC
|
|
637
|
+
removes it). `--allow-foreign-server` is only needed when deleting a blob
|
|
638
|
+
from a different env than the one that created it — not for the normal
|
|
639
|
+
same-server cleanup.
|
|
640
|
+
|
|
641
|
+
**Desk is NOT part of this clone today.** The full org export/import copies
|
|
642
|
+
Desk config only when the server lists `DESK_AI` in `LIST_OF_MICROSERVICES`
|
|
643
|
+
(pending monolith work); tickets, teams and their members are not. To provision
|
|
644
|
+
Desk in a demo org, run the org clone (or use an existing org) and then
|
|
645
|
+
[`dr desk clone`](desk.md#clone-a-reference-orgs-desk-into-another-org-desk-clone-dr-52763),
|
|
646
|
+
which matches-or-creates the users, teams and Desk data from a reference org
|
|
647
|
+
without touching anything else.
|
|
648
|
+
|
|
649
|
+
**Undo an import:** `organizations import delete-orgs --domain <dest>` —
|
|
650
|
+
**PERMANENTLY DELETES the imported organization** (the full org-delete flow,
|
|
651
|
+
all tenant data destroyed). Two-step confirm, prod word-gate (**Tier A**).
|
|
652
|
+
The server only accepts domains created via the import API; anything else
|
|
653
|
+
must go through the standard permanent-delete flow. The "successfully
|
|
654
|
+
deleted" response acks the **dispatch** — deletion itself runs
|
|
655
|
+
asynchronously, so the org can linger in `organizations list` for a few
|
|
656
|
+
minutes before disappearing. The export blob is not touched — that cleanup
|
|
657
|
+
is `export delete`.
|
|
658
|
+
|
|
659
|
+
- `devices list`, `devices get`, `devices logs`,
|
|
660
|
+
`devices received-logs <machine_id>`, `devices commands <machine_id>`.
|
|
661
|
+
- `devices request-logs <id>…`, `devices download-logs`,
|
|
662
|
+
`devices command-delete <cmd_id>` (two-step confirm-token flow).
|
|
663
|
+
|
|
664
|
+
## Feature flags (IT)
|
|
665
|
+
|
|
666
|
+
- `features list`, `features groups list`, `features groups get`,
|
|
667
|
+
`features orgs list <org_id>`.
|
|
668
|
+
- `features orgs add <org_id> --flag …`, `features orgs remove <org_id> --flag …`
|
|
669
|
+
(both two-step confirm-token flow — flipping flags changes customer-visible
|
|
670
|
+
behavior).
|
|
671
|
+
|
|
672
|
+
**Know the flag name? Skip the listing entirely.** `--flag` accepts a flag **id
|
|
673
|
+
or name** (repeatable), and the server resolves it — so to enable/disable a flag
|
|
674
|
+
you already know, go straight to `features orgs add <org_id> --flag <name>`. Only
|
|
675
|
+
reach for `features list` when you need to *discover* or confirm a flag's exact
|
|
676
|
+
name.
|
|
677
|
+
|
|
678
|
+
**The two list shapes differ — don't confuse them:**
|
|
679
|
+
|
|
680
|
+
- `features list` (the global catalog) is **paginated** — the `--json` is a DRF
|
|
681
|
+
envelope `{count, next, previous, results}` and returns only **one page**
|
|
682
|
+
(default 50; there are 270+ flags). A name grep on the default page silently
|
|
683
|
+
finds nothing for anything past #50. Pull the whole catalog in one shot with
|
|
684
|
+
`--page-size 500` (max 500), or walk `--page <n>`. This paginates in **both**
|
|
685
|
+
table and `--json` mode; the table footer says "Showing 1-50 of N (more
|
|
686
|
+
available — bump --page)".
|
|
687
|
+
- `features orgs list <org_id>` is a **plain JSON array** of exactly the flags
|
|
688
|
+
**enabled/overridden for that org** — a flag's *presence* in this list is what
|
|
689
|
+
tells you it's on for the org. The `default_on` field on each row is the
|
|
690
|
+
flag's **global default**, NOT the org state (a flag can be enabled for the
|
|
691
|
+
org while `default_on:false`). To confirm an `orgs add` worked, check the flag
|
|
692
|
+
now *appears* here — don't read `default_on`.
|
|
693
|
+
|
|
694
|
+
## Semantic tables / FX (finance-os)
|
|
695
|
+
|
|
696
|
+
- `semantictables list|get|data|distinct` (alias `semtables`).
|
|
697
|
+
- `fx rates`.
|
|
698
|
+
|
|
699
|
+
### FX configuration (bulk enablement)
|
|
700
|
+
|
|
701
|
+
- `fx set-posting <template_id> [--field <id|name>]` — mark the table's Monetary
|
|
702
|
+
**posting** amount (`fx_role: posting_amount`). Defaults to the field named
|
|
703
|
+
"Posting Amount" (case/underscore-insensitive). Refuses a second posting field.
|
|
704
|
+
Two-step confirm (`--confirm-token`); Tier B on prod.
|
|
705
|
+
- `fx set-reporting <template_id>` — mark Monetary **reporting** amount(s) with
|
|
706
|
+
currency code + symbol. Three forms:
|
|
707
|
+
- no flags: heuristic, in this precedence — (1) a field already carrying a
|
|
708
|
+
reporting currency keeps it (so a re-run is a no-op, and such a field is listed
|
|
709
|
+
whatever it is named); (2) a currency in the field NAME, read wherever it sits —
|
|
710
|
+
`USD Amount`, `Amount USD`, `Reporting Amount (EUR)`, `Amount in GBP`,
|
|
711
|
+
`Reporting_Amount_USD` all work; (3) a single bare `Amount` / `Reporting Amount`
|
|
712
|
+
takes its code from the table's "Reporting Currency" column when that column
|
|
713
|
+
holds exactly one distinct value (otherwise the error tells you to map
|
|
714
|
+
explicitly). A name that contradicts the stored currency is refused, not guessed.
|
|
715
|
+
- `--field <id|name> --code <CODE> [--symbol <S>]` for one field;
|
|
716
|
+
- repeatable `--map "<field>=<CODE>[:<SYMBOL>]"` for several.
|
|
717
|
+
Symbols default from the FE's ISO catalog; codes validate against the catalog +
|
|
718
|
+
org custom currencies (`--allow-custom` bypasses, and then **registers** the
|
|
719
|
+
unknown code — see below). One reporting field per currency per table is
|
|
720
|
+
enforced. Two-step confirm.
|
|
721
|
+
- **`--all-tables`** on both field commands sweeps every table in the org instead of
|
|
722
|
+
taking a template id — the onboarding shape. A table it cannot judge is reported and
|
|
723
|
+
**skipped, never guessed**: the plan separates tables that need a human decision
|
|
724
|
+
(ambiguous currency, two posting candidates, a currency collision) from tables that
|
|
725
|
+
simply are not FX tables, which are counted rather than listed. One confirm token
|
|
726
|
+
covers the whole sweep. `--field`/`--map`/`--code` name one table's fields, so they
|
|
727
|
+
cannot be combined with it; single-table mode still raises the detailed error with the
|
|
728
|
+
field roster.
|
|
729
|
+
- `fx default-currency [<CODE>] [--none] [--confirm]` — show or set the **org-wide
|
|
730
|
+
default** reporting currency (`GET/PUT /api/v1/currencies/default-currency`), i.e. the
|
|
731
|
+
star in the dashboard dropdown. With no argument it prints the current default plus
|
|
732
|
+
every currency the org can offer (`organization-currencies`) — the answer to "what will
|
|
733
|
+
the selector show". `--none` records the "None" choice, meaning the posting currency.
|
|
734
|
+
A code the org cannot offer is refused (`--allow-unlisted` overrides), because a
|
|
735
|
+
default the selector cannot serve leaves every dashboard on a currency with no data.
|
|
736
|
+
Dry-run by default. **Setting it needs Super Admin server-side**, and the server has
|
|
737
|
+
the last word — the command reports what was stored, not what was sent. This matters
|
|
738
|
+
beyond the UI: an AI agent with no currency of its own uses this default.
|
|
739
|
+
- `fx add-currency <CODE> [--symbol <S>]` — register an org-level custom currency
|
|
740
|
+
(`POST /api/v1/currencies/custom-currencies`), i.e. a code the ISO list doesn't
|
|
741
|
+
carry, such as `BUSD` for "budgeted USD" (compare actuals to budget without FX
|
|
742
|
+
noise). 2-5 letters/digits, upper-cased; re-running with a different `--symbol`
|
|
743
|
+
updates it, mirroring the web app. An ISO code is refused — there is nothing to
|
|
744
|
+
register. Not gated (additive org config, same as `cash currencies set-prefs`).
|
|
745
|
+
|
|
746
|
+
> **Why registration matters.** The web app registers a custom code the moment
|
|
747
|
+
> someone uses it, so the org's custom-currency list and the field's
|
|
748
|
+
> `currency_code` never drift. `set-reporting --allow-custom` therefore registers
|
|
749
|
+
> an unknown code itself, **before** writing the field, and aborts without touching
|
|
750
|
+
> any field if that fails — a field pointing at a code the org doesn't know is the
|
|
751
|
+
> exact state this avoids (the Tables dialog stops offering it). Use
|
|
752
|
+
> `fx add-currency` to pre-register a code before a bulk run, or to fix a symbol.
|
|
753
|
+
- `fx convert-functions [--template <id>] [--force-sum] [--off] [--confirm]` — set
|
|
754
|
+
`is_fx_aware: true` on every Excel/DR.GET function whose value field is Monetary
|
|
755
|
+
**and carries an fx_role**. Non-SUM functions are skipped unless `--force-sum`
|
|
756
|
+
switches them. **Dry-run by default** — always run once without `--confirm` and
|
|
757
|
+
read the plan.
|
|
758
|
+
- `fx convert-widgets (--dashboard <id> | --all) [--force-sum] [--off] [--confirm]` —
|
|
759
|
+
same sweep for dashboard widgets (eligible = exactly one value field, Monetary,
|
|
760
|
+
with an fx_role). The server has the final say per widget; an echo that doesn't
|
|
761
|
+
confirm the flag is reported as a failure, not silently accepted. Dry-run by
|
|
762
|
+
default.
|
|
763
|
+
|
|
764
|
+
**Eligibility is stricter server-side than in the web app.** `FxEligibilityService`
|
|
765
|
+
requires the value field to be Monetary AND to have an `fx_role`; the serializer
|
|
766
|
+
answers a flip on a role-less field with a **400**, not a silent skip. Both
|
|
767
|
+
converters therefore report those as `needs-fx-role` and skip them — run
|
|
768
|
+
`fx set-posting` / `fx set-reporting` first. Chain the four commands in that order:
|
|
769
|
+
tag the posting field, tag the reporting fields, then sweep widgets and functions.
|
|
770
|
+
|
|
771
|
+
**Rollback — `--off`.** Both converters take `--off` to sweep the other way and turn
|
|
772
|
+
conversion off. It acts on the stored flag alone: eligibility and the aggregator are
|
|
773
|
+
irrelevant to a rollback, and the aggregator is never touched (so `--off --force-sum`
|
|
774
|
+
is refused). This deliberately reaches targets the enable direction would call
|
|
775
|
+
ineligible — a widget whose value section has since gained a second field, or a
|
|
776
|
+
function whose field lost its Monetary type, can strand a flag that nothing else can
|
|
777
|
+
clear. Dry-run by default, same as the enable direction.
|
|
778
|
+
|
|
779
|
+
## Knowledge base
|
|
780
|
+
|
|
781
|
+
`kb search|get|categories|sections|articles-in-section|recent|attachments|article-images`.
|
|
782
|
+
|
|
783
|
+
## MCP & skills (the binary's own surface)
|
|
784
|
+
|
|
785
|
+
- `mcp register|unregister|list|serve`.
|
|
786
|
+
- `skill list` (file count + total bytes per skill),
|
|
787
|
+
`skill print [name] [--file <relPath>] [--hash]` (defaults to `dr-cli`'s router
|
|
788
|
+
`SKILL.md`; `--file reference/<x>.md` prints a reference file),
|
|
789
|
+
`skill install <target_dir>`, `skill init-project [dir]`.
|
|
790
|
+
- `update` (top-level) — self-upgrade the CLI in place.
|
|
791
|
+
|
|
792
|
+
## Customer-facing docs (KB)
|
|
793
|
+
|
|
794
|
+
Help Center → the **"User & Permissions"**, **"Billing"**, and **"Security & Legal"** categories. Find current articles with `dr kb search "users permissions billing security"` (or
|
|
795
|
+
browse `dr kb categories` / `dr kb sections`). Cite the article; don't
|
|
796
|
+
paraphrase from memory.
|