vortex-cli 6.3.0__tar.gz → 6.4.0__tar.gz
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.
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/PKG-INFO +40 -1
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/README.md +39 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/pyproject.toml +1 -1
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/cli.py +58 -0
- vortex_cli-6.4.0/vortex/commands/keyword.py +309 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/gateway.py +20 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/main.py +12 -1
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/PKG-INFO +40 -1
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/SOURCES.txt +1 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/LICENSE +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/setup.cfg +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/__init__.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/__main__.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/colour.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/__init__.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/agenda.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/clean.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/clone.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/code.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/compile.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/config.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/copy.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/db.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/delete.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/docs.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/execute.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/export.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/find.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/grep.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/import_.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/libs.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/list.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/log.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/new.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/pull.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/push.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/render.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/schema.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/status.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/undo.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/use.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/commands/watch.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/constants.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/docs/Blackbook v2.md +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/docs/Blackbook.pdf +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/docs/index.html +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/docs/marked.min.js +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/lib/puakma-6.0.40.jar +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/libs.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/logging.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/models.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/soap.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/spinner.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/util.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/webdesign.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex/workspace.py +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/dependency_links.txt +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/entry_points.txt +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/requires.txt +0 -0
- {vortex_cli-6.3.0 → vortex_cli-6.4.0}/vortex_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vortex_cli
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.4.0
|
|
4
4
|
Summary: Vortex CLI
|
|
5
5
|
Author-email: Jordan Amos <jordan.amos@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -182,6 +182,7 @@ For a full list of commands see `--help`.
|
|
|
182
182
|
- `find`: Find Design Objects of cloned applications by name.
|
|
183
183
|
- `grep`: Search the contents of cloned Design Objects using a Regular Expression.
|
|
184
184
|
- `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
|
|
185
|
+
- `keyword` (or `kw`): Read or update an application's Keyword values. `vortex keyword 9` lists them, `-n <Name>` filters to one, and `-n <Name> --values ...` updates it, printing the prior value beside the new one before asking to continue. Secret-looking names are redacted unless `--reveal` is given - see [Keyword values and secrets](#keyword-values-and-secrets).
|
|
185
186
|
- `copy`: Copy a Design Object from one application to another.
|
|
186
187
|
- `delete`: Delete Design Objects by ID.
|
|
187
188
|
- `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
|
|
@@ -191,6 +192,42 @@ For a full list of commands see `--help`.
|
|
|
191
192
|
- `docs`: Open the Tornado Server Blackbook.
|
|
192
193
|
- `execute`: Execute a command on the server.
|
|
193
194
|
|
|
195
|
+
### Keyword values and secrets
|
|
196
|
+
|
|
197
|
+
Keywords are an application's live configuration, and in practice they hold credentials in
|
|
198
|
+
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
|
|
199
|
+
**redacts by default**, following the same precedent as `vortex config --show-server`:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
vortex keyword 9 # list every Keyword (secrets redacted)
|
|
203
|
+
vortex keyword 9 -n AppVersion # just this one
|
|
204
|
+
vortex keyword 9 -n AppVersion --values 3.7.0 # update it
|
|
205
|
+
vortex keyword 9 --reveal # print secret values in full
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- A Keyword is treated as secret when its **name** contains a marker such as `password`,
|
|
209
|
+
`passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
|
|
210
|
+
`keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
|
|
211
|
+
is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
|
|
212
|
+
`cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
|
|
213
|
+
camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
|
|
214
|
+
`Monkey` and `Concert` are not. The match is on the name only - a value-based heuristic
|
|
215
|
+
would leak the thing it is trying to hide on every near miss. Classification is
|
|
216
|
+
deliberately biased towards over-redaction: a false positive costs one `--reveal`, a
|
|
217
|
+
false negative prints a live credential.
|
|
218
|
+
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
|
|
219
|
+
- `--reveal` is the explicit opt-in. Without it, neither a listing nor an update's
|
|
220
|
+
before/after diff can put a live credential into your terminal scrollback, your shell
|
|
221
|
+
history or a CI log.
|
|
222
|
+
|
|
223
|
+
An update always prints the prior value beside the new one and waits for `[Y/y]` on stdin
|
|
224
|
+
before writing; on a `protected = true` server the server name must also be typed back
|
|
225
|
+
(`--yes` does not bypass that). The write uses the gateway's keyword upsert, so it never
|
|
226
|
+
inserts a duplicate KEYWORD row.
|
|
227
|
+
|
|
228
|
+
Reading does **not** require the application to be cloned locally - diagnosing a bad
|
|
229
|
+
configuration value should not depend on having a working copy.
|
|
230
|
+
|
|
194
231
|
### Cloning a whole group
|
|
195
232
|
|
|
196
233
|
`vortex clone` takes an ID, a TemplateName, an application group, or `group/name`:
|
|
@@ -365,6 +402,8 @@ needs no change to the gateway.
|
|
|
365
402
|
| `copy`, `new object` | webdesign `POST`/`PUT design`, `PUT design/params` | SOAP |
|
|
366
403
|
| `new app` | webdesign `POST vortex` | SOAP `saveApplication` |
|
|
367
404
|
| `new keyword` | gateway `keyword` (upsert) | SOAP `saveKeyword` |
|
|
405
|
+
| `keyword` (read) | gateway `download` (metadata window; `GatewayDesignRead`) | **unsupported** - SOAP has no keyword read call |
|
|
406
|
+
| `keyword --values` (write) | gateway `keyword` (upsert) | **unsupported** |
|
|
368
407
|
| `schema`, `db --list`, `db --schema` | gateway `dictionary` (webdesign's `database`/`table`/`column` routes run server-side; `GatewayDBRead`/`GatewayDBWrite`) | SOAP SQL + `savePuakma*` |
|
|
369
408
|
| `export` | webdesign `ExportPMX` action | SOAP `downloadPmx` |
|
|
370
409
|
| `import` | **still SOAP** - webdesign has no import route yet | SOAP `uploadPmx` |
|
|
@@ -138,6 +138,7 @@ For a full list of commands see `--help`.
|
|
|
138
138
|
- `find`: Find Design Objects of cloned applications by name.
|
|
139
139
|
- `grep`: Search the contents of cloned Design Objects using a Regular Expression.
|
|
140
140
|
- `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
|
|
141
|
+
- `keyword` (or `kw`): Read or update an application's Keyword values. `vortex keyword 9` lists them, `-n <Name>` filters to one, and `-n <Name> --values ...` updates it, printing the prior value beside the new one before asking to continue. Secret-looking names are redacted unless `--reveal` is given - see [Keyword values and secrets](#keyword-values-and-secrets).
|
|
141
142
|
- `copy`: Copy a Design Object from one application to another.
|
|
142
143
|
- `delete`: Delete Design Objects by ID.
|
|
143
144
|
- `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
|
|
@@ -147,6 +148,42 @@ For a full list of commands see `--help`.
|
|
|
147
148
|
- `docs`: Open the Tornado Server Blackbook.
|
|
148
149
|
- `execute`: Execute a command on the server.
|
|
149
150
|
|
|
151
|
+
### Keyword values and secrets
|
|
152
|
+
|
|
153
|
+
Keywords are an application's live configuration, and in practice they hold credentials in
|
|
154
|
+
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
|
|
155
|
+
**redacts by default**, following the same precedent as `vortex config --show-server`:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
vortex keyword 9 # list every Keyword (secrets redacted)
|
|
159
|
+
vortex keyword 9 -n AppVersion # just this one
|
|
160
|
+
vortex keyword 9 -n AppVersion --values 3.7.0 # update it
|
|
161
|
+
vortex keyword 9 --reveal # print secret values in full
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- A Keyword is treated as secret when its **name** contains a marker such as `password`,
|
|
165
|
+
`passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
|
|
166
|
+
`keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
|
|
167
|
+
is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
|
|
168
|
+
`cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
|
|
169
|
+
camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
|
|
170
|
+
`Monkey` and `Concert` are not. The match is on the name only - a value-based heuristic
|
|
171
|
+
would leak the thing it is trying to hide on every near miss. Classification is
|
|
172
|
+
deliberately biased towards over-redaction: a false positive costs one `--reveal`, a
|
|
173
|
+
false negative prints a live credential.
|
|
174
|
+
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
|
|
175
|
+
- `--reveal` is the explicit opt-in. Without it, neither a listing nor an update's
|
|
176
|
+
before/after diff can put a live credential into your terminal scrollback, your shell
|
|
177
|
+
history or a CI log.
|
|
178
|
+
|
|
179
|
+
An update always prints the prior value beside the new one and waits for `[Y/y]` on stdin
|
|
180
|
+
before writing; on a `protected = true` server the server name must also be typed back
|
|
181
|
+
(`--yes` does not bypass that). The write uses the gateway's keyword upsert, so it never
|
|
182
|
+
inserts a duplicate KEYWORD row.
|
|
183
|
+
|
|
184
|
+
Reading does **not** require the application to be cloned locally - diagnosing a bad
|
|
185
|
+
configuration value should not depend on having a working copy.
|
|
186
|
+
|
|
150
187
|
### Cloning a whole group
|
|
151
188
|
|
|
152
189
|
`vortex clone` takes an ID, a TemplateName, an application group, or `group/name`:
|
|
@@ -321,6 +358,8 @@ needs no change to the gateway.
|
|
|
321
358
|
| `copy`, `new object` | webdesign `POST`/`PUT design`, `PUT design/params` | SOAP |
|
|
322
359
|
| `new app` | webdesign `POST vortex` | SOAP `saveApplication` |
|
|
323
360
|
| `new keyword` | gateway `keyword` (upsert) | SOAP `saveKeyword` |
|
|
361
|
+
| `keyword` (read) | gateway `download` (metadata window; `GatewayDesignRead`) | **unsupported** - SOAP has no keyword read call |
|
|
362
|
+
| `keyword --values` (write) | gateway `keyword` (upsert) | **unsupported** |
|
|
324
363
|
| `schema`, `db --list`, `db --schema` | gateway `dictionary` (webdesign's `database`/`table`/`column` routes run server-side; `GatewayDBRead`/`GatewayDBWrite`) | SOAP SQL + `savePuakma*` |
|
|
325
364
|
| `export` | webdesign `ExportPMX` action | SOAP `downloadPmx` |
|
|
326
365
|
| `import` | **still SOAP** - webdesign has no import route yet | SOAP `uploadPmx` |
|
|
@@ -198,6 +198,7 @@ def validate_args(
|
|
|
198
198
|
new_parser: ArgumentParser,
|
|
199
199
|
clone_parser: ArgumentParser,
|
|
200
200
|
db_parser: ArgumentParser,
|
|
201
|
+
keyword_parser: ArgumentParser,
|
|
201
202
|
) -> None:
|
|
202
203
|
if args.command == "new":
|
|
203
204
|
msg = None
|
|
@@ -265,6 +266,11 @@ def validate_args(
|
|
|
265
266
|
clone_parser.error(
|
|
266
267
|
f"--group expects group names, not {', '.join(not_groups)}"
|
|
267
268
|
)
|
|
269
|
+
elif args.command in ("keyword", "kw"):
|
|
270
|
+
# A write needs a target: '--values' with no '--name' would
|
|
271
|
+
# otherwise be a silent no-op, or worse, ambiguous
|
|
272
|
+
if args.values is not None and not args.name:
|
|
273
|
+
keyword_parser.error("--values requires --name")
|
|
268
274
|
elif args.command == "db" and args.sql is not None:
|
|
269
275
|
try:
|
|
270
276
|
args.database = int(args.database)
|
|
@@ -1006,6 +1012,58 @@ def add_new_parser(
|
|
|
1006
1012
|
return new_parser
|
|
1007
1013
|
|
|
1008
1014
|
|
|
1015
|
+
def add_keyword_parser(
|
|
1016
|
+
command_parser: _SubParsersAction[ArgumentParser],
|
|
1017
|
+
) -> ArgumentParser:
|
|
1018
|
+
keyword_parser = command_parser.add_parser(
|
|
1019
|
+
"keyword",
|
|
1020
|
+
aliases=("kw",),
|
|
1021
|
+
help=(
|
|
1022
|
+
"Read or update an application's Keyword values. With no "
|
|
1023
|
+
"--values it lists them; with --values it updates one, "
|
|
1024
|
+
"showing the prior value first (gateway only)"
|
|
1025
|
+
),
|
|
1026
|
+
)
|
|
1027
|
+
keyword_parser.add_argument(
|
|
1028
|
+
"app_id",
|
|
1029
|
+
metavar="APP_ID",
|
|
1030
|
+
type=int,
|
|
1031
|
+
help="The application id whose Keywords to read or update",
|
|
1032
|
+
)
|
|
1033
|
+
keyword_parser.add_argument(
|
|
1034
|
+
"--name",
|
|
1035
|
+
"-n",
|
|
1036
|
+
help=(
|
|
1037
|
+
"Only this Keyword (exact, case-insensitive). Required when "
|
|
1038
|
+
"--values is given"
|
|
1039
|
+
),
|
|
1040
|
+
)
|
|
1041
|
+
keyword_parser.add_argument(
|
|
1042
|
+
"--values",
|
|
1043
|
+
nargs="*",
|
|
1044
|
+
default=None,
|
|
1045
|
+
metavar="VALUE",
|
|
1046
|
+
help=(
|
|
1047
|
+
"Set --name to these value(s), replacing what is there. Pass "
|
|
1048
|
+
"with no values to set it empty. Requires --name"
|
|
1049
|
+
),
|
|
1050
|
+
)
|
|
1051
|
+
keyword_parser.add_argument(
|
|
1052
|
+
"--reveal",
|
|
1053
|
+
action="store_true",
|
|
1054
|
+
help=(
|
|
1055
|
+
"Print secret-looking Keyword values in full. Values of "
|
|
1056
|
+
"Keywords whose NAME looks like a credential (password, "
|
|
1057
|
+
"secret, key, token, auth, cert, hash, salt, pin, otp, "
|
|
1058
|
+
"webhook, connection string and similar) are redacted by "
|
|
1059
|
+
"default so a plain listing cannot leak a live credential "
|
|
1060
|
+
"into the terminal, shell history or a CI log"
|
|
1061
|
+
),
|
|
1062
|
+
)
|
|
1063
|
+
_add_server_option(keyword_parser)
|
|
1064
|
+
return keyword_parser
|
|
1065
|
+
|
|
1066
|
+
|
|
1009
1067
|
def add_copy_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
|
|
1010
1068
|
copy_parser = command_parser.add_parser(
|
|
1011
1069
|
"copy", help="Copy a Design Object from one application to another"
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import tabulate
|
|
8
|
+
|
|
9
|
+
from vortex.colour import Colour
|
|
10
|
+
from vortex.gateway import GatewayClient
|
|
11
|
+
from vortex.gateway import GatewayRefused
|
|
12
|
+
from vortex.gateway import GatewayUnavailable
|
|
13
|
+
from vortex.gateway import refusal_detail
|
|
14
|
+
from vortex.models import PuakmaServer
|
|
15
|
+
from vortex.util import confirm_protected
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("vortex")
|
|
18
|
+
|
|
19
|
+
_REFUSAL_HELP = {
|
|
20
|
+
"FORBIDDEN": (
|
|
21
|
+
"this identity does not hold the required gateway role on this "
|
|
22
|
+
"server (listing needs GatewayDesignRead; setting a value needs "
|
|
23
|
+
"GatewayDesignWrite - or Admin)"
|
|
24
|
+
),
|
|
25
|
+
"NOT_FOUND": "no application with that id on this server",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Keyword values are an application's live configuration and routinely
|
|
29
|
+
# hold credentials in cleartext (API keys, passwords, vendor secrets,
|
|
30
|
+
# signing keys). Printing them by default would put them in the terminal
|
|
31
|
+
# scrollback, the shell history and any CI log that captured the run, so
|
|
32
|
+
# a Keyword whose NAME matches this is redacted unless --reveal is given.
|
|
33
|
+
# This follows the gateway's own 'server_config' precedent, which
|
|
34
|
+
# redacts the server's secrets before they ever reach the wire.
|
|
35
|
+
# Long, unambiguous markers - matched anywhere in the name.
|
|
36
|
+
SECRET_SUBSTRINGS = (
|
|
37
|
+
"password",
|
|
38
|
+
"passwd",
|
|
39
|
+
"passphrase",
|
|
40
|
+
"secret",
|
|
41
|
+
"credential",
|
|
42
|
+
"signature",
|
|
43
|
+
"mnemonic",
|
|
44
|
+
"keystore",
|
|
45
|
+
"truststore",
|
|
46
|
+
"keyphrase",
|
|
47
|
+
"connectionstring",
|
|
48
|
+
"connstring",
|
|
49
|
+
"webhook",
|
|
50
|
+
"apikey",
|
|
51
|
+
"privatekey",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Short/ambiguous markers - matched only as a whole word of the name, so
|
|
55
|
+
# 'Monkey' and 'Spinner' are not mistaken for 'key' and 'pin'.
|
|
56
|
+
SECRET_WORDS = frozenset(
|
|
57
|
+
{
|
|
58
|
+
"pwd",
|
|
59
|
+
"token",
|
|
60
|
+
"tokens",
|
|
61
|
+
"salt",
|
|
62
|
+
"hash",
|
|
63
|
+
"key",
|
|
64
|
+
"keys",
|
|
65
|
+
"private",
|
|
66
|
+
"bearer",
|
|
67
|
+
"auth",
|
|
68
|
+
"authorization",
|
|
69
|
+
"authentication",
|
|
70
|
+
"cert",
|
|
71
|
+
"certificate",
|
|
72
|
+
"pem",
|
|
73
|
+
"pfx",
|
|
74
|
+
"jwt",
|
|
75
|
+
"dsn",
|
|
76
|
+
"otp",
|
|
77
|
+
"pin",
|
|
78
|
+
"seed",
|
|
79
|
+
"sig",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# camelCase / PascalCase / snake_case / kebab-case / ALLCAPS -> words
|
|
84
|
+
_WORD_PATTERN = re.compile(r"[A-Z]+(?![a-z])|[A-Z][a-z]*|[a-z]+|\d+")
|
|
85
|
+
|
|
86
|
+
REDACTED = "<redacted>"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def name_words(name: str) -> list[str]:
|
|
90
|
+
"""The name split into lowercased words, however it is cased."""
|
|
91
|
+
return [w.casefold() for w in _WORD_PATTERN.findall(name)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_secret_name(name: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
True when a Keyword name looks like it holds a credential. Matched
|
|
97
|
+
on the NAME only - a value-based heuristic would leak the very thing
|
|
98
|
+
it is trying to hide on every near miss.
|
|
99
|
+
|
|
100
|
+
Deliberately biased towards over-redaction: a false positive costs
|
|
101
|
+
one '--reveal', a false negative prints a live credential into the
|
|
102
|
+
terminal scrollback, the shell history and any CI log.
|
|
103
|
+
"""
|
|
104
|
+
folded = name.casefold()
|
|
105
|
+
if any(marker in folded for marker in SECRET_SUBSTRINGS):
|
|
106
|
+
return True
|
|
107
|
+
return any(word in SECRET_WORDS for word in name_words(name))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def redact_values(name: str, values: list[str], reveal: bool) -> list[str]:
|
|
111
|
+
"""
|
|
112
|
+
What to display for a Keyword: the real values, or one <redacted>
|
|
113
|
+
marker per value (the COUNT is safe and useful; the content is not).
|
|
114
|
+
"""
|
|
115
|
+
if reveal or not is_secret_name(name):
|
|
116
|
+
return list(values)
|
|
117
|
+
return [REDACTED for _ in values]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _format_values(values: list[str]) -> str:
|
|
121
|
+
return " | ".join(values)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _app_label(app: dict[str, Any], app_id: int) -> str:
|
|
125
|
+
group = str(app.get("group") or "")
|
|
126
|
+
name = str(app.get("name") or "")
|
|
127
|
+
label = f"{group}/{name}".strip("/")
|
|
128
|
+
return f"{label} [{app_id}]" if label else f"[{app_id}]"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fetch(
|
|
132
|
+
client: GatewayClient, app_id: int, name: str | None
|
|
133
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
134
|
+
app, rows = client.get_keywords(app_id)
|
|
135
|
+
if name is not None:
|
|
136
|
+
needle = name.casefold()
|
|
137
|
+
rows = [r for r in rows if str(r.get("name", "")).casefold() == needle]
|
|
138
|
+
return app, rows
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _client_or_error(server: PuakmaServer) -> GatewayClient | None:
|
|
142
|
+
"""
|
|
143
|
+
The gateway client, or None with the reason already logged. Keywords
|
|
144
|
+
are gateway-only here: the legacy SOAP designer has no keyword READ
|
|
145
|
+
call at all, which is exactly why reading one was impossible before
|
|
146
|
+
this command existed.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
client = server.negotiate_gateway()
|
|
150
|
+
except GatewayUnavailable as e:
|
|
151
|
+
logger.error(e)
|
|
152
|
+
return None
|
|
153
|
+
if client is None:
|
|
154
|
+
logger.error(
|
|
155
|
+
"'vortex keyword' needs the gateway backend "
|
|
156
|
+
f"(set 'backend = gateway' for '{server.name}') - the legacy "
|
|
157
|
+
"SOAP designer has no keyword read call"
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
return client
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def keyword(
|
|
164
|
+
server: PuakmaServer,
|
|
165
|
+
app_id: int,
|
|
166
|
+
*,
|
|
167
|
+
name: str | None = None,
|
|
168
|
+
values: list[str] | None = None,
|
|
169
|
+
reveal: bool = False,
|
|
170
|
+
yes: bool = False,
|
|
171
|
+
) -> int:
|
|
172
|
+
"""
|
|
173
|
+
Read or update an application's Keyword values.
|
|
174
|
+
|
|
175
|
+
With no --values this LISTS Keywords (all of them, or just --name).
|
|
176
|
+
With --values it UPDATES --name, showing the prior value beside the
|
|
177
|
+
new one before writing, so the change is visible rather than blind;
|
|
178
|
+
the write goes through the gateway's keyword upsert, which never
|
|
179
|
+
inserts a duplicate row.
|
|
180
|
+
|
|
181
|
+
The application does NOT need to be cloned locally - diagnosing a
|
|
182
|
+
bad configuration value must not require a working copy.
|
|
183
|
+
"""
|
|
184
|
+
if values is not None:
|
|
185
|
+
if not name:
|
|
186
|
+
# argparse rejects this first; belt-and-braces so a direct
|
|
187
|
+
# call cannot upsert a nameless Keyword
|
|
188
|
+
logger.error("--values requires --name")
|
|
189
|
+
return 1
|
|
190
|
+
return _set_keyword(server, app_id, name, values, reveal=reveal, yes=yes)
|
|
191
|
+
return _list_keywords(server, app_id, name, reveal=reveal)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _list_keywords(
|
|
195
|
+
server: PuakmaServer,
|
|
196
|
+
app_id: int,
|
|
197
|
+
name: str | None,
|
|
198
|
+
*,
|
|
199
|
+
reveal: bool,
|
|
200
|
+
) -> int:
|
|
201
|
+
with server as s:
|
|
202
|
+
client = _client_or_error(s)
|
|
203
|
+
if client is None:
|
|
204
|
+
return 1
|
|
205
|
+
try:
|
|
206
|
+
app, rows = _fetch(client, app_id, name)
|
|
207
|
+
except GatewayRefused as e:
|
|
208
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
209
|
+
return 1
|
|
210
|
+
except GatewayUnavailable as e:
|
|
211
|
+
logger.error(e)
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
label = _app_label(app, app_id)
|
|
215
|
+
if not rows:
|
|
216
|
+
if name:
|
|
217
|
+
logger.error(f"No Keyword named '{name}' in {label}")
|
|
218
|
+
return 1
|
|
219
|
+
print(f"[{server.name}] {label} has no Keywords.")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
redacted = 0
|
|
223
|
+
table = []
|
|
224
|
+
for row in sorted(rows, key=lambda r: str(r.get("name", "")).casefold()):
|
|
225
|
+
kw_name = str(row.get("name") or "")
|
|
226
|
+
raw = [str(v) for v in (row.get("values") or [])]
|
|
227
|
+
shown = redact_values(kw_name, raw, reveal)
|
|
228
|
+
if shown != raw:
|
|
229
|
+
redacted += 1
|
|
230
|
+
flag = "DUPLICATE" if row.get("duplicate") else ""
|
|
231
|
+
table.append([row.get("id"), kw_name, _format_values(shown), flag])
|
|
232
|
+
|
|
233
|
+
header = f"[{server.name}] {label} - {len(table)} Keyword(s)"
|
|
234
|
+
if redacted:
|
|
235
|
+
header += f", {redacted} redacted (--reveal to show)"
|
|
236
|
+
print(header)
|
|
237
|
+
print(tabulate.tabulate(table, headers=["ID", "Name", "Values", ""]))
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _set_keyword(
|
|
242
|
+
server: PuakmaServer,
|
|
243
|
+
app_id: int,
|
|
244
|
+
name: str,
|
|
245
|
+
values: list[str],
|
|
246
|
+
*,
|
|
247
|
+
reveal: bool,
|
|
248
|
+
yes: bool,
|
|
249
|
+
) -> int:
|
|
250
|
+
with server as s:
|
|
251
|
+
client = _client_or_error(s)
|
|
252
|
+
if client is None:
|
|
253
|
+
return 1
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
app, existing = _fetch(client, app_id, name)
|
|
257
|
+
except GatewayRefused as e:
|
|
258
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
259
|
+
return 1
|
|
260
|
+
except GatewayUnavailable as e:
|
|
261
|
+
logger.error(e)
|
|
262
|
+
return 1
|
|
263
|
+
|
|
264
|
+
prior = (
|
|
265
|
+
[str(v) for v in (existing[0].get("values") or [])] if existing else None
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Showing the prior value IS the feature: a blind write is how a
|
|
269
|
+
# bad Keyword value becomes an outage nobody can diagnose. A
|
|
270
|
+
# secret-looking name stays redacted even in the diff.
|
|
271
|
+
label = _app_label(app, app_id)
|
|
272
|
+
verb = (
|
|
273
|
+
Colour.colour("UPDATED", Colour.YELLOW)
|
|
274
|
+
if existing
|
|
275
|
+
else Colour.colour("CREATED", Colour.GREEN)
|
|
276
|
+
)
|
|
277
|
+
print(f"Keyword '{name}' in {label} will be {verb}:\n")
|
|
278
|
+
if prior is None:
|
|
279
|
+
print(" from: (does not exist)")
|
|
280
|
+
else:
|
|
281
|
+
print(f" from: {_format_values(redact_values(name, prior, reveal))}")
|
|
282
|
+
print(f" to: {_format_values(redact_values(name, values, reveal))}")
|
|
283
|
+
if prior is not None and prior == values:
|
|
284
|
+
print("\nThe Keyword already has this value; nothing to do.")
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
if not yes and input("\n[Y/y] to continue:") not in ["Y", "y"]:
|
|
288
|
+
logger.error("Operation Cancelled")
|
|
289
|
+
return 1
|
|
290
|
+
# Deliberately NOT bypassed by --yes: on a protected server the
|
|
291
|
+
# name must still be typed back
|
|
292
|
+
if not confirm_protected(s, f"update the Keyword '{name}'"):
|
|
293
|
+
return 1
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
reply = client.keyword_upsert(app_id, name, values)
|
|
297
|
+
except GatewayRefused as e:
|
|
298
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
299
|
+
return 1
|
|
300
|
+
except GatewayUnavailable as e:
|
|
301
|
+
logger.error(e)
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
dupes = int(reply.get("duplicatesConsolidated") or 0)
|
|
305
|
+
extra = f" ({dupes} duplicate row(s) consolidated)" if dupes else ""
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Upserted Keyword '{name}' [{reply.get('keywordId')}] via gateway{extra}"
|
|
308
|
+
)
|
|
309
|
+
return 0
|
|
@@ -631,6 +631,26 @@ class GatewayClient:
|
|
|
631
631
|
params["confirm"] = 1
|
|
632
632
|
return self._get("delete", params=params)
|
|
633
633
|
|
|
634
|
+
def get_keywords(self, app_id: int) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
635
|
+
"""
|
|
636
|
+
One app's row and its KEYWORD rows - name, values, id and
|
|
637
|
+
whether the name has duplicate rows (GatewayDesignRead).
|
|
638
|
+
|
|
639
|
+
The gateway ships no dedicated keyword read endpoint and needs
|
|
640
|
+
none: 'download' already returns the keywords, so this asks for
|
|
641
|
+
its metadata-only window ('ids=' selects no element content)
|
|
642
|
+
rather than requiring a server-side change. Values come back
|
|
643
|
+
RAW - redaction is the caller's job (commands.keyword).
|
|
644
|
+
"""
|
|
645
|
+
reply = self._get("download", params={"appId": app_id, "ids": ""})
|
|
646
|
+
try:
|
|
647
|
+
return dict(reply["app"]), [dict(row) for row in reply["keywords"]]
|
|
648
|
+
except (KeyError, TypeError) as e:
|
|
649
|
+
raise GatewayUnavailable(
|
|
650
|
+
f"Malformed download reply from {self._url('download')} "
|
|
651
|
+
"(no 'app'/'keywords')"
|
|
652
|
+
) from e
|
|
653
|
+
|
|
634
654
|
def keyword_upsert(
|
|
635
655
|
self, app_id: int, name: str, values: list[str]
|
|
636
656
|
) -> dict[str, Any]:
|
|
@@ -28,6 +28,7 @@ from vortex.commands.export import export
|
|
|
28
28
|
from vortex.commands.find import find
|
|
29
29
|
from vortex.commands.grep import grep
|
|
30
30
|
from vortex.commands.import_ import import_
|
|
31
|
+
from vortex.commands.keyword import keyword
|
|
31
32
|
from vortex.commands.libs import libs_
|
|
32
33
|
from vortex.commands.list import list_
|
|
33
34
|
from vortex.commands.log import log
|
|
@@ -259,13 +260,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
259
260
|
code_parser = cli.add_code_parser(command_parser)
|
|
260
261
|
db_parser = cli.add_db_parser(command_parser)
|
|
261
262
|
new_parser = cli.add_new_parser(command_parser)
|
|
263
|
+
keyword_parser = cli.add_keyword_parser(command_parser)
|
|
262
264
|
|
|
263
265
|
args, remaining_args = parser.parse_known_args(argv)
|
|
264
266
|
# Call this for initial Validation
|
|
265
267
|
if args.command != "code":
|
|
266
268
|
parser.parse_args(argv)
|
|
267
269
|
|
|
268
|
-
cli.validate_args(args, new_parser, clone_parser, db_parser)
|
|
270
|
+
cli.validate_args(args, new_parser, clone_parser, db_parser, keyword_parser)
|
|
269
271
|
|
|
270
272
|
if args.no_colour:
|
|
271
273
|
Colour.disable()
|
|
@@ -484,6 +486,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
484
486
|
"'new' command requires a sub command 'object', 'obj', 'app' or 'keyword' or 'kw'"
|
|
485
487
|
)
|
|
486
488
|
return new(workspace, server, args)
|
|
489
|
+
elif args.command in ("keyword", "kw"):
|
|
490
|
+
return keyword(
|
|
491
|
+
server,
|
|
492
|
+
args.app_id,
|
|
493
|
+
name=args.name,
|
|
494
|
+
values=args.values,
|
|
495
|
+
reveal=args.reveal,
|
|
496
|
+
yes=args.yes,
|
|
497
|
+
)
|
|
487
498
|
elif args.command == "delete":
|
|
488
499
|
return delete(workspace, server, obj_ids, yes=args.yes)
|
|
489
500
|
elif args.command == "copy":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vortex_cli
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.4.0
|
|
4
4
|
Summary: Vortex CLI
|
|
5
5
|
Author-email: Jordan Amos <jordan.amos@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -182,6 +182,7 @@ For a full list of commands see `--help`.
|
|
|
182
182
|
- `find`: Find Design Objects of cloned applications by name.
|
|
183
183
|
- `grep`: Search the contents of cloned Design Objects using a Regular Expression.
|
|
184
184
|
- `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
|
|
185
|
+
- `keyword` (or `kw`): Read or update an application's Keyword values. `vortex keyword 9` lists them, `-n <Name>` filters to one, and `-n <Name> --values ...` updates it, printing the prior value beside the new one before asking to continue. Secret-looking names are redacted unless `--reveal` is given - see [Keyword values and secrets](#keyword-values-and-secrets).
|
|
185
186
|
- `copy`: Copy a Design Object from one application to another.
|
|
186
187
|
- `delete`: Delete Design Objects by ID.
|
|
187
188
|
- `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
|
|
@@ -191,6 +192,42 @@ For a full list of commands see `--help`.
|
|
|
191
192
|
- `docs`: Open the Tornado Server Blackbook.
|
|
192
193
|
- `execute`: Execute a command on the server.
|
|
193
194
|
|
|
195
|
+
### Keyword values and secrets
|
|
196
|
+
|
|
197
|
+
Keywords are an application's live configuration, and in practice they hold credentials in
|
|
198
|
+
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
|
|
199
|
+
**redacts by default**, following the same precedent as `vortex config --show-server`:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
vortex keyword 9 # list every Keyword (secrets redacted)
|
|
203
|
+
vortex keyword 9 -n AppVersion # just this one
|
|
204
|
+
vortex keyword 9 -n AppVersion --values 3.7.0 # update it
|
|
205
|
+
vortex keyword 9 --reveal # print secret values in full
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- A Keyword is treated as secret when its **name** contains a marker such as `password`,
|
|
209
|
+
`passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
|
|
210
|
+
`keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
|
|
211
|
+
is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
|
|
212
|
+
`cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
|
|
213
|
+
camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
|
|
214
|
+
`Monkey` and `Concert` are not. The match is on the name only - a value-based heuristic
|
|
215
|
+
would leak the thing it is trying to hide on every near miss. Classification is
|
|
216
|
+
deliberately biased towards over-redaction: a false positive costs one `--reveal`, a
|
|
217
|
+
false negative prints a live credential.
|
|
218
|
+
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
|
|
219
|
+
- `--reveal` is the explicit opt-in. Without it, neither a listing nor an update's
|
|
220
|
+
before/after diff can put a live credential into your terminal scrollback, your shell
|
|
221
|
+
history or a CI log.
|
|
222
|
+
|
|
223
|
+
An update always prints the prior value beside the new one and waits for `[Y/y]` on stdin
|
|
224
|
+
before writing; on a `protected = true` server the server name must also be typed back
|
|
225
|
+
(`--yes` does not bypass that). The write uses the gateway's keyword upsert, so it never
|
|
226
|
+
inserts a duplicate KEYWORD row.
|
|
227
|
+
|
|
228
|
+
Reading does **not** require the application to be cloned locally - diagnosing a bad
|
|
229
|
+
configuration value should not depend on having a working copy.
|
|
230
|
+
|
|
194
231
|
### Cloning a whole group
|
|
195
232
|
|
|
196
233
|
`vortex clone` takes an ID, a TemplateName, an application group, or `group/name`:
|
|
@@ -365,6 +402,8 @@ needs no change to the gateway.
|
|
|
365
402
|
| `copy`, `new object` | webdesign `POST`/`PUT design`, `PUT design/params` | SOAP |
|
|
366
403
|
| `new app` | webdesign `POST vortex` | SOAP `saveApplication` |
|
|
367
404
|
| `new keyword` | gateway `keyword` (upsert) | SOAP `saveKeyword` |
|
|
405
|
+
| `keyword` (read) | gateway `download` (metadata window; `GatewayDesignRead`) | **unsupported** - SOAP has no keyword read call |
|
|
406
|
+
| `keyword --values` (write) | gateway `keyword` (upsert) | **unsupported** |
|
|
368
407
|
| `schema`, `db --list`, `db --schema` | gateway `dictionary` (webdesign's `database`/`table`/`column` routes run server-side; `GatewayDBRead`/`GatewayDBWrite`) | SOAP SQL + `savePuakma*` |
|
|
369
408
|
| `export` | webdesign `ExportPMX` action | SOAP `downloadPmx` |
|
|
370
409
|
| `import` | **still SOAP** - webdesign has no import route yet | SOAP `uploadPmx` |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|