Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filterAuth as option to deny filtering on fields #561

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/schematools/permissions/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ def has_field_access(self, field: DatasetFieldSchema) -> Permission:
# Otherwise, the field + table rules are checked from the profile.
return self._has_field_auth_access(field) or self._has_field_profile_access(field)

def has_field_filter_access(self, field: DatasetFieldSchema) -> Permission:
"""Tell whether a field may be used in searching.

Some fields do not allow filtering the data for privacy reasons.
An example is filtering all buildings by owner (hence knowing their portfolio),
while the user is still allowed to see the owner of each individual building.

This also checks whether the field has read access (:meth:`has_field_access`).
"""
# There is no profile option yet for profile checking here.
# Tell whether the 'filterAuth' gives extra permission to filter.
# As this logic is an extension of the existing read-access check,
# there is no dataset/table-level variant of the 'filterAuth'.
if self.has_any_scope(field.filter_auth) and (field_auth := self.has_field_access(field)):
# The if-statement checks filterAuth first. When it's defined, it likely denies access.
# That way the other more complex checks are not needed.
return (
Permission(PermissionLevel.highest, source="field.filter_auth")
if field.filter_auth
else field_auth # Return original permission if there is no filterAuth
)
else:
return Permission.none

def _has_dataset_auth_access(self, dataset: DatasetSchema) -> Permission:
"""Tell whether the 'auth' rules give access to the dataset."""
if self.has_any_scope(dataset.auth):
Expand Down
43 changes: 33 additions & 10 deletions src/schematools/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,24 @@ def __deepcopy__(self, memo):
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.data!r})"

def __init__(self, *args, **kwargs):
self.__initialized = False
super().__init__(*args, **kwargs)
self.__initialized = True

def __setitem__(self, key, value):
"""Check for changes to the dictionary data. Note this is also called on __init__()."""
if key == "auth" and key in self.data:
logger.warning("auth field is patched by tests")
if self.__initialized:
logger.info("patching '%s' on %r id %d", key, self, id(self))
property_name = to_snake_case(key) # filterAuth / filter_auth
if (
property_name in self.__dict__
and (prop := getattr(self.__class__, property_name, None)) is not None
and isinstance(prop, cached_property)
):
# Clear the @cached_property cache value
del self.__dict__[property_name]

if (
key in self.__dict__
and (prop := getattr(self.__class__, key, None)) is not None
and isinstance(prop, cached_property)
):
# Clear the @cached_property cache value
del self.__dict__[key]
super().__setitem__(key, value)

if IS_DEBUGGER:
Expand Down Expand Up @@ -1082,7 +1088,7 @@ def additional_relations(self) -> list[AdditionalRelationSchema]:

def get_reverse_relation(self, field: DatasetFieldSchema) -> AdditionalRelationSchema | None:
"""Find the description of a reverse relation for a field."""
if not field.relation and not field.nm_relation:
if not field.is_relation:
raise ValueError("Field is not a relation")

for relation in self.additional_relations:
Expand Down Expand Up @@ -1473,6 +1479,13 @@ def is_internal(self) -> bool:
"""Id fields for table with composite key is only for internal (Django) use."""
return self.is_primary and self._parent_table.has_composite_key

@cached_property
def is_relation(self) -> bool:
"""Tell whether the field is a relation.
This is a convenience for ``bool(field.relation or field.nm_relation)``.
"""
return bool(self.get("relation"))

@cached_property
def relation(self) -> str | None:
"""Give the 1:N relation, if it exists."""
Expand Down Expand Up @@ -1773,6 +1786,16 @@ def auth(self) -> frozenset[str]:
return self.parent_field.auth
return _normalize_scopes(self.get("auth"))

@cached_property
def filter_auth(self) -> frozenset[str]:
"""Auth of the field, or OPENBAAR.
This setting allows denying access to query/search on fields,
e.g. to block searching all buildings from a certain owner.
"""
if self.is_subfield:
return self.parent_field.filter_auth
return _normalize_scopes(self.get("filterAuth"))

@cached_property
def is_composite_key(self):
"""Tell whether the relation uses a composite key"""
Expand Down
Loading