Skip to content

Commit

Permalink
Add permission service (#296)
Browse files Browse the repository at this point in the history
Add permission service
  • Loading branch information
FinnStutzenstein committed Nov 17, 2020
1 parent 93030c6 commit 1b69f26
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 23 deletions.
2 changes: 1 addition & 1 deletion dev/cleanup.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

printf "Black:\n"
black openslides_backend tests cli
black openslides_backend tests cli --exclude tests/integration/old
printf "\nIsort:\n"
isort openslides_backend tests cli
printf "\nFlake8:\n"
Expand Down
10 changes: 7 additions & 3 deletions openslides_backend/action/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..services.auth.interface import AuthenticationService
from ..services.datastore.interface import DatastoreService
from ..services.media.interface import MediaService
from ..services.permission.interface import PermissionService
from ..services.permission.interface import NotAllowed, PermissionService
from ..shared.exceptions import ActionException, PermissionDenied
from ..shared.interfaces.event import Event
from ..shared.interfaces.services import Services
Expand Down Expand Up @@ -107,9 +107,13 @@ def check_permissions(self, payload: ActionPayload) -> None:
"""
Checks permission by requesting permission service.
"""
if not self.permission.check_action(self.user_id, self.name, payload):
try:
self.additions = self.permission.is_allowed(
self.name, self.user_id, list(payload)
)
except NotAllowed as e:
raise PermissionDenied(
f"You are not allowed to perform action {self.name}."
f"You are not allowed to perform action {self.name}: {e.reason}"
)

def prepare_dataset(self, payload: ActionPayload) -> DataSet:
Expand Down
2 changes: 1 addition & 1 deletion openslides_backend/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"PERMISSION_PROTOCOL": "http",
"PERMISSION_HOST": "localhost",
"PERMISSION_PORT": "9005",
"PERMISSION_PATH": "",
"PERMISSION_PATH": "/internal/permission",
"MEDIA_PROTOCOL": "http",
"MEDIA_HOST": "localhost",
"MEDIA_PORT": "9006",
Expand Down
73 changes: 64 additions & 9 deletions openslides_backend/services/permission/adapter.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
from typing import Any
import json
from typing import Any, Dict, List

from .interface import PermissionService
import requests

from ...shared.exceptions import PermissionException
from ...shared.interfaces.logging import LoggingModule
from .interface import NotAllowed, PermissionService


class PermissionHTTPAdapter(PermissionService):
"""
Adapter to connect to permission service.
"""

def __init__(self, permission_url: str) -> None:
self.url = permission_url
# self.headers = {"Content-Type": "application/json"}
def __init__(self, permission_url: str, logging: LoggingModule) -> None:
self.endpoint = permission_url + "/is_allowed"
self.logger = logging.getLogger(__name__)

def is_allowed(
self, name: str, user_id: int, data_list: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
payload = json.dumps(
{"name": name, "user_id": user_id, "data": data_list}, separators=(",", ":")
)

try:
response = requests.post(
url=self.endpoint,
data=payload,
headers={"Content-Type": "application/json"},
)
except requests.exceptions.ConnectionError as e:
raise PermissionException(
f"Cannot reach the permission service on {self.endpoint}. Error: {e}"
)

content = response.json()
self.logger.debug(f"Permission service response: {str(content)}")

if "error" in content:
type = content["error"]["type"]
msg = content["error"]["msg"]
raise PermissionException(f"Error in permission service. {type}: {msg}")

allowed = content.get("allowed", False)

if not allowed:
reason = content.get("reason")
error_index = content.get("error_index")
if error_index < 0:
error_index = None

# TODO: dev only. Log about missing perms check
if "no such query" in reason:
self.logger.warning(
f"Action {name} has no permission check. Return a default-true."
)
return [{} for _ in data_list]

raise NotAllowed(reason, error_index)

additions = content.get("additions")
if not isinstance(additions, list):
raise PermissionException("additions must be a list")

for i in range(len(additions)):
if additions[i] is None:
additions[i] = {}
if not isinstance(additions[i], dict):
raise PermissionError(f"Addition {i} is not a dict")

def check_action(self, user_id: int, action: str, data: Any) -> bool:
# TODO: Do not use hardcoded value here but send request to
# permission service.
return True
return additions
17 changes: 14 additions & 3 deletions openslides_backend/services/permission/interface.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from typing import Any, Protocol
from typing import Any, Dict, List, Optional, Protocol


class NotAllowed(Exception):
def __init__(self, reason: str, error_index: Optional[int]):
self.reason = reason
self.error_index = error_index
super().__init__(reason)


class PermissionService(Protocol):
"""
Interface of the permission service.
"""

def check_action(self, user_id: int, action: str, data: Any) -> bool:
def is_allowed(
self, name: str, user_id: int, data_list: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Check if the given action with the given data is allowed for the fiven user.
Checks, if the user is allowed to execute the `name` with the list of data.
If it is allowed, additional information will be returned for each data. If not, NotAllowed
will be thrown providing more information.
"""
4 changes: 4 additions & 0 deletions openslides_backend/shared/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class DatastoreException(ServiceException):
pass


class PermissionException(ServiceException):
pass


class EventStoreException(View400Exception):
pass

Expand Down
4 changes: 3 additions & 1 deletion openslides_backend/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class OpenSlidesBackendServices(containers.DeclarativeContainer):
config = providers.Configuration("config")
logging = providers.Object(0)
authentication = providers.Singleton(AuthenticationHTTPAdapter, logging)
permission = providers.Singleton(PermissionHTTPAdapter, config.permission_url)
permission = providers.Singleton(
PermissionHTTPAdapter, config.permission_url, logging
)
media = providers.Singleton(MediaServiceAdapter, config.media_url, logging)
engine = providers.Singleton(
HTTPEngine, config.datastore_reader_url, config.datastore_writer_url, logging
Expand Down
17 changes: 12 additions & 5 deletions tests/integration/old/fake_services/permission.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any
from typing import Any, Dict, List

from openslides_backend.services.permission.interface import NotAllowed


class PermissionTestAdapter:
Expand All @@ -8,8 +10,13 @@ class PermissionTestAdapter:
implementation.
"""

def __init__(self, superuser: int, *args: Any, **kwargs: Any) -> None:
self.superuser = superuser
def __init__(self, superuser_id: int, *args: Any, **kwargs: Any) -> None:
self.superuser_id = superuser_id

def check_action(self, user_id: int, action: str, data: Any) -> bool:
return user_id == self.superuser
def is_allowed(
self, name: str, user_id: int, data: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if user_id == self.superuser_id:
return [{} for _ in data]
else:
raise NotAllowed("Not the superuser")
Empty file.
4 changes: 4 additions & 0 deletions tests/system/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from openslides_backend.environment import get_environment
from openslides_backend.http.views import ActionView, PresenterView
from openslides_backend.services.media.interface import MediaService
from openslides_backend.services.permission.interface import PermissionService
from openslides_backend.shared.interfaces.wsgi import View, WSGIApplication
from openslides_backend.wsgi import OpenSlidesBackendServices, OpenSlidesBackendWSGI

Expand All @@ -28,6 +29,9 @@ def create_test_application(view: Type[View]) -> WSGIApplication:
)
mock_media_service = Mock(MediaService)
services.media = MagicMock(return_value=mock_media_service)
mock_permission_service = Mock(PermissionService)
mock_permission_service.is_allowed = MagicMock(return_value=True)
services.permission = MagicMock(return_value=mock_permission_service)

# Create WSGI application instance. Inject logging module, view class and services container.
application_factory = OpenSlidesBackendWSGI(
Expand Down

0 comments on commit 1b69f26

Please sign in to comment.