Skip to content

Commit c156e2a

Browse files
authored
Merge pull request #3 from alxboyle/add-ifm-support
feat: Add ifm support, bump version
2 parents 925008d + d937c6e commit c156e2a

10 files changed

Lines changed: 1145 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
# Changelog
22

3+
## v1.2.0
4+
5+
### Added
6+
- `dm ifm` command group
7+
for managing in-flight masking ruleset plans
8+
and running mask operations against the IFM service:
9+
- `dm ifm list`
10+
list all IFM ruleset plans.
11+
- `dm ifm get <name>`
12+
show plan metadata,
13+
or the ruleset YAML with `--yaml`.
14+
- `dm ifm create --name <name> --file <yaml>`
15+
create a plan from a YAML ruleset,
16+
with optional `--enabled/--disabled` and `--log-level`.
17+
- `dm ifm update <name>`
18+
update a plan;
19+
pass any of `--file`, `--enabled/--disabled`, `--log-level`
20+
and only those fields are sent.
21+
- `dm ifm delete <name>`
22+
delete a plan
23+
(interactive confirm,
24+
or `--yes` to skip).
25+
- `dm ifm mask <name> --data <file|->`
26+
mask a JSON list of records against a plan,
27+
with `--disable-instance-secret`,
28+
`--run-secret`,
29+
`--log-level`,
30+
`--request-id`,
31+
and `--json/--no-json` (NDJSON) output.
32+
- `dm ifm verify-token`
33+
verify the current IFM token and list its scopes.
34+
35+
Authentication reuses your existing `dm` profile credentials
36+
via the SDK's `DataMasqueIfmClient`,
37+
which transparently exchanges admin-server credentials for an IFM JWT.
38+
339
## v1.1.0
440

541
### Added

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ so teams can use production-shaped data in non-production environments without e
99
DataMasque CLI `dm` covers:
1010

1111
- connections, rulesets, ruleset libraries, and masking runs
12+
- in-flight masking (IFM) ruleset plans and on-demand mask requests
1213
- schema discovery and sensitive-data discovery
1314
- users, files, and DataMasque instance administration
1415

@@ -166,6 +167,26 @@ dm libraries validate <name> # Re-validate against current
166167
dm libraries usage <name> # Show rulesets using it
167168
```
168169

170+
### In-flight masking
171+
172+
The IFM service runs alongside the admin server,
173+
reached at `<DataMasque URL>/ifm` via the standard nginx topology.
174+
175+
```console
176+
dm ifm list # List ruleset plans
177+
dm ifm get <name> # Show plan metadata
178+
dm ifm get <name> --yaml # Print the ruleset YAML
179+
dm ifm create --name myplan --file rules.yaml # Create (server suffixes a random string to the name)
180+
dm ifm create --name myplan --file rules.yaml --disabled --log-level DEBUG
181+
dm ifm update <name> --file rules.yaml # Replace the ruleset YAML
182+
dm ifm update <name> --enabled # Toggle without re-sending the YAML
183+
dm ifm update <name> --log-level INFO
184+
dm ifm delete <name> --yes # Delete a plan
185+
dm ifm mask <name> --data input.json # Mask a JSON list of records
186+
dm ifm mask <name> --data - # Read records from stdin
187+
dm ifm verify-token # Show scopes granted to the current IFM token
188+
```
189+
169190
### Masking runs
170191

171192
```console

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "datamasque-cli"
3-
version = "1.1.0"
3+
version = "1.2.0"
44
description = "Official command-line interface for the DataMasque data-masking platform."
55
authors = [
66
{ name = "DataMasque Ltd" },

src/datamasque_cli/client.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import os
1010

11-
from datamasque.client import DataMasqueClient
12-
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueTransportError
11+
from datamasque.client import DataMasqueClient, DataMasqueIfmClient
12+
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueTransportError, IfmAuthError
1313
from datamasque.client.models.dm_instance import DataMasqueInstanceConfig
14+
from datamasque.client.models.ifm import DataMasqueIfmInstanceConfig
1415

1516
from datamasque_cli.config import Config, Profile, load_config
1617
from datamasque_cli.output import ErrorCode, abort
@@ -59,25 +60,45 @@ def _resolve_profile(config: Config, profile_name: str | None) -> Profile:
5960
return profile
6061

6162

63+
def _resolve_profile_with_verify(profile_name: str | None) -> tuple[Profile, bool]:
64+
"""Resolve the active `Profile` and apply env-var overrides for `verify_ssl`."""
65+
env_profile = profile_from_env() if profile_name is None else None
66+
if env_profile is not None:
67+
profile = env_profile
68+
else:
69+
config = load_config()
70+
profile = _resolve_profile(config, profile_name)
71+
return profile, _verify_ssl_from_env(default=profile.verify_ssl)
72+
73+
74+
def _authenticate_or_abort(
75+
client: DataMasqueClient | DataMasqueIfmClient,
76+
url: str,
77+
*,
78+
verify_ssl: bool,
79+
failure_label: str = "Authentication",
80+
extra_auth_excs: tuple[type[Exception], ...] = (),
81+
) -> None:
82+
try:
83+
client.authenticate()
84+
except DataMasqueTransportError as e:
85+
abort(_format_transport_error(url, e, verify_ssl=verify_ssl), code=ErrorCode.TRANSPORT_ERROR)
86+
except (DataMasqueApiError, *extra_auth_excs) as e:
87+
abort(f"{failure_label} failed: {e}", code=ErrorCode.AUTH_FAILED)
88+
89+
6290
def get_client(profile_name: str | None = None) -> DataMasqueClient:
6391
"""Build and authenticate a `DataMasqueClient`.
6492
6593
Credential resolution order:
6694
1. Environment variables (DATAMASQUE_URL, DATAMASQUE_USERNAME, DATAMASQUE_PASSWORD)
6795
2. Named profile (--profile flag)
6896
3. Active profile from config file
69-
"""
70-
# Env vars take precedence unless a specific profile was requested.
71-
env_profile = profile_from_env() if profile_name is None else None
72-
if env_profile is not None:
73-
profile = env_profile
74-
else:
75-
config = load_config()
76-
profile = _resolve_profile(config, profile_name)
7797
78-
# `DATAMASQUE_VERIFY_SSL` always wins over the stored profile so you can
79-
# flip TLS verification per-call without re-running `dm auth login`.
80-
verify_ssl = _verify_ssl_from_env(default=profile.verify_ssl)
98+
`DATAMASQUE_VERIFY_SSL` always wins over the stored profile so you can
99+
flip TLS verification per-call without re-running `dm auth login`.
100+
"""
101+
profile, verify_ssl = _resolve_profile_with_verify(profile_name)
81102
instance_config = DataMasqueInstanceConfig(
82103
base_url=profile.url,
83104
username=profile.username,
@@ -86,14 +107,7 @@ def get_client(profile_name: str | None = None) -> DataMasqueClient:
86107
)
87108

88109
client = DataMasqueClient(instance_config)
89-
90-
try:
91-
client.authenticate()
92-
except DataMasqueTransportError as e:
93-
abort(_format_transport_error(profile.url, e, verify_ssl=verify_ssl), code=ErrorCode.TRANSPORT_ERROR)
94-
except DataMasqueApiError as e:
95-
abort(f"Authentication failed: {e}", code=ErrorCode.AUTH_FAILED)
96-
110+
_authenticate_or_abort(client, profile.url, verify_ssl=verify_ssl)
97111
return client
98112

99113

@@ -107,3 +121,30 @@ def _format_transport_error(url: str, error: Exception, *, verify_ssl: bool) ->
107121
if verify_ssl and any(term in str(error).lower() for term in _SSL_HINT_TERMS):
108122
message += "\nIf this is a self-signed local build, retry with --insecure or set DATAMASQUE_VERIFY_SSL=false."
109123
return message
124+
125+
126+
def get_ifm_client(profile_name: str | None = None) -> DataMasqueIfmClient:
127+
"""Build and authenticate a `DataMasqueIfmClient`.
128+
129+
Credential resolution order matches `get_client`.
130+
The IFM base URL is derived as `<admin_url>/ifm`,
131+
matching the standard nginx topology that proxies `/ifm/` to the IFM container on the same hostname.
132+
"""
133+
profile, verify_ssl = _resolve_profile_with_verify(profile_name)
134+
instance_config = DataMasqueIfmInstanceConfig(
135+
admin_server_base_url=profile.url,
136+
ifm_base_url=f"{profile.url.rstrip('/')}/ifm",
137+
username=profile.username,
138+
password=profile.password,
139+
verify_ssl=verify_ssl,
140+
)
141+
142+
client = DataMasqueIfmClient(instance_config)
143+
_authenticate_or_abort(
144+
client,
145+
profile.url,
146+
verify_ssl=verify_ssl,
147+
failure_label="IFM authentication",
148+
extra_auth_excs=(IfmAuthError,),
149+
)
150+
return client

0 commit comments

Comments
 (0)