Skip to content

Commit

Permalink
[API+SDK] Add option to query permissions from the client (#1161)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hedingber committed Jul 29, 2021
1 parent b67f3cd commit 6ea27f3
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 29 deletions.
4 changes: 4 additions & 0 deletions mlrun/api/api/api.py
Expand Up @@ -3,6 +3,7 @@
from mlrun.api.api import deps
from mlrun.api.api.endpoints import (
artifacts,
auth,
background_tasks,
feature_store,
files,
Expand All @@ -26,6 +27,9 @@
api_router.include_router(
artifacts.router, tags=["artifacts"], dependencies=[Depends(deps.AuthVerifier)]
)
api_router.include_router(
auth.router, tags=["auth"], dependencies=[Depends(deps.AuthVerifier)]
)
api_router.include_router(
background_tasks.router,
tags=["background-tasks"],
Expand Down
21 changes: 21 additions & 0 deletions mlrun/api/api/endpoints/auth.py
@@ -0,0 +1,21 @@
import fastapi

import mlrun.api.api.deps
import mlrun.api.schemas
import mlrun.api.utils.clients.opa

router = fastapi.APIRouter()


@router.post("/authorization/verifications")
def verify_authorization(
authorization_verification_input: mlrun.api.schemas.AuthorizationVerificationInput,
auth_verifier: mlrun.api.api.deps.AuthVerifier = fastapi.Depends(
mlrun.api.api.deps.AuthVerifier
),
):
mlrun.api.utils.clients.opa.Client().query_permissions(
authorization_verification_input.resource,
authorization_verification_input.action,
auth_verifier.auth_info,
)
1 change: 1 addition & 0 deletions mlrun/api/schemas/__init__.py
Expand Up @@ -5,6 +5,7 @@
AuthInfo,
AuthorizationAction,
AuthorizationResourceTypes,
AuthorizationVerificationInput,
ProjectsRole,
)
from .background_task import (
Expand Down
25 changes: 25 additions & 0 deletions mlrun/api/schemas/auth.py
Expand Up @@ -39,6 +39,31 @@ class AuthorizationResourceTypes(str, enum.Enum):
run = "run"
model_endpoint = "model-endpoint"

def to_resource_string(
self, project_name: str, resource_name: str,
):
return {
AuthorizationResourceTypes.function: "/projects/{project_name}/functions/{resource_name}",
AuthorizationResourceTypes.artifact: "/projects/{project_name}/artifacts/{resource_name}",
AuthorizationResourceTypes.background_task: "/projects/{project_name}/background-tasks/{resource_name}",
AuthorizationResourceTypes.feature_set: "/projects/{project_name}/feature-sets/{resource_name}",
AuthorizationResourceTypes.feature_vector: "/projects/{project_name}/feature-vectors/{resource_name}",
AuthorizationResourceTypes.feature: "/projects/{project_name}/features/{resource_name}",
AuthorizationResourceTypes.entity: "/projects/{project_name}/entities/{resource_name}",
AuthorizationResourceTypes.log: "/projects/{project_name}/runs/{resource_name}/logs",
AuthorizationResourceTypes.schedule: "/projects/{project_name}/schedules/{resource_name}",
AuthorizationResourceTypes.secret: "/projects/{project_name}/secrets/{resource_name}",
AuthorizationResourceTypes.run: "/projects/{project_name}/runs/{resource_name}",
# runtime resource doesn't have a get (one) object endpoint, it doesn't have an identifier
AuthorizationResourceTypes.runtime_resource: "/projects/{project_name}/runtime-resources/",
AuthorizationResourceTypes.model_endpoint: "/projects/{project_name}/model-endpoints/{resource_name}",
}[self].format(project_name=project_name, resource_name=resource_name)


class AuthorizationVerificationInput(pydantic.BaseModel):
resource: str
action: AuthorizationAction


class AuthInfo(pydantic.BaseModel):
# Basic + Iguazio auth
Expand Down
30 changes: 1 addition & 29 deletions mlrun/api/utils/clients/opa.py
Expand Up @@ -101,7 +101,7 @@ def query_resource_permissions(
if not resource_name:
resource_name = "*"
return self.query_permissions(
self._generate_resource_string(project_name, resource_type, resource_name),
resource_type.to_resource_string(project_name, resource_name),
action,
auth_info,
raise_on_forbidden,
Expand Down Expand Up @@ -202,34 +202,6 @@ def add_allowed_project_for_owner(
allowed_projects[project_name] = ttl
self._allowed_project_owners_cache[auth_info.user_id] = allowed_projects

def _generate_resource_string(
self,
project_name: str,
resource_type: mlrun.api.schemas.AuthorizationResourceTypes,
resource_name: str,
):
return {
mlrun.api.schemas.AuthorizationResourceTypes.function: "/projects/{project_name}/functions/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.artifact: "/projects/{project_name}/artifacts/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.background_task: "/projects/{project_name}/background-tasks/{r"
"esource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.feature_set: "/projects/{project_name}/feature-sets/{resource_"
"name}",
mlrun.api.schemas.AuthorizationResourceTypes.feature_vector: "/projects/{project_name}/feature-vectors/{res"
"ource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.feature: "/projects/{project_name}/features/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.entity: "/projects/{project_name}/entities/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.log: "/projects/{project_name}/runs/{resource_name}/logs",
mlrun.api.schemas.AuthorizationResourceTypes.schedule: "/projects/{project_name}/schedules/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.secret: "/projects/{project_name}/secrets/{resource_name}",
mlrun.api.schemas.AuthorizationResourceTypes.run: "/projects/{project_name}/runs/{resource_name}",
# runtime resource doesn't have a get (one) object endpoint, it doesn't have an identifier
mlrun.api.schemas.AuthorizationResourceTypes.runtime_resource: "/projects/{project_name}/runtime-resources/"
"",
mlrun.api.schemas.AuthorizationResourceTypes.model_endpoint: "/projects/{project_name}/model-endpoints/{res"
"ource_name}",
}[resource_type].format(project_name=project_name, resource_name=resource_name)

def _is_request_from_leader(
self, projects_role: typing.Optional[mlrun.api.schemas.ProjectsRole]
):
Expand Down
6 changes: 6 additions & 0 deletions mlrun/db/base.py
Expand Up @@ -415,3 +415,9 @@ def get_endpoint(
access_key: Optional[str] = None,
):
pass

@abstractmethod
def verify_authorization(
self, authorization_verification_input: schemas.AuthorizationVerificationInput
):
pass
6 changes: 6 additions & 0 deletions mlrun/db/filedb.py
Expand Up @@ -722,6 +722,12 @@ def get_endpoint(
):
raise NotImplementedError()

def verify_authorization(
self,
authorization_verification_input: mlrun.api.schemas.AuthorizationVerificationInput,
):
raise NotImplementedError()


def make_time_pred(since, until):
if not (since or until):
Expand Down
17 changes: 17 additions & 0 deletions mlrun/db/httpdb.py
Expand Up @@ -2239,6 +2239,23 @@ def get_endpoint(
)
return schemas.ModelEndpoint(**response.json())

def verify_authorization(
self, authorization_verification_input: schemas.AuthorizationVerificationInput
):
""" Verifies authorization for the provided action on the provided resource.
:param authorization_verification_input: Instance of
:py:class:`~mlrun.api.schemas.AuthorizationVerificationInput` that includes all the needed parameters for
the auth verification
"""
error_message = "Authorization check failed"
self.api_call(
"POST",
"authorization/verifications",
error_message,
body=dict_to_json(authorization_verification_input),
)


def _as_json(obj):
fn = getattr(obj, "to_json", None)
Expand Down
6 changes: 6 additions & 0 deletions mlrun/db/sqldb.py
Expand Up @@ -732,3 +732,9 @@ def get_endpoint(
access_key=None,
):
raise NotImplementedError()

def verify_authorization(
self,
authorization_verification_input: mlrun.api.schemas.AuthorizationVerificationInput,
):
raise NotImplementedError()
27 changes: 27 additions & 0 deletions tests/api/api/test_auth.py
@@ -0,0 +1,27 @@
import http

import fastapi.testclient
import sqlalchemy.orm

import mlrun.api.schemas
import mlrun.api.utils.clients.opa


def test_verify_authorization(
db: sqlalchemy.orm.Session, client: fastapi.testclient.TestClient
) -> None:
authorization_verification_input = mlrun.api.schemas.AuthorizationVerificationInput(
resource="/some-resource", action=mlrun.api.schemas.AuthorizationAction.create
)

def _mock_successful_query_permissions(resource, action, *args):
assert authorization_verification_input.resource == resource
assert authorization_verification_input.action == action

mlrun.api.utils.clients.opa.Client().query_permissions = (
_mock_successful_query_permissions
)
response = client.post(
"/api/authorization/verifications", json=authorization_verification_input.dict()
)
assert response.status_code == http.HTTPStatus.OK.value

0 comments on commit 6ea27f3

Please sign in to comment.