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

fix: Validating permission to update an existing request on both the new and the old instance #4449

Merged
merged 3 commits into from
Aug 27, 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
2 changes: 1 addition & 1 deletion sdk/python/feast/permissions/enforcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def enforce_policy(
Define the logic to apply the configured permissions when a given action is requested on
a protected resource.

If no permissions are defined, the result is to allow the execution.
If no permissions are defined, the result is to deny the execution.

Args:
permissions: The configured set of `Permission`.
Expand Down
48 changes: 44 additions & 4 deletions sdk/python/feast/permissions/security_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from contextvars import ContextVar
from typing import List, Optional, Union
from typing import Callable, List, Optional, Union

from feast.errors import FeastObjectNotFoundException
from feast.feast_object import FeastObject
from feast.infra.registry.base_registry import BaseRegistry
from feast.permissions.action import AuthzedAction
Expand Down Expand Up @@ -59,9 +60,9 @@ def assert_permissions(
filter_only: bool = False,
) -> list[FeastObject]:
"""
Verify if the current user is authorized ro execute the requested actions on the given resources.
Verify if the current user is authorized to execute the requested actions on the given resources.

If no permissions are defined, the result is to allow the execution.
If no permissions are defined, the result is to deny the execution.

Args:
resources: The resources for which we need to enforce authorized permission.
Expand All @@ -73,7 +74,7 @@ def assert_permissions(
list[FeastObject]: A filtered list of the permitted resources, possibly empty.

Raises:
PermissionError: If the current user is not authorized to eecute all the requested actions on the given resources.
PermissionError: If the current user is not authorized to execute all the requested actions on the given resources.
"""
return enforce_policy(
permissions=self.permissions,
Expand All @@ -84,6 +85,45 @@ def assert_permissions(
)


def assert_permissions_to_update(
resource: FeastObject,
getter: Callable[[str, str, bool], FeastObject],
project: str,
allow_cache: bool = True,
) -> FeastObject:
"""
Verify if the current user is authorized to create or update the given resource.
If the resource already exists, the user must be granted permission to execute DESCRIBE and UPDATE actions.
If the resource does not exist, the user must be granted permission to execute the CREATE action.

If no permissions are defined, the result is to deny the execution.

Args:
resource: The resources for which we need to enforce authorized permission.
getter: The getter function used to retrieve the existing resource instance by name.
The signature must be `get_permission(self, name: str, project: str, allow_cache: bool)`
project: The project nane used in the getter function.
allow_cache: Whether to use cached data. Defaults to `True`.
Returns:
FeastObject: The original `resource`, if permitted.

Raises:
PermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one.
"""
actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE]
try:
existing_resource = getter(
name=resource.name,
project=project,
allow_cache=allow_cache,
) # type: ignore[call-arg]
assert_permissions(resource=existing_resource, actions=actions)
except FeastObjectNotFoundException:
actions = [AuthzedAction.CREATE]
resource_to_update = assert_permissions(resource=resource, actions=actions)
tokoko marked this conversation as resolved.
Show resolved Hide resolved
return resource_to_update


def assert_permissions(
resource: FeastObject,
actions: Union[AuthzedAction, List[AuthzedAction]],
Expand Down
123 changes: 70 additions & 53 deletions sdk/python/feast/registry_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
from feast.infra.infra_object import Infra
from feast.infra.registry.base_registry import BaseRegistry
from feast.on_demand_feature_view import OnDemandFeatureView
from feast.permissions.action import CRUD, AuthzedAction
from feast.permissions.action import AuthzedAction
from feast.permissions.permission import Permission
from feast.permissions.security_manager import assert_permissions, permitted_resources
from feast.permissions.security_manager import (
assert_permissions,
assert_permissions_to_update,
permitted_resources,
)
from feast.permissions.server.grpc import grpc_interceptors
from feast.permissions.server.utils import (
ServerType,
Expand All @@ -37,14 +41,16 @@ def __init__(self, registry: BaseRegistry) -> None:
self.proxied_registry = registry

def ApplyEntity(self, request: RegistryServer_pb2.ApplyEntityRequest, context):
self.proxied_registry.apply_entity(
entity=cast(
Entity,
assert_permissions(
resource=Entity.from_proto(request.entity),
actions=CRUD,
),
entity = cast(
Entity,
assert_permissions_to_update(
resource=Entity.from_proto(request.entity),
getter=self.proxied_registry.get_entity,
project=request.project,
),
)
self.proxied_registry.apply_entity(
entity=entity,
project=request.project,
commit=request.commit,
)
Expand Down Expand Up @@ -95,19 +101,19 @@ def DeleteEntity(self, request: RegistryServer_pb2.DeleteEntityRequest, context)
def ApplyDataSource(
self, request: RegistryServer_pb2.ApplyDataSourceRequest, context
):
(
self.proxied_registry.apply_data_source(
data_source=cast(
DataSource,
assert_permissions(
resource=DataSource.from_proto(request.data_source),
actions=CRUD,
),
),
data_source = cast(
DataSource,
assert_permissions_to_update(
resource=DataSource.from_proto(request.data_source),
getter=self.proxied_registry.get_data_source,
project=request.project,
commit=request.commit,
),
)
self.proxied_registry.apply_data_source(
data_source=data_source,
project=request.project,
commit=request.commit,
)

return Empty()

Expand Down Expand Up @@ -182,12 +188,16 @@ def ApplyFeatureView(
elif feature_view_type == "stream_feature_view":
feature_view = StreamFeatureView.from_proto(request.stream_feature_view)

assert_permissions_to_update(
resource=feature_view,
# Will replace with the new get_any_feature_view method later
getter=self.proxied_registry.get_feature_view,
project=request.project,
)

(
self.proxied_registry.apply_feature_view(
feature_view=cast(
FeatureView,
assert_permissions(resource=feature_view, actions=CRUD),
),
feature_view=feature_view,
project=request.project,
commit=request.commit,
),
Expand Down Expand Up @@ -305,14 +315,16 @@ def ListOnDemandFeatureViews(
def ApplyFeatureService(
self, request: RegistryServer_pb2.ApplyFeatureServiceRequest, context
):
self.proxied_registry.apply_feature_service(
feature_service=cast(
FeatureService,
assert_permissions(
resource=FeatureService.from_proto(request.feature_service),
actions=CRUD,
),
feature_service = cast(
FeatureService,
assert_permissions_to_update(
resource=FeatureService.from_proto(request.feature_service),
getter=self.proxied_registry.get_feature_service,
project=request.project,
),
)
self.proxied_registry.apply_feature_service(
feature_service=feature_service,
project=request.project,
commit=request.commit,
)
Expand Down Expand Up @@ -371,19 +383,19 @@ def DeleteFeatureService(
def ApplySavedDataset(
self, request: RegistryServer_pb2.ApplySavedDatasetRequest, context
):
(
self.proxied_registry.apply_saved_dataset(
saved_dataset=cast(
SavedDataset,
assert_permissions(
resource=SavedDataset.from_proto(request.saved_dataset),
actions=CRUD,
),
),
saved_dataset = cast(
SavedDataset,
assert_permissions_to_update(
resource=SavedDataset.from_proto(request.saved_dataset),
getter=self.proxied_registry.get_saved_dataset,
project=request.project,
commit=request.commit,
),
)
self.proxied_registry.apply_saved_dataset(
saved_dataset=saved_dataset,
project=request.project,
commit=request.commit,
)

return Empty()

Expand Down Expand Up @@ -437,14 +449,16 @@ def DeleteSavedDataset(
def ApplyValidationReference(
self, request: RegistryServer_pb2.ApplyValidationReferenceRequest, context
):
self.proxied_registry.apply_validation_reference(
validation_reference=cast(
ValidationReference,
assert_permissions(
ValidationReference.from_proto(request.validation_reference),
actions=CRUD,
),
validation_reference = cast(
ValidationReference,
assert_permissions_to_update(
resource=ValidationReference.from_proto(request.validation_reference),
getter=self.proxied_registry.get_validation_reference,
project=request.project,
),
)
self.proxied_registry.apply_validation_reference(
validation_reference=validation_reference,
project=request.project,
commit=request.commit,
)
Expand Down Expand Up @@ -547,13 +561,16 @@ def GetInfra(self, request: RegistryServer_pb2.GetInfraRequest, context):
def ApplyPermission(
self, request: RegistryServer_pb2.ApplyPermissionRequest, context
):
self.proxied_registry.apply_permission(
permission=cast(
Permission,
assert_permissions(
Permission.from_proto(request.permission), actions=CRUD
),
permission = cast(
Permission,
assert_permissions_to_update(
resource=Permission.from_proto(request.permission),
getter=self.proxied_registry.get_permission,
project=request.project,
),
)
self.proxied_registry.apply_permission(
permission=permission,
project=request.project,
commit=request.commit,
)
Expand Down
24 changes: 22 additions & 2 deletions sdk/python/tests/unit/permissions/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from feast import FeatureView
from feast.entity import Entity
from feast.infra.registry.base_registry import BaseRegistry
from feast.permissions.decorator import require_permissions
from feast.permissions.permission import AuthzedAction, Permission
Expand Down Expand Up @@ -48,7 +49,10 @@ def users() -> list[User]:
users.append(User("r", ["reader"]))
users.append(User("w", ["writer"]))
users.append(User("rw", ["reader", "writer"]))
users.append(User("admin", ["reader", "writer", "admin"]))
users.append(User("special", ["reader", "writer", "special-reader"]))
users.append(User("updater", ["updater"]))
users.append(User("creator", ["creator"]))
users.append(User("admin", ["updater", "creator"]))
return dict([(u.username, u) for u in users])


Expand Down Expand Up @@ -76,10 +80,26 @@ def security_manager() -> SecurityManager:
name="special",
types=FeatureView,
name_pattern="special.*",
policy=RoleBasedPolicy(roles=["admin", "special-reader"]),
policy=RoleBasedPolicy(roles=["special-reader"]),
actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE],
)
)
permissions.append(
Permission(
name="entity_updater",
types=Entity,
policy=RoleBasedPolicy(roles=["updater"]),
actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE],
)
)
permissions.append(
Permission(
name="entity_creator",
types=Entity,
policy=RoleBasedPolicy(roles=["creator"]),
actions=[AuthzedAction.CREATE],
)
)

registry = Mock(spec=BaseRegistry)
registry.list_permissions = Mock(return_value=permissions)
Expand Down
Loading
Loading