JingsongLi commented on code in PR #10503:
URL: https://github.com/apache/paimon/pull/10503#discussion_r4253599697


##########
paimon-python/pypaimon/tests/rest/rest_server.py:
##########
@@ -587,6 +716,189 @@ def _handle_table_resource(self, method: str, path_parts: 
List[str],
             return self._mock_response(ErrorResponse(None, None, "Not Found", 
404), 404)
         return self._mock_response(ErrorResponse(None, None, "Not Found", 
404), 404)
 
+    def set_query_principals(self, *principals: str) -> None:
+        self.query_principals = set(principals)
+
+    def _permissions_handle(self, method: str, resource_path: str, data: str,
+                            parameters: Dict[str, str]) -> Tuple[str, int]:
+        if method == "GET" and resource_path == 
self.resource_paths.permissions():
+            target = PermissionResource(
+                parameters.get("resourceType"), parameters.get("database"),
+                parameters.get("table"), parameters.get("function"), 
parameters.get("view"))
+            missing = self._missing_permission_resource(target)
+            if missing is not None:
+                return missing
+            matched = [assignment for assignment in 
self.permission_store.values()
+                       if assignment.get_resource() == target
+                       and _matches(parameters, "principal", 
assignment.get_principal())
+                       and _matches(parameters, "access", 
assignment.get_access())]
+            return self._mock_response(
+                ListPermissionsResponse(sorted(matched, key=_assignment_key)), 
200)
+        if method == "POST" and resource_path == 
self.resource_paths.grant_permission():
+            assignment = JSON.from_json(data, 
GrantPermissionRequest).assignment()
+            missing = 
self._missing_permission_resource(assignment.get_resource())
+            if missing is not None:
+                return missing
+            invalid = self._invalid_column_grant(assignment)
+            if invalid is not None:
+                return invalid
+            self.permission_store[_assignment_key(assignment)] = assignment
+            return self._mock_response("", 200)
+        if method == "POST" and resource_path == 
self.resource_paths.revoke_permission():
+            request = JSON.from_json(data, RevokePermissionRequest)
+            missing = self._missing_permission_resource(request.get_resource())
+            if missing is not None:
+                return missing
+            self.permission_store.pop(_permission_key(
+                request.get_resource(), request.get_access(), 
request.get_principal()), None)
+            return self._mock_response("", 200)
+        return self._mock_response(ErrorResponse(None, None, "Not Found", 
404), 404)
+
+    def _missing_permission_resource(
+            self, resource: PermissionResource) -> Optional[Tuple[str, int]]:
+        resource_type = resource.get_type()
+        database = resource.get_database()
+        if resource_type in (ResourceType.CATALOG, ResourceType.CATALOG_ALL):
+            return None
+        exists = database in self.database_store
+        if resource_type in (ResourceType.TABLE, ResourceType.COLUMN):
+            exists = exists and Identifier.create(
+                database, resource.get_table()).get_full_name() in 
self.table_metadata_store
+        elif resource_type == ResourceType.FUNCTION:
+            exists = exists and "{}.{}".format(
+                database, resource.get_function()) in self.function_store
+        elif resource_type == ResourceType.VIEW:
+            exists = False
+        if exists:
+            return None
+        return self._mock_response(ErrorResponse(
+            resource_type.name, _resource_name(resource),
+            "Permission resource does not exist.", 404), 404)
+
+    def _invalid_column_grant(self, assignment: PermissionAssignment) -> 
Optional[Tuple[str, int]]:
+        resource = assignment.get_resource()
+        if resource.get_type() != ResourceType.COLUMN:
+            return None
+        schema = self.table_metadata_store[
+            Identifier.create(resource.get_database(), 
resource.get_table()).get_full_name()].schema
+        if not _query_auth_enabled(schema):
+            return self._mock_response(ErrorResponse(
+                ErrorResponse.RESOURCE_TYPE_TABLE, _resource_name(resource),
+                "Column permissions require query-auth.enabled=true.", 409), 
409)
+        names = {field.name for field in schema.fields}
+        for column in _granted_columns(assignment):
+            if column not in names:
+                return self._mock_response(ErrorResponse(
+                    ErrorResponse.RESOURCE_TYPE_COLUMN, column,
+                    "Permission column does not exist.", 404), 404)
+        return None
+
+    def _column_grants(self, identifier: Identifier) -> 
List[PermissionAssignment]:
+        return [assignment for assignment in self.permission_store.values()
+                if assignment.get_resource().get_type() == ResourceType.COLUMN
+                and _on_table(assignment.get_resource(), identifier)]
+
+    def _policies_handle(self, method: str, data: str, identifier: Identifier,
+                         parameters: Dict[str, str], drop: bool) -> Tuple[str, 
int]:
+        resource = PermissionResource(
+            ResourceType.TABLE, identifier.get_database_name(), 
identifier.get_table_name())
+        metadata = self.table_metadata_store.get(identifier.get_full_name())
+        if metadata is None:
+            return self._missing_permission_resource(resource)
+        policies = self._table_policies(identifier, metadata)
+        if method == "GET" and not drop:
+            matched = [DataPolicy(resource, policy.get_row_filter(), 
policy.get_column_mask(),
+                                  policy.get_principal())
+                       for policy in policies.values() if 
_matches_policy(policy, parameters)]
+            return self._mock_response(ListPoliciesResponse(sorted(matched, 
key=_policy_key)), 200)
+        if method == "POST" and not drop:
+            policy = JSON.from_json(data, PolicyRequest).policy(resource)
+            if not _query_auth_enabled(metadata.schema):
+                return self._mock_response(ErrorResponse(
+                    ErrorResponse.RESOURCE_TYPE_TABLE, 
identifier.get_full_name(),
+                    "Data policies require the target table option 
query-auth.enabled=true.",
+                    409), 409)
+            identity = _policy_identity(policy)
+            missing = _policy_columns(policy) - {field.name for field in 
metadata.schema.fields}

Review Comment:
   [P2] Validate mask output types as well as referenced column names
   
   This validation accepts a mask for STRING `email` with 
`{'name':'YEAR','fieldRef':{'name':'joined','type':'DATE'}}`, because both 
column names exist. I reproduced through 
`catalog.policy_management().create_policy`: it succeeds, while an assertion 
expecting `BadRequestException` fails. YEAR produces INT; the documented 
management contract requires a mask's output type to match its protected 
column, and Java `RESTCatalogServerUtils.parseColumnMask` rejects this 
mismatch. Direct Java probes confirmed the rejection.
   
   The new successful fixtures also depend on this looser mock: `NULL_MASK` is 
used on STRING `email`, whereas Java's `NullTransform.outputType()` is BOOLEAN 
and its reference server rejects that policy too. Thus the cross-client pass 
can exercise policies the reference service cannot create. Validate 
schema-bound mask transforms before storing them and when validating schema 
changes; add a wrong-output-type negative case. Use a valid STRING mask for 
`email`, and a nullable BOOLEAN column for NULL-mask coverage (or separately 
establish and synchronize a broader NULL policy contract). This finding 
concerns the new mock/test validity, not an unchanged Java implementation 
regression.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to