This helper deliberately hard-codes {@code aboutFqnHash}; the {@code prefix} arg
+ * only namespaces the bound parameter keys. Don't reuse it for other columns — copy and
+ * adjust instead so the column choice stays explicit at the callsite.
+ */
+ private String buildFqnPrefixOrCondition(String prefix, String commaSeparatedFqns) {
+ List Convention for custom workflows: any transition that is intended to be terminal MUST
+ * declare an explicit {@code resolutionType} in the workflow JSON, matching the seeded
+ * GlossaryApproval / DescriptionUpdate / etc. definitions. Returning {@code null} here is
+ * the explicit signal that the transition is non-terminal — callers route through the
+ * workflow advancement path instead of {@code applyTaskResolution}, so the task stays
+ * alive on the next user-task node.
+ */
private TaskResolutionType resolveResolutionType(
Task task,
TaskResolutionType requestedResolutionType,
@@ -1183,13 +1234,12 @@ private TaskResolutionType resolveResolutionType(
if (selectedTransition != null && selectedTransition.getTargetTaskStatus() != null) {
return switch (selectedTransition.getTargetTaskStatus()) {
- case Approved -> TaskResolutionType.Approved;
case Rejected -> TaskResolutionType.Rejected;
case Completed -> TaskResolutionType.Completed;
case Cancelled -> TaskResolutionType.Cancelled;
case Revoked -> TaskResolutionType.Revoked;
case Failed -> TaskResolutionType.TimedOut;
- case Open, InProgress, Pending -> null;
+ case Open, InProgress, Pending, Approved, Granted -> null;
};
}
diff --git a/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json b/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json
index 185039044964..76588285e174 100644
--- a/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json
+++ b/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json
@@ -2,7 +2,7 @@
"name": "DataAccessRequestTaskWorkflow",
"fullyQualifiedName": "DataAccessRequestTaskWorkflow",
"displayName": "Data Access Request Task Workflow",
- "description": "Default workflow-driven lifecycle for data access request tasks. After approval, access can be revoked by the approver, owner, or domain owner.",
+ "description": "Default workflow-driven lifecycle for data access request tasks. After review the request moves to Approved (banner shown until access is provisioned), then to Granted once an admin marks access as provisioned. Access can later be revoked by the approver, owner, or domain owner.",
"config": {
"storeStageStatus": true
},
@@ -40,7 +40,7 @@
"id": "approve",
"label": "Approve",
"targetStageId": "approved",
- "targetTaskStatus": "InProgress",
+ "targetTaskStatus": "Approved",
"formRef": "approve",
"requiresComment": false
},
@@ -63,7 +63,7 @@
"type": "userTask",
"subType": "userApprovalTask",
"name": "ApprovedAccess",
- "displayName": "Active Access",
+ "displayName": "Approved - Awaiting Grant",
"config": {
"assignees": {
"addReviewers": true,
@@ -74,7 +74,48 @@
"rejectionThreshold": 1,
"stageId": "approved",
"stageDisplayName": "Approved",
- "taskStatus": "InProgress",
+ "taskStatus": "Approved",
+ "assigneeStrategy": "owners-and-reviewers",
+ "transitionMetadata": [
+ {
+ "id": "markAsGranted",
+ "label": "Mark as Granted",
+ "targetStageId": "granted",
+ "targetTaskStatus": "Granted",
+ "formRef": "grant",
+ "requiresComment": false
+ },
+ {
+ "id": "revoke",
+ "label": "Revoke Access",
+ "targetStageId": "revoked",
+ "targetTaskStatus": "Revoked",
+ "resolutionType": "Revoked",
+ "formRef": "revoke",
+ "requiresComment": true
+ }
+ ]
+ },
+ "inputNamespaceMap": {
+ "relatedEntity": "global"
+ }
+ },
+ {
+ "type": "userTask",
+ "subType": "userApprovalTask",
+ "name": "GrantedAccess",
+ "displayName": "Active Access",
+ "config": {
+ "assignees": {
+ "addReviewers": true,
+ "addOwners": true,
+ "candidates": []
+ },
+ "approvalThreshold": 1,
+ "rejectionThreshold": 1,
+ "stageId": "granted",
+ "stageDisplayName": "Granted",
+ "taskStatus": "Granted",
"assigneeStrategy": "owners-and-reviewers",
"transitionMetadata": [
{
@@ -122,6 +163,16 @@
},
{
"from": "ApprovedAccess",
+ "to": "GrantedAccess",
+ "condition": "markAsGranted"
+ },
+ {
+ "from": "ApprovedAccess",
+ "to": "RevokedEnd",
+ "condition": "revoke"
+ },
+ {
+ "from": "GrantedAccess",
"to": "RevokedEnd",
"condition": "revoke"
}
diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json
index a02039cbb76e..c1a8e3c65999 100644
--- a/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json
+++ b/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json
@@ -13,6 +13,13 @@
"resources" : ["all"],
"operations": ["ViewAll", "EditDescription", "EditTags", "EditGlossaryTerms", "EditTier", "EditCertification"],
"effect": "allow"
+ },
+ {
+ "name": "DataConsumerPolicy-CreateTask-Rule",
+ "description" : "Allow authenticated users to create tasks (data access requests, suggestions, etc.).",
+ "resources" : ["task"],
+ "operations": ["Create"],
+ "effect": "allow"
}
]
-}
\ No newline at end of file
+}
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java
index 9005cfc5c6e6..ba3e38a38684 100644
--- a/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java
@@ -172,11 +172,11 @@ void testFindExistingTaskWithRetryReturnsNullAfterExhaustingWorkflowManagedLooku
@Test
void testIsTerminalTaskStatusReturnsTrueForResolvedStates() {
- assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Approved));
assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Rejected));
assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Completed));
assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Cancelled));
assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Failed));
+ assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Revoked));
}
@Test
@@ -184,6 +184,12 @@ void testIsTerminalTaskStatusReturnsFalseForOpenStates() {
assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Open));
assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.InProgress));
assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Pending));
+ // Approved and Granted are non-terminal so the next-stage CreateTask listener
+ // (e.g. Data Access Request's ApprovedAccess → GrantedAccess advancement) can
+ // update status/workflowStageId/availableTransitions instead of preserving
+ // stale state. See the DataAccessRequestTaskWorkflow.json edges.
+ assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Approved));
+ assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Granted));
assertFalse(CreateTask.isTerminalTaskStatus(null));
}
}
diff --git a/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json b/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json
index 1a9a6f992bb9..c859cbeb60c2 100644
--- a/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json
+++ b/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json
@@ -16,6 +16,16 @@
"type": "integer",
"minimum": 0
},
+ "approved": {
+ "description": "Total count of all tasks currently in the Approved status (across all task types). For Data Access Requests this is the 'awaiting grant' bucket; for workflows where Approved is terminal (e.g. Glossary, DescriptionUpdate) it reflects resolved tasks.",
+ "type": "integer",
+ "minimum": 0
+ },
+ "granted": {
+ "description": "Total count of all tasks currently in the Granted status. Today this status is only emitted by the Data Access Request workflow to indicate access has been provisioned and is active.",
+ "type": "integer",
+ "minimum": 0
+ },
"completed": {
"description": "Total count of all completed/closed tasks.",
"type": "integer",
diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json b/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json
index 667eb1a97ef9..9968bea41b7b 100644
--- a/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json
+++ b/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json
@@ -74,6 +74,7 @@
"InProgress",
"Pending",
"Approved",
+ "Granted",
"Rejected",
"Completed",
"Cancelled",
@@ -85,6 +86,7 @@
{"name": "InProgress"},
{"name": "Pending"},
{"name": "Approved"},
+ {"name": "Granted"},
{"name": "Rejected"},
{"name": "Completed"},
{"name": "Cancelled"},
@@ -319,6 +321,18 @@
"description": "UUID of the user who created this task. Stored in JSON for efficient querying via generated column index.",
"type": "string"
},
+ "approvedBy": {
+ "description": "User who approved this task (set when an approval transition fires; distinct from resolution.resolvedBy which is set only on terminal transitions).",
+ "$ref": "../../type/entityReference.json"
+ },
+ "approvedById": {
+ "description": "UUID of the user who approved this task. Stored in JSON for efficient querying via generated column index.",
+ "type": "string"
+ },
+ "approvedAt": {
+ "description": "Timestamp when the task was approved.",
+ "$ref": "../../type/basic.json#/definitions/timestamp"
+ },
"assignees": {
"description": "Users or teams assigned to complete this task.",
"$ref": "../../type/entityReferenceList.json"
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
index df4f6805ff4e..2802479bdee1 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
@@ -11,7 +11,16 @@
* limitations under the License.
*/
import Icon from '@ant-design/icons';
-import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd';
+import {
+ Alert,
+ Button,
+ Col,
+ Divider,
+ Row,
+ Space,
+ Tooltip,
+ Typography,
+} from 'antd';
import ButtonGroup from 'antd/lib/button/button-group';
import { AxiosError } from 'axios';
import classNames from 'classnames';
@@ -162,7 +171,11 @@ export const DataAssetsHeader = ({
const { entityRules } = useEntityRules(entityType);
const [dataContract, setDataContract] = useState
+ *
+ *
+ *