Skip to content

[DSIP-105][API][UI] Add Property.sensitive with API/UI masking - #18585

Open
det101 wants to merge 23 commits into
apache:devfrom
det101:feature-17937-sensitive-property-pr1
Open

[DSIP-105][API][UI] Add Property.sensitive with API/UI masking#18585
det101 wants to merge 23 commits into
apache:devfrom
det101:feature-17937-sensitive-property-pr1

Conversation

@det101

@det101 det101 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #18586 (subtask of DSIP-105 / #17937): add Property.sensitive and mask sensitive values as ****** on API/UI, with keep-original merge on write/start.

In scope (#18586)

  • Property.sensitive (default false)
  • HTTP outbound mask: SensitivePropertyResponseAdvice copy-then-masks Result.data that carries workflow/task/instance Property (query, version lists, and create/update replies). Persistence / in-process stay plaintext.
  • Write-path merge: only ****** means keep original; empty string is a real empty value
  • Reject creating/enabling/disabling sensitive with placeholder-only ******
  • UI: Sensitive checkbox on global params and task localParams
  • Start/command path skips ****** startParams so they do not overwrite definition secrets

Out of scope (follow-up subtasks)

Related

Test plan

  • Unit: PropertySensitiveUtilsTest, SensitivePropertyUtilsTest
  • Local standalone: create workflow with sensitive global + local params
  • API query returns masked ****** (globalParamList / localParams / view-variables)
  • Create/update Result is masked without mutating the persisted object
  • UI save modal shows Sensitive checked and value ****** after reload
  • CI

Verification screenshots

UI — reload workflow: sensitive global param masked as ******

masked-global-params

UI — save modal: Sensitive checkbox available

sensitive-checkbox

API — same workflow masked on query / view-variables

=== Workflow Definition Query (masked) ===
globalParamList: [{"prop": "db_pwd", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]
task localParams: [{"prop": "token", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]

=== view-variables (masked) ===
{
  "globalParams": [
    {
      "prop": "db_pwd",
      "direct": "IN",
      "type": "VARCHAR",
      "value": "******",
      "sensitive": true
    }
  ],
  "localParams": {
    "shell_node_1": {
      "localParamsList": [
        {
          "prop": "token",
          "direct": "IN",
          "type": "VARCHAR",
          "value": "******",
          "sensitive": true
        }
      ],
      "taskType": "SHELL"
    }
  }
}

Screenshot branch (fork only, not part of review diff): det101:pr1-17937-verification-screenshots

@det101

det101 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

UI / API verification screenshots (PR1)

1) Reload existing sensitive workflow — global params masked

Save modal shows:

  • db_pwd value = ******
  • Sensitive checkbox checked

masked-global-params

2) Create / save modal — Sensitive checkbox present

sensitive-checkbox

3) API mask proof (same workflow)

=== Workflow Definition Query (masked) ===
globalParamList: [{"prop": "db_pwd", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]
task localParams: [{"prop": "token", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]

=== view-variables (masked) ===
{
  "globalParams": [
    {
      "prop": "db_pwd",
      "direct": "IN",
      "type": "VARCHAR",
      "value": "******",
      "sensitive": true
    }
  ],
  "localParams": {
    "shell_node_1": {
      "localParamsList": [
        {
          "prop": "token",
          "direct": "IN",
          "type": "VARCHAR",
          "value": "******",
          "sensitive": true
        }
      ],
      "taskType": "SHELL"
    }
  }
}

@det101

det101 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Verification screenshots (hosted on fork evidence branch)

Reload existing sensitive workflow — value masked + Sensitive checked

masked

Save modal — Sensitive checkbox present

checkbox

API mask proof

=== Workflow Definition Query (masked) ===
globalParamList: [{"prop": "db_pwd", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]
task localParams: [{"prop": "token", "direct": "IN", "type": "VARCHAR", "value": "******", "sensitive": true}]

=== view-variables (masked) ===
{
  "globalParams": [
    {
      "prop": "db_pwd",
      "direct": "IN",
      "type": "VARCHAR",
      "value": "******",
      "sensitive": true
    }
  ],
  "localParams": {
    "shell_node_1": {
      "localParamsList": [
        {
          "prop": "token",
          "direct": "IN",
          "type": "VARCHAR",
          "value": "******",
          "sensitive": true
        }
      ],
      "taskType": "SHELL"
    }
  }
}

@det101 det101 changed the title [Feature-17937][API][UI] Add Property.sensitive with API/UI masking (PR1) [DSIP-105][API][UI] Add Property.sensitive with API/UI masking (PR1) Aug 24, 2026
@det101
det101 force-pushed the feature-17937-sensitive-property-pr1 branch from 2793cc3 to b1d4895 Compare August 24, 2026 08:46
…PR1)

Introduce sensitive flag on Property, deep-copy mask on read paths, and
****** keep-original merge on write/start. UI adds Sensitive checkbox.
Encryption and worker log redaction are deferred to follow-up PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
…onflict

Reconcile with apache#18569 removal of obsolete update-with-upstream API while
keeping Property.sensitive masking on task definition read paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@det101 det101 changed the title [DSIP-105][API][UI] Add Property.sensitive with API/UI masking (PR1) [Feature-18586][API][UI] Add Property.sensitive with API/UI masking Aug 25, 2026
det101 and others added 7 commits August 25, 2026 10:00
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep both SensitivePropertyUtils (PR masking) and WorkflowInstanceSummaryVO
(upstream list-query optimization) imports in WorkflowInstanceServiceImpl.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@SbloodyS SbloodyS added feature new feature DSIP labels Aug 28, 2026
@SbloodyS SbloodyS added this to the 3.5.0 milestone Aug 28, 2026
@SbloodyS SbloodyS changed the title [Feature-18586][API][UI] Add Property.sensitive with API/UI masking [DSIP-105][API][UI] Add Property.sensitive with API/UI masking Aug 28, 2026

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two blocking issues:

  1. [P1] Sensitive values are still exposed by the definition-version APIs

The new masking is applied to the current workflow/task definition query paths, but the version-list endpoints still return the persisted entities directly:

  • GET /projects/{projectCode}/task-definition/{code}/versions returns TaskDefinitionLog.taskParams.
  • GET /projects/{projectCode}/workflow-definition/{code}/versions returns WorkflowDefinitionLog.globalParams.

Both SQL projections include these fields, and the service methods put the records into the response without masking them. Therefore, a parameter with sensitive=true can still be read in plaintext by querying its version history, which violates the acceptance criterion that sensitive values must never be returned through external APIs.

Please either mask the returned version entities using the same deep-copy strategy or use dedicated summary DTOs that omit these fields. Regression tests should cover both version endpoints.

  1. [P1] Updating an old workflow instance can restore the secret from the wrong task version

WorkflowInstanceServiceImpl.mergeSensitiveLocalParams() resolves ****** using taskDefinitionDao.queryByCodes(), which loads the current task definitions. However, the workflow-instance detail shown to the user may have been generated from an older workflow/task version.

If the task's sensitive value changed after that instance was created, editing and saving the old instance will silently replace ****** with the latest task definition's value instead of preserving the value belonging to the displayed instance version.

Please resolve each original value using the matching task definition code and version associated with the instance/submitted task definition, rather than querying only the current definition by code. A regression test should cover updating an old instance after its task secret has changed in a newer version.

det101 and others added 2 commits August 28, 2026 14:34
… version

Mask sensitive values on task/workflow definition version list responses, and
restore ****** from the matching TaskDefinitionLog code+version when updating
an old workflow instance.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid N+1 on instance secret merge via queryByTaskDefinitions, and mask
version-list responses on JSON copies so mapper entities stay unmasked.

Co-authored-by: Cursor <cursoragent@cursor.com>
@det101

det101 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@SbloodyS Thanks for the review. Both P1s are addressed, with a few extra hardening changes.

  1. Version-list APIs
    GET .../task-definition/{code}/versions and GET .../workflow-definition/{code}/versions now mask sensitive taskParams / globalParams. Following the deep-copy approach you suggested, masking is applied to JSON copies (copyAndMask*) so MyBatis-mapped log entities are not mutated in place. Regression tests cover both endpoints and assert the original records stay unmasked.

  2. Old workflow instance update
    WorkflowInstanceServiceImpl.mergeSensitiveLocalParams no longer uses taskDefinitionDao.queryByCodes(). It restores ****** from TaskDefinitionLog by code + version. To avoid N+1 queries it uses a single queryByTaskDefinitions call, with a List of keys (TaskDefinition.equals() ignores code/version, so a Set would collapse distinct versions). A regression test updates an old instance after the current task secret has changed and asserts the matching version value is kept.

  3. Extra hardening

    • maskWorkflowDefinition / maskTaskDefinition now clear the cached globalParamMap / taskParamMap, because setGlobalParams does not rebuild the map and getters could still return plaintext.
    • Workflow definition update still merges against the current definition via queryByCodes(), because that path edits the live definition rather than an old instance.

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling sensitive can silently overwrite the real value with ******

There is still a data-loss path when an existing sensitive parameter is changed to non-sensitive:

  1. The API returns the parameter as value = "******", sensitive = true.
  2. The user unchecks Sensitive without editing the value.
  3. The UI submits value = "******", sensitive = false.
  4. findInvalidSensitivePlaceholderProp() skips the parameter because it is no longer sensitive.
  5. mergeSensitiveValuePlaceholder() also skips it for the same reason.
  6. The update therefore persists the literal ****** and permanently overwrites the original value.

This affects both workflow global parameters and task localParams. Besides losing the secret, subsequent task executions will receive ****** instead of the configured credential.

Please reject ****** when an existing sensitive parameter is being changed to non-sensitive, requiring the user to enter an explicit replacement value. This validation must be enforced by the backend rather than only by the UI. Restoring the original value while setting sensitive = false would not be safe either, because the next read would expose that value as non-sensitive.

det101 and others added 3 commits August 31, 2026 14:01
Co-authored-by: Cursor <cursoragent@cursor.com>
- Reject true → false with ****** so the mask cannot overwrite the secret
- Skip startParam ****** overlay, including map-format that loses sensitive
- Leave ****** in the UI on uncheck so save fails instead of wiping with empty

Co-authored-by: Cursor <cursoragent@cursor.com>
@det101

det101 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@SbloodyS Thank you for the careful review — this is a real data-loss path, and your analysis is correct.

Unchecking Sensitive while leaving ****** must not persist the mask. Restoring the original value with sensitive = false would also be unsafe, as you noted, because the next read would return the secret as non-sensitive. We followed that guidance.

The backend now rejects this for both workflow global params and task localParams. findInvalidSensitivePlaceholderProp treats true → false with value ****** as invalid and returns REQUEST_PARAMS_NOT_VALID_ERROR, asking the user to enter an explicit replacement. Toggle sequences are judged only by the final (sensitive, value) pair, so uncheck → check again → save with true + ****** still keeps the original secret.

On the UI we chose not to auto-clear ****** when the checkbox is unchecked. Clearing it would make it too easy to save an empty string and wipe the secret by accident. Leaving the mask in the field means save fails until the user types a real new value, or checks Sensitive again (keep-original). Empty string remains a real empty value; only ****** means keep-original.

Unit tests cover the reject path and the toggle-back keep-original path.

As a related hardening, start/backfill also skips a startParam whose value is ****** instead of overlaying it onto workflow globals. That includes map-format {"pwd":"******"}, which loses sensitive=true. Please let us know if you would prefer a different approach here.

Thanks again for the detailed write-up.

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensitive global parameters are exposed by create/update responses

The read/query endpoints now mask sensitive parameters, but the write endpoints still return the in-memory WorkflowDefinition containing the merged plaintext globalParams:

  • WorkflowDefinitionServiceImpl#createWorkflowDefinition() returns the object created from the submitted plaintext parameters.
  • WorkflowDefinitionServiceImpl#updateWorkflowDefinition() returns the object after mergeGlobalParams() has restored the real value.
  • WorkflowInstanceServiceImpl#updateWorkflowInstance() also returns a WorkflowDefinition containing the merged plaintext value.

The corresponding controllers serialize these objects directly in their Result<WorkflowDefinition> responses. Therefore, creating a sensitive global parameter or updating it with a new value returns the real value through an external API, which violates the acceptance criterion that sensitive parameters must only be returned as ******.

Please return a masked deep copy from these external write paths, for example by applying copyAndMaskWorkflowDefinition() after persistence. Do not mask the object before it has finished being persisted or reuse a masked object for execution.

Please add regression coverage for:

  1. Creating a workflow with a sensitive global parameter.
  2. Updating a workflow with a new sensitive value.
  3. Updating a workflow instance containing a sensitive global parameter.

Each response should contain ******, while the persisted/internal value must remain unchanged and usable for execution.

det101 and others added 2 commits September 1, 2026 15:30
- Copy-then-mask Result.data in ResponseBodyAdvice so create/update replies are not plaintext
- Remove per-query masking from definition/instance services; persistence stays plaintext

Co-authored-by: Cursor <cursoragent@cursor.com>
@det101

det101 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@SbloodyS Thank you — you are right that create/update responses were still serializing the merged plaintext WorkflowDefinition.

We did not only wrap those three service returns. Query-time masking was too easy to miss on write endpoints (and on any new Result that carries Property), so masking now happens at the HTTP outbound boundary:

  • SensitivePropertyResponseAdvice copy-then-masks Result.data when it contains workflow/task/instance Property payloads. That includes createWorkflowDefinition, updateWorkflowDefinition, updateWorkflowInstance, version lists, and the existing query/view-variables paths.
  • Query/write services keep returning plaintext. Persistence and in-process objects are not mutated; only a JSON copy is masked before Jackson writes the body.
  • Unrelated Result payloads (login, cluster, user, …) are left unchanged.

Acceptance is unchanged: API/UI still show ******; the metadata DB is still plaintext in this PR (encryption remains #18587).

det101 and others added 3 commits September 2, 2026 09:46
Keep-original ****** must restore the relation-pinned TaskDefinitionLog, not the main-table latest task.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend DSIP feature new feature test UI ui and front end related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DSIP-105][Feature][API] Add Property.sensitive and mask values in API/UI

2 participants