From febd195bfd79c47c27b5dab760b49ecdcfa3e459 Mon Sep 17 00:00:00 2001 From: Ram Narayan Balaji <81347100+yan-3005@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:36:20 +0530 Subject: [PATCH] #16279 Update Classification Schema to include Governance Fields - Schema and Java Implementation (#21636) * Update Classification Schema to include Governance Fields * Removed Tags, Reviewers, Domain from Classification as they are needed and corrected tests * Added Permission check for owners in Classification Resource Test * Added LoadTags.ts generated from createClassificationSchema.json * Only have my schema changes in the typescript files, ignore other changes. --------- Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> --- .../tags/ClassificationResource.java | 2 +- .../tags/ClassificationResourceTest.java | 64 +++++++++++++++++-- .../classification/createClassification.json | 5 ++ .../entity/classification/classification.json | 4 ++ .../classification/createClassification.ts | 62 +++++++++++++++++- .../generated/api/classification/loadTags.ts | 62 +++++++++++++++++- .../entity/classification/classification.ts | 13 +++- 7 files changed, 204 insertions(+), 8 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java index 0bccd42a6e8..82235a000fa 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java @@ -79,7 +79,7 @@ public class ClassificationResource extends EntityResource { private final ClassificationMapper mapper = new ClassificationMapper(); public static final String TAG_COLLECTION_PATH = "/v1/classifications/"; - static final String FIELDS = "usageCount,termCount"; + static final String FIELDS = "owners,usageCount,termCount"; static class ClassificationList extends ResultList { /* Required for serde */ diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/tags/ClassificationResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/tags/ClassificationResourceTest.java index 41462b3e2a9..45fb819cb4f 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/tags/ClassificationResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/tags/ClassificationResourceTest.java @@ -14,9 +14,14 @@ package org.openmetadata.service.resources.tags; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; +import static org.openmetadata.service.Entity.FIELD_OWNERS; +import static org.openmetadata.service.exception.CatalogExceptionMessage.permissionNotAllowed; +import static org.openmetadata.service.security.SecurityUtil.authHeaders; +import static org.openmetadata.service.util.EntityUtil.fieldAdded; import static org.openmetadata.service.util.EntityUtil.fieldUpdated; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE; @@ -40,6 +45,7 @@ import org.openmetadata.schema.api.classification.CreateClassification; import org.openmetadata.schema.entity.classification.Classification; import org.openmetadata.schema.entity.classification.Tag; import org.openmetadata.schema.type.ChangeDescription; +import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.ProviderType; import org.openmetadata.service.Entity; import org.openmetadata.service.exception.CatalogExceptionMessage; @@ -91,6 +97,54 @@ public class ClassificationResourceTest classification.getName(), Entity.CLASSIFICATION)); } + @Test + void test_classificationOwnerPermissions(TestInfo test) throws IOException { + // Create classification without owners + CreateClassification create = createRequest(getEntityName(test)); + Classification classification = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + assertTrue( + listOrEmpty(classification.getOwners()).isEmpty(), + "Classification should have no owners initially"); + + // Update classification owners as admin using PATCH + String json = JsonUtils.pojoToJson(classification); + classification.setOwners(List.of(USER1.getEntityReference())); + ChangeDescription change = getChangeDescription(classification, MINOR_UPDATE); + fieldAdded(change, FIELD_OWNERS, List.of(USER1.getEntityReference())); + classification = + patchEntityAndCheck(classification, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change); + assertEquals( + 1, listOrEmpty(classification.getOwners()).size(), "Classification should have one owner"); + assertEquals( + USER1.getId(), classification.getOwners().get(0).getId(), "Owner should match USER1"); + + // Update owners as USER2 with USER2 credentials (should fail with 403) + String originalJson = JsonUtils.pojoToJson(classification); + classification.setOwners(List.of(USER2.getEntityReference())); + Classification finalClassification = classification; + assertResponse( + () -> + patchEntity( + finalClassification.getId(), + originalJson, + finalClassification, + authHeaders(USER2.getName())), + FORBIDDEN, + permissionNotAllowed(USER2.getName(), List.of(MetadataOperation.EDIT_OWNERS))); + + // Verify the above change did not change the owners, USER1 should still be the owner + Classification retrievedClassification = + getEntity(classification.getId(), "owners", ADMIN_AUTH_HEADERS); + assertEquals( + 1, + listOrEmpty(retrievedClassification.getOwners()).size(), + "Classification should still have one owner"); + assertEquals( + USER1.getId(), + retrievedClassification.getOwners().get(0).getId(), + "Owner should still be USER1"); + } + @Override public CreateClassification createRequest(String name) { return new CreateClassification() @@ -120,18 +174,20 @@ public class ClassificationResourceTest @Override public Classification validateGetWithDifferentFields( Classification classification, boolean byName) throws HttpResponseException { + String fields = ""; classification = byName - ? getEntityByName(classification.getFullyQualifiedName(), null, ADMIN_AUTH_HEADERS) - : getEntity(classification.getId(), null, ADMIN_AUTH_HEADERS); - assertListNull(classification.getUsageCount()); + ? getEntityByName(classification.getFullyQualifiedName(), fields, ADMIN_AUTH_HEADERS) + : getEntity(classification.getId(), fields, ADMIN_AUTH_HEADERS); + assertListNull(classification.getOwners()); - String fields = "usageCount"; + fields = "owners,usageCount"; classification = byName ? getEntityByName(classification.getFullyQualifiedName(), fields, ADMIN_AUTH_HEADERS) : getEntity(classification.getId(), fields, ADMIN_AUTH_HEADERS); assertListNotNull(classification.getUsageCount()); + assertListNotNull(classification.getOwners()); return classification; } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/classification/createClassification.json b/openmetadata-spec/src/main/resources/json/schema/api/classification/createClassification.json index c8c6e905b7b..495d51e1d90 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/classification/createClassification.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/classification/createClassification.json @@ -30,6 +30,11 @@ "domain" : { "description": "Fully qualified name of the domain the Table belongs to.", "type": "string" + }, + "owners": { + "description": "Owners of this classification term.", + "$ref": "../../type/entityReferenceList.json", + "default": null } }, "required": ["name", "description"], diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/classification/classification.json b/openmetadata-spec/src/main/resources/json/schema/entity/classification/classification.json index 3ccca782aa2..51ae54efb61 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/classification/classification.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/classification/classification.json @@ -80,6 +80,10 @@ "domain" : { "description": "Domain the asset belongs to. When not set, the asset inherits the domain from the parent it belongs to.", "$ref": "../../type/entityReference.json" + }, + "owners": { + "description": "Owners of this Classification.", + "$ref": "../../type/entityReferenceList.json" } }, "required": ["name", "description"], diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/createClassification.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/createClassification.ts index f4659402d49..8dd1e6823e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/createClassification.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/createClassification.ts @@ -36,7 +36,67 @@ export interface CreateClassification { */ mutuallyExclusive?: boolean; name: string; - provider?: ProviderType; + /** + * Owners of this classification term. + */ + owners?: EntityReference[]; + provider?: ProviderType; +} + +/** + * Owners of this classification term. + * + * This schema defines the EntityReferenceList type used for referencing an entity. + * EntityReference is used for capturing relationships from one entity to another. For + * example, a table has an attribute called database of type EntityReference that captures + * the relationship of a table `belongs to a` database. + * + * This schema defines the EntityReference type used for referencing an entity. + * EntityReference is used for capturing relationships from one entity to another. For + * example, a table has an attribute called database of type EntityReference that captures + * the relationship of a table `belongs to a` database. + */ +export interface EntityReference { + /** + * If true the entity referred to has been soft-deleted. + */ + deleted?: boolean; + /** + * Optional description of entity. + */ + description?: string; + /** + * Display Name that identifies this entity. + */ + displayName?: string; + /** + * Fully qualified name of the entity instance. For entities such as tables, databases + * fullyQualifiedName is returned in this field. For entities that don't have name hierarchy + * such as `user` and `team` this will be same as the `name` field. + */ + fullyQualifiedName?: string; + /** + * Link to the entity resource. + */ + href?: string; + /** + * Unique identifier that identifies an entity instance. + */ + id: string; + /** + * If true the relationship indicated by this entity reference is inherited from the parent + * entity. + */ + inherited?: boolean; + /** + * Name of the entity instance. + */ + name?: string; + /** + * Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`, + * `dashboardService`... + */ + type: string; } /** diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/loadTags.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/loadTags.ts index dc4852ef3eb..7ec9bc3f3c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/loadTags.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/classification/loadTags.ts @@ -44,7 +44,67 @@ export interface CreateClassificationRequest { */ mutuallyExclusive?: boolean; name: string; - provider?: ProviderType; + /** + * Owners of this classification term. + */ + owners?: EntityReference[]; + provider?: ProviderType; +} + +/** + * Owners of this classification term. + * + * This schema defines the EntityReferenceList type used for referencing an entity. + * EntityReference is used for capturing relationships from one entity to another. For + * example, a table has an attribute called database of type EntityReference that captures + * the relationship of a table `belongs to a` database. + * + * This schema defines the EntityReference type used for referencing an entity. + * EntityReference is used for capturing relationships from one entity to another. For + * example, a table has an attribute called database of type EntityReference that captures + * the relationship of a table `belongs to a` database. + */ +export interface EntityReference { + /** + * If true the entity referred to has been soft-deleted. + */ + deleted?: boolean; + /** + * Optional description of entity. + */ + description?: string; + /** + * Display Name that identifies this entity. + */ + displayName?: string; + /** + * Fully qualified name of the entity instance. For entities such as tables, databases + * fullyQualifiedName is returned in this field. For entities that don't have name hierarchy + * such as `user` and `team` this will be same as the `name` field. + */ + fullyQualifiedName?: string; + /** + * Link to the entity resource. + */ + href?: string; + /** + * Unique identifier that identifies an entity instance. + */ + id: string; + /** + * If true the relationship indicated by this entity reference is inherited from the parent + * entity. + */ + inherited?: boolean; + /** + * Name of the entity instance. + */ + name?: string; + /** + * Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`, + * `dashboardService`... + */ + type: string; } /** diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/classification/classification.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/classification/classification.ts index 9c75e28611e..01a528dfbf7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/classification/classification.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/classification/classification.ts @@ -66,7 +66,11 @@ export interface Classification { */ mutuallyExclusive?: boolean; name: string; - provider?: ProviderType; + /** + * Owners of this Classification. + */ + owners?: EntityReference[]; + provider?: ProviderType; /** * Total number of children tag terms under this classification. This includes all the * children in the hierarchy. @@ -164,6 +168,13 @@ export interface FieldChange { * EntityReference is used for capturing relationships from one entity to another. For * example, a table has an attribute called database of type EntityReference that captures * the relationship of a table `belongs to a` database. + * + * Owners of this Classification. + * + * This schema defines the EntityReferenceList type used for referencing an entity. + * EntityReference is used for capturing relationships from one entity to another. For + * example, a table has an attribute called database of type EntityReference that captures + * the relationship of a table `belongs to a` database. */ export interface EntityReference { /**