#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>
This commit is contained in:
Ram Narayan Balaji 2025-06-10 19:36:20 +05:30 committed by GitHub
parent b8cb82c25c
commit febd195bfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 204 additions and 8 deletions

View File

@ -79,7 +79,7 @@ public class ClassificationResource
extends EntityResource<Classification, ClassificationRepository> {
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<Classification> {
/* Required for serde */

View File

@ -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;
}

View File

@ -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"],

View File

@ -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"],

View File

@ -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;
}
/**

View File

@ -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;
}
/**

View File

@ -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 {
/**