Skip to content
Snippets Groups Projects
Commit db02c6d1 authored by Mohammad Torkashvand's avatar Mohammad Torkashvand
Browse files

rebase with develop and fix tests

parent 76a4008d
Branches
Tags
1 merge request!215Feature/nat 468 refactor auth
Showing with 102 additions and 78 deletions
......@@ -10,8 +10,7 @@ import gso.products
import gso.workflows # noqa: F401
from gso.api import router as api_router
from gso.auth.oidc import oidc_instance
from gso.auth.opa import opa_instance
from gso.middlewares import ModifyProcessEndpointResponse
from gso.auth.opa import graphql_opa_instance, opa_instance
from gso.graphql_api.types import GSO_SCALAR_OVERRIDES
SCALAR_OVERRIDES.update(GSO_SCALAR_OVERRIDES)
......@@ -22,6 +21,7 @@ def init_gso_app() -> OrchestratorCore:
app = OrchestratorCore(base_settings=app_settings)
app.register_authentication(oidc_instance)
app.register_authorization(opa_instance)
app.register_graphql_authorization(graphql_opa_instance)
app.register_graphql()
app.include_router(api_router, prefix="/api")
return app
......
......@@ -8,7 +8,6 @@ from orchestrator.db import ProcessStepTable
from orchestrator.schemas.base import OrchestratorBaseModel
from orchestrator.security import authorize
router = APIRouter(prefix="/processes", tags=["Processes"], dependencies=[Depends(authorize)])
......
"""OpenID Connect and Open Policy Agent Integration for GSO Application.
This module provides helper functions and classes for handling OpenID Connect (OIDC) and
Open Policy Agent (OPA) related functionalities within the GSO application. It includes
implementations for OIDC-based user authentication and user information modeling. Additionally,
it facilitates making authorization decisions based on policies defined in OPA. Key components
comprise OIDCUser, OIDCUserModel, OPAResult, and opa_decision. These elements integrate with
FastAPI to ensure secure API development.
"""
"""Module contains the OIDC Authentication class."""
import re
from collections.abc import Callable
from functools import wraps
from http import HTTPStatus
from json import JSONDecodeError
from typing import Any
from fastapi.exceptions import HTTPException
from fastapi.requests import Request
......@@ -28,14 +22,19 @@ _CALLBACK_STEP_API_URL_PATTERN = re.compile(
)
def _is_client_credentials_token(intercepted_token: dict) -> bool:
return "sub" not in intercepted_token
def _is_callback_step_endpoint(request: Request) -> bool:
"""Check if the request is a callback step API call."""
return re.match(_CALLBACK_STEP_API_URL_PATTERN, request.url.path) is not None
def ensure_openid_config_loaded(func):
def ensure_openid_config_loaded(func: Callable) -> Callable:
"""Ensure that the openid_config is loaded before calling the function."""
@wraps(func)
async def wrapper(self, async_request: AsyncClient, *args, **kwargs):
async def wrapper(self: OIDCAuth, async_request: AsyncClient, *args: Any, **kwargs: Any) -> dict:
await self.check_openid_config(async_request)
return await func(self, async_request, *args, **kwargs)
......@@ -49,15 +48,9 @@ class OIDCAuthentication(OIDCAuth):
1. Validate the Credentials at :term: `AAI` proxy by calling the UserInfo endpoint
"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(OIDCAuthentication, cls).__new__(cls)
return cls._instance
@staticmethod
async def is_bypassable_request(request: Request) -> bool:
"""Check if the request is a callback step API call."""
return _is_callback_step_endpoint(request=request)
@ensure_openid_config_loaded
......@@ -69,7 +62,12 @@ class OIDCAuthentication(OIDCAuth):
:return: OIDCUserModel: OIDC user model from openid server
"""
await self.introspect_token(async_request, token)
intercepted_token = await self.introspect_token(async_request, token)
client_id = intercepted_token.get("client_id")
if _is_client_credentials_token(intercepted_token):
return OIDCUserModel(
client_id=client_id
)
response = await async_request.post(
self.openid_config.userinfo_endpoint,
......@@ -97,6 +95,8 @@ class OIDCAuthentication(OIDCAuth):
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=response.text)
data["client_id"] = client_id
return OIDCUserModel(data)
@ensure_openid_config_loaded
......
"""Module contains the OPA authorization class that is used to get decisions from the OPA server."""
from http import HTTPStatus
from fastapi.exceptions import HTTPException
from fastapi.params import Depends
from httpx import AsyncClient, NetworkError
from oauth2_lib.fastapi import OIDCUserModel, OPAAuthorization, OPAResult
from oauth2_lib.fastapi import GraphQLOPAAuthorization, OPAAuthorization, OPAResult
from oauth2_lib.settings import oauth2lib_settings
from starlette.requests import Request
from structlog import get_logger
from gso.auth.oidc import oidc_instance
logger = get_logger(__name__)
class OPAAuthorization(OPAAuthorization):
_instance = None
async def _get_decision(opa_url: str, async_request: AsyncClient, opa_input: dict) -> OPAResult:
logger.debug("Posting input json to Policy agent", opa_url=opa_url, input=opa_input)
try:
result = await async_request.post(opa_url, json=opa_input)
except (NetworkError, TypeError) as exc:
logger.debug("Could not get decision from policy agent", error=str(exc))
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Policy agent is unavailable") from exc
json_result = result.json()
logger.debug("Received decision from policy agent", decision=json_result)
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(OPAAuthorization, cls).__new__(cls)
return cls._instance
return OPAResult(decision_id=json_result["decision_id"], result=json_result["result"]["allow"])
class OPAAuthZ(OPAAuthorization):
"""Applies OPA decisions to HTTP requests for authorization."""
async def get_decision(self, async_request: AsyncClient, opa_input: dict) -> OPAResult:
logger.debug("Posting input json to Policy agent", opa_url=self.opa_url, input=opa_input)
try:
result = await async_request.post(self.opa_url, json=opa_input)
except (NetworkError, TypeError) as exc:
logger.debug("Could not get decision from policy agent", error=str(exc))
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Policy agent is unavailable")
"""Get the decision from the OPA server."""
return await _get_decision(self.opa_url, async_request, opa_input)
json_result = result.json()
logger.debug("Received decision from policy agent", decision=json_result)
return OPAResult(decision_id=json_result["decision_id"], result=json_result["result"]["allow"])
class GraphQLOPAAuthZ(GraphQLOPAAuthorization):
"""Specializes OPA authorization for GraphQL operations."""
opa_instance = OPAAuthorization(
async def get_decision(self, async_request: AsyncClient, opa_input: dict) -> OPAResult:
"""Get the decision from the OPA server."""
return await _get_decision(self.opa_url, async_request, opa_input)
opa_instance = OPAAuthZ(
opa_url=oauth2lib_settings.OPA_URL,
)
graphql_opa_instance = GraphQLOPAAuthZ(
opa_url=oauth2lib_settings.OPA_URL,
)
# TODO - Think about Inventoryo-provider since it is not defined in the code but is used in the old branch
\ No newline at end of file
......@@ -37,7 +37,7 @@ def _setup_connection() -> tuple[connector.Connector, IPAMParams]:
return connector.Connector(options), oss
def _allocate_network( # noqa: PLR0917
def _allocate_network(
conn: connector.Connector,
dns_view: str,
network_view: str,
......
......@@ -173,6 +173,7 @@ class SharepointParams(BaseSettings):
class AuthParams(BaseSettings):
"""Parameters for the authentication service."""
opa_url: str
......
......@@ -10,17 +10,19 @@ ALL_ALIVE_STATES: list[str] = [
SubscriptionLifecycle.ACTIVE,
]
WF_USABLE_MAP.update({
"redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_router": [SubscriptionLifecycle.PROVISIONING],
"deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_iptrunk": [SubscriptionLifecycle.PROVISIONING],
"terminate_site": ALL_ALIVE_STATES,
"terminate_router": ALL_ALIVE_STATES,
"terminate_iptrunk": ALL_ALIVE_STATES,
})
WF_USABLE_MAP.update(
{
"redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_router": [SubscriptionLifecycle.PROVISIONING],
"deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_iptrunk": [SubscriptionLifecycle.PROVISIONING],
"terminate_site": ALL_ALIVE_STATES,
"terminate_router": ALL_ALIVE_STATES,
"terminate_iptrunk": ALL_ALIVE_STATES,
}
)
# IP trunk workflows
LazyWorkflowInstance("gso.workflows.iptrunk.activate_iptrunk", "activate_iptrunk")
......
......@@ -123,10 +123,12 @@ def initialize_subscription(
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member),
)
side_names = sorted([
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
])
side_names = sorted(
[
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
]
)
subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, geant_s_sid:{geant_s_sid}"
return {"subscription": subscription}
......
......@@ -229,10 +229,12 @@ def modify_iptrunk_subscription(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member),
)
side_names = sorted([
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
])
side_names = sorted(
[
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
]
)
subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, geant_s_sid:{geant_s_sid}"
return {
......
orchestrator-core==2.1.2
orchestrator-core==2.3.0rc3
requests==2.31.0
infoblox-client~=0.6.0
pycountry==23.12.11
......
......@@ -11,7 +11,7 @@ setup(
url="https://gitlab.software.geant.org/goat/gap/geant-service-orchestrator",
packages=find_packages(),
install_requires=[
"orchestrator-core==2.2.1",
"orchestrator-core==2.3.0rc3",
"requests==2.31.0",
"infoblox-client~=0.6.0",
"pycountry==23.12.11",
......
......@@ -6,8 +6,14 @@ from fastapi import HTTPException, Request
from httpx import AsyncClient, NetworkError, Response
from gso.auth.oidc import (
OIDCAuthentication,
OIDCConfig,
OIDCAuthentication, OIDCUserModel, OPAResult, opa_decision, _get_decision, _evaluate_decision, _is_callback_step_endpoint,
OIDCUserModel,
OPAResult,
_evaluate_decision,
_get_decision,
_is_callback_step_endpoint,
opa_decision,
)
from gso.auth.settings import oauth2lib_settings
......@@ -275,7 +281,7 @@ async def test_oidc_user_call_no_token(oidc_user, mock_request):
mock_post.return_value = MagicMock(status_code=200, json=lambda: {"active": False})
mock_get.return_value = MagicMock(status_code=200, json=dict)
result = await oidc_user.__call__(mock_request) # noqa: PLC2801
result = await oidc_user.__call__(mock_request)
assert result is None
......@@ -288,7 +294,7 @@ async def test_oidc_user_call_token_from_request(oidc_user, mock_request, mock_a
oidc_user.introspect_token = AsyncMock(return_value={"active": True, "sub": "123"})
oidc_user.userinfo = AsyncMock(return_value=OIDCUserModel({"sub": "123", "name": "John Doe"}))
result = await oidc_user.__call__(mock_request) # noqa: PLC2801
result = await oidc_user.__call__(mock_request)
assert isinstance(result, OIDCUserModel)
assert result["sub"] == "123"
......
......@@ -137,10 +137,12 @@ def test_successful_iptrunk_creation_with_standard_lso_result(
subscription_id = state["subscription_id"]
subscription = Iptrunk.from_subscription(subscription_id)
sorted_sides = sorted([
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
])
sorted_sides = sorted(
[
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
]
)
assert subscription.status == "provisioning"
assert subscription.description == (
f"IP trunk {sorted_sides[0]} {sorted_sides[1]}, geant_s_sid:{input_form_wizard_data[0]["geant_s_sid"]}"
......
......@@ -158,10 +158,12 @@ def test_iptrunk_modify_trunk_interface_success(
assert mocked_detach_interfaces_from_lag.call_count == num_lag_ifaces # 1 time per nokia side
# Assert all subscription properties have been updated correctly
side_names = sorted([
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
])
side_names = sorted(
[
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
]
)
assert subscription.description == f"IP trunk {side_names[0]} {side_names[1]}, geant_s_sid:{new_sid}"
assert subscription.iptrunk.geant_s_sid == input_form_iptrunk_data[1]["geant_s_sid"]
assert subscription.iptrunk.iptrunk_description == input_form_iptrunk_data[1]["iptrunk_description"]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment