Skip to content
Open
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
7 changes: 7 additions & 0 deletions frontend/amundsen_application/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ class MailClientNotImplemented(Exception):
An exception when Mail Client is not implemented
"""
pass


class AuthorizationMappingMissingException(Exception):
"""
An exception raised when mapping from given request to required action is missing
"""
pass
18 changes: 16 additions & 2 deletions frontend/amundsen_application/api/metadata/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from amundsen_application.api.utils.request_utils import get_query_param, request_metadata

from amundsen_application.api.utils.search_utils import execute_search_document_request
from amundsen_application.api.utils.authz_utils import get_required_action_from_request, \
is_subject_authorized_to_perform_action_on_object


LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -138,8 +140,20 @@ def get_table_metadata() -> Response:
list_item_index = request.args.get('index', None)
list_item_source = request.args.get('source', None)

results_dict = _get_table_metadata(table_key=table_key, index=list_item_index, source=list_item_source)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
required_action_permission = get_required_action_from_request(request)
is_authorized = is_subject_authorized_to_perform_action_on_object(
user = app.config['AUTH_USER_METHOD'](app),
object_type = ResourceType.Table,
object_id = table_key,
required_action = required_action_permission,
)
if is_authorized == True:
results_dict = _get_table_metadata(table_key=table_key, index=list_item_index, source=list_item_source)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
else:
message = "User is not authorized to access the resource"
return make_response(jsonify({'tableData': {}, 'msg': message}), HTTPStatus.FORBIDDEN)

except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
Expand Down
56 changes: 56 additions & 0 deletions frontend/amundsen_application/api/utils/authz_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from flask import Request, current_app as app

from amundsen_common.entity.resource_type import ResourceType
from amundsen_common.models.user import User
from amundsen_application.authz.actions.base import BaseAction
from amundsen_application.authz.clients.base import BaseClient
from amundsen_application.authz.mappers.base import BaseMapper
from amundsen_application.api.exceptions import AuthorizationMappingMissingException
from typing import Optional

AUTHZ_CLIENT_INSTANCE = None

def get_authz_client() -> Optional[BaseClient]:
global AUTHZ_CLIENT_INSTANCE
if app.config["AUTHORIZATION_ENABLED"] and app.config["AUTHORIZATION_CLIENT_CLASS"] is None:
raise Exception("Authorization client is not configured")
if app.config["AUTHORIZATION_ENABLED"] and AUTHZ_CLIENT_INSTANCE is None:
AUTHZ_CLIENT_INSTANCE = app.config["AUTHORIZATION_CLIENT_CLASS"]()

return AUTHZ_CLIENT_INSTANCE


def get_required_action_from_request(request: Request) -> BaseAction:
request_to_action_mapper: BaseMapper = app.config["AUTHORIZATION_REQUEST_TO_ACTION_MAPPER"]
if app.config["AUTHORIZATION_ENABLED"] and request_to_action_mapper is None:
raise Exception("Request to action mapping is not configured")

return request_to_action_mapper.get_mapping(request=request)


def is_subject_authorized_to_perform_action_on_object(
*,
user: User,
object_type: ResourceType,
object_id: str,
required_action: BaseAction) -> bool:
is_authorized = False
if app.config["AUTHORIZATION_ENABLED"] == False:
is_authorized = True
return is_authorized
else:
authz_client = get_authz_client()
if authz_client is None:
raise Exception("Can not get authorization client. Make sure that AUTHORIZATION_CLIENT_CLASS is set")
try:
is_authorized = authz_client.is_authorized(
user=user,
object_type=object_type,
object_id=object_id,
action=required_action,
)

except AuthorizationMappingMissingException as e:
is_authorized = app.config["AUTHORIZATION_ALLOW_ACCESS_ON_MISSING_MAPPING"]

return is_authorized
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions frontend/amundsen_application/authz/actions/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from enum import Enum
from typing import Type

class BaseAction(Enum):
pass

def to_action(*, action_enum_cls: Type[BaseAction], label: str) -> Enum:
return action_enum_cls[label.title()]

def to_label(*, action: BaseAction) -> str:
return action.name.lower()
6 changes: 6 additions & 0 deletions frontend/amundsen_application/authz/actions/rw_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from amundsen_application.authz.actions.base import BaseAction
from enum import auto

class RWAction(BaseAction):
READ = auto()
WRITE = auto()
Empty file.
20 changes: 20 additions & 0 deletions frontend/amundsen_application/authz/clients/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from abc import ABCMeta, abstractmethod
from amundsen_common.models.user import User
from amundsen_common.entity.resource_type import ResourceType
from amundsen_application.authz.actions.base import (BaseAction)

from enum import Enum, auto


class BaseClient(metaclass=ABCMeta):
"""
Base Client, which behaves like an interface for all
"""

@abstractmethod
def is_authorized(self, *, user: User, object_type: ResourceType, object_id: str, action: BaseAction) -> bool:
pass

"""
TODO - different methods - get_user_permissions, get_authorized_users, filter_search_request
"""
33 changes: 33 additions & 0 deletions frontend/amundsen_application/authz/clients/casbin_db_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from amundsen_common.entity.resource_type import ResourceType, to_label as resource_type_to_label
from amundsen_application.authz.actions.base import BaseAction, to_label as action_to_label
from amundsen_application.authz.clients.base import BaseClient
from amundsen_common.models.user import User
import casbin_sqlalchemy_adapter
import casbin
import os
from sqlalchemy import create_engine

class CasbinDbClient(BaseClient):
"""
WIP - Authorization Client that leverages Casbin as policy enforcer and persistent database as policy storage
"""

def __init__(self) -> None:
db_url = os.getenv("CASBIN_MODEL_DATABASE_ENGINE_URL")
if db_url is None:
raise Exception("Casbin Database URL not specified. set url as 'CASBIN_MODEL_DATABASE_ENGINE_URL' env variable")
casbin_model_config_path = os.getenv("CASBIN_MODEL_CONFIG_PATH")
if casbin_model_config_path is None:
raise Exception("Casbin config file path not specified. Set path to the file as 'CASBIN_MODEL_CONFIG_PATH' env variable")

self.engine = create_engine()
self.adapter = casbin_sqlalchemy_adapter.Adapter(self.engine)
self.enforcer = casbin.Enforcer(casbin_model_config_path, self.adapter)

def is_authorized(self, *, user: User, object_type: ResourceType, object_id: str, action: BaseAction) -> bool:
return self.enforcer.enforce(
user.user_id,
resource_type_to_label(resource_type=object_type),
object_id,
action_to_label(action=action)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from amundsen_common.entity.resource_type import ResourceType, to_label as resource_type_to_label
from amundsen_application.authz.actions.base import BaseAction, to_label as action_to_label
from amundsen_application.authz.clients.base import BaseClient
from amundsen_common.models.user import User
import casbin
import os
import sys
import inspect

class CasbinExampleCsvClient(BaseClient):
"""
Example implementation of Authorization Client using Casbin
"""

def __init__(self) -> None:
script_dir = os.path.dirname(inspect.getfile(CasbinExampleCsvClient))
base_path = os.path.join(script_dir, "casbin_example_csv_client")
policy_file = os.path.join(base_path, "policy.csv")
model_file = os.path.join(base_path, "model.conf")
self.enforcer = casbin.Enforcer(model_file, policy_file)

def is_authorized(self, *, user: User, object_type: ResourceType, object_id: str, action: BaseAction) -> bool:
return self.enforcer.enforce(
user.user_id,
resource_type_to_label(resource_type=object_type),
object_id,
action_to_label(action=action)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[request_definition]
r = sub,type, obj, act

[policy_definition]
p = sub, type, obj, act


[policy_effect]
e = some(where (p.eft == allow))

[matchers]
# match subject (e.g. user), match action(e.g. read), regex match type (e.g table), regex match object (e.g. table id)
m = r.sub == p.sub && r.act == p.act && regexMatch(r.type, p.type) && regexMatch(r.obj, p.obj)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
p, test_user_id, table, hive://*, read
Empty file.
20 changes: 20 additions & 0 deletions frontend/amundsen_application/authz/mappers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from abc import ABCMeta, abstractmethod
from amundsen_application.authz.actions.base import BaseAction
from typing import Dict, Any
from flask import Request

class BaseMapper(metaclass=ABCMeta):
"""
Base class for adding mappings between requests and actions
"""
@abstractmethod
def __init__(self) -> None:
self._mappings: Dict[Any, Any] = {}

@abstractmethod
def add_mapping(self, required_action: BaseAction, **kwargs: Any) -> None:
pass

@abstractmethod
def get_mapping(self, *, request: Request) -> BaseAction:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from flask import Request
from typing import Dict, Any
from amundsen_application.authz.actions.base import BaseAction
from amundsen_application.authz.mappers.base import BaseMapper
from amundsen_application.api.exceptions import AuthorizationMappingMissingException


class DefaultRequestToActionMapper(BaseMapper):
"""
Reference implementation of mapper.
Given request context, checks blueprint and function used to process the request
and returns the corresponding action.
"""
def __init__(self) -> None:
self._mappings: Dict[str, Dict[str, BaseAction]] = {}

def add_mapping(self, required_action: BaseAction, **kwargs: Any) -> None:
if not "blueprint_name" in kwargs:
raise Exception("Expected `blueprint_name` in keyword arguments")
if not "function_name" in kwargs:
raise Exception("Expected `function_name` in keyword arguments")

blueprint_name = kwargs["blueprint_name"]
function_name = kwargs["function_name"]
self._mappings[blueprint_name] = self._mappings.get(blueprint_name, {})
self._mappings[blueprint_name][function_name] = required_action

def get_mapping(self, *, request: Request) -> BaseAction:
if not request.endpoint:
raise Exception(
"Unexpected error: Request do not contain an endpoint"
)
blueprint_name, function_name = request.endpoint.split('.')
if blueprint_name not in self._mappings:
raise AuthorizationMappingMissingException(
f'Authorization mapping not specified for blueprint {blueprint_name}'
)
if function_name not in self._mappings[blueprint_name]:
raise AuthorizationMappingMissingException(
f'Authorization mapping not specified for function {function_name} of blueprint {blueprint_name}'
)
return self._mappings[blueprint_name][function_name]
32 changes: 32 additions & 0 deletions frontend/amundsen_application/authz_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright Contributors to the Amundsen project.
# SPDX-License-Identifier: Apache-2.0

from amundsen_application.authz.actions.rw_action import RWAction
from amundsen_application.authz.mappers.default_request_to_action_mapper import DefaultRequestToActionMapper
from amundsen_application.authz.clients.casbin_example_csv_client import CasbinExampleCsvClient

AUTHORIZATION_ENABLED = True
AUTHORIZATION_CLIENT_CLASS = CasbinExampleCsvClient
AUTHORIZATION_REQUEST_TO_ACTION_MAPPER = DefaultRequestToActionMapper()
AUTHORIZATION_ACTION_ENUM = RWAction
AUTHORIZATION_ALLOW_ACCESS_ON_MISSING_MAPPING = True


# Subject accessing 'get_table_metadata' defined in blueprint 'metadata'
# has to have 'read' action allowed in order to access the table metadata
AUTHORIZATION_REQUEST_TO_ACTION_MAPPER.add_mapping(
blueprint_name="metadata",
function_name="get_table_metadata",
required_action=AUTHORIZATION_ACTION_ENUM.READ,
)


"""
# One can follow the same logic to add more mappings...

AUTHORIZATION_REQUEST_TO_ACTION_MAPPER.add_mapping(
blueprint_name="metadata",
function_name="update_table_tags",
required_action=AUTHORIZATION_ACTION_ENUM.WRITE,
)
"""
9 changes: 9 additions & 0 deletions frontend/amundsen_application/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class Config:
CREDENTIALS_MODE_ADMIN_PASSWORD = os.getenv('CREDENTIALS_MODE_ADMIN_PASSWORD', None)
MODE_ORGANIZATION = None
MODE_REPORT_URL_TEMPLATE = None

# Add Preview class name below to enable ACL, assuming it is supported by the Preview class
# e.g: ACL_ENABLED_DASHBOARD_PREVIEW = {'ModePreview'}
ACL_ENABLED_DASHBOARD_PREVIEW = set() # type: Set[Optional[str]]
Expand All @@ -145,6 +146,13 @@ class Config:
MTLS_CLIENT_KEY = os.getenv('MTLS_CLIENT_KEY')
"""Optional. The path to a PEM formatted key to use with the MTLS_CLIENT_CERT. MTLS_CLIENT_CERT must also be set."""

from amundsen_application.authz_config import (
AUTHORIZATION_ENABLED,
AUTHORIZATION_CLIENT_CLASS,
AUTHORIZATION_REQUEST_TO_ACTION_MAPPER,
AUTHORIZATION_ALLOW_ACCESS_ON_MISSING_MAPPING
)


class LocalConfig(Config):
DEBUG = False
Expand Down Expand Up @@ -184,6 +192,7 @@ class LocalConfig(Config):
class TestConfig(LocalConfig):
POPULAR_RESOURCES_PERSONALIZATION = True
AUTH_USER_METHOD = get_test_user
AUTHORIZATION_ENABLED = False
NOTIFICATIONS_ENABLED = True
ISSUE_TRACKER_URL = 'test_url'
ISSUE_TRACKER_USER = 'test_user'
Expand Down
Loading