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
No related branches found
No related tags found
1 merge request!215Feature/nat 468 refactor auth
Showing with 102 additions and 78 deletions
...@@ -10,8 +10,7 @@ import gso.products ...@@ -10,8 +10,7 @@ import gso.products
import gso.workflows # noqa: F401 import gso.workflows # noqa: F401
from gso.api import router as api_router from gso.api import router as api_router
from gso.auth.oidc import oidc_instance from gso.auth.oidc import oidc_instance
from gso.auth.opa import opa_instance from gso.auth.opa import graphql_opa_instance, opa_instance
from gso.middlewares import ModifyProcessEndpointResponse
from gso.graphql_api.types import GSO_SCALAR_OVERRIDES from gso.graphql_api.types import GSO_SCALAR_OVERRIDES
SCALAR_OVERRIDES.update(GSO_SCALAR_OVERRIDES) SCALAR_OVERRIDES.update(GSO_SCALAR_OVERRIDES)
...@@ -22,6 +21,7 @@ def init_gso_app() -> OrchestratorCore: ...@@ -22,6 +21,7 @@ def init_gso_app() -> OrchestratorCore:
app = OrchestratorCore(base_settings=app_settings) app = OrchestratorCore(base_settings=app_settings)
app.register_authentication(oidc_instance) app.register_authentication(oidc_instance)
app.register_authorization(opa_instance) app.register_authorization(opa_instance)
app.register_graphql_authorization(graphql_opa_instance)
app.register_graphql() app.register_graphql()
app.include_router(api_router, prefix="/api") app.include_router(api_router, prefix="/api")
return app return app
......
...@@ -8,7 +8,6 @@ from orchestrator.db import ProcessStepTable ...@@ -8,7 +8,6 @@ from orchestrator.db import ProcessStepTable
from orchestrator.schemas.base import OrchestratorBaseModel from orchestrator.schemas.base import OrchestratorBaseModel
from orchestrator.security import authorize from orchestrator.security import authorize
router = APIRouter(prefix="/processes", tags=["Processes"], dependencies=[Depends(authorize)]) router = APIRouter(prefix="/processes", tags=["Processes"], dependencies=[Depends(authorize)])
......
"""OpenID Connect and Open Policy Agent Integration for GSO Application. """Module contains the OIDC Authentication class."""
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.
"""
import re import re
from collections.abc import Callable
from functools import wraps from functools import wraps
from http import HTTPStatus from http import HTTPStatus
from json import JSONDecodeError from json import JSONDecodeError
from typing import Any
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.requests import Request from fastapi.requests import Request
...@@ -28,14 +22,19 @@ _CALLBACK_STEP_API_URL_PATTERN = re.compile( ...@@ -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: def _is_callback_step_endpoint(request: Request) -> bool:
"""Check if the request is a callback step API call.""" """Check if the request is a callback step API call."""
return re.match(_CALLBACK_STEP_API_URL_PATTERN, request.url.path) is not None 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) @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) await self.check_openid_config(async_request)
return await func(self, async_request, *args, **kwargs) return await func(self, async_request, *args, **kwargs)
...@@ -49,15 +48,9 @@ class OIDCAuthentication(OIDCAuth): ...@@ -49,15 +48,9 @@ class OIDCAuthentication(OIDCAuth):
1. Validate the Credentials at :term: `AAI` proxy by calling the UserInfo endpoint 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 @staticmethod
async def is_bypassable_request(request: Request) -> bool: 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) return _is_callback_step_endpoint(request=request)
@ensure_openid_config_loaded @ensure_openid_config_loaded
...@@ -69,7 +62,12 @@ class OIDCAuthentication(OIDCAuth): ...@@ -69,7 +62,12 @@ class OIDCAuthentication(OIDCAuth):
:return: OIDCUserModel: OIDC user model from openid server :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( response = await async_request.post(
self.openid_config.userinfo_endpoint, self.openid_config.userinfo_endpoint,
...@@ -97,6 +95,8 @@ class OIDCAuthentication(OIDCAuth): ...@@ -97,6 +95,8 @@ class OIDCAuthentication(OIDCAuth):
) )
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=response.text) raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=response.text)
data["client_id"] = client_id
return OIDCUserModel(data) return OIDCUserModel(data)
@ensure_openid_config_loaded @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 http import HTTPStatus
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.params import Depends
from httpx import AsyncClient, NetworkError 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 oauth2_lib.settings import oauth2lib_settings
from starlette.requests import Request
from structlog import get_logger from structlog import get_logger
from gso.auth.oidc import oidc_instance
logger = get_logger(__name__) logger = get_logger(__name__)
class OPAAuthorization(OPAAuthorization): async def _get_decision(opa_url: str, async_request: AsyncClient, opa_input: dict) -> OPAResult:
_instance = None 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): return OPAResult(decision_id=json_result["decision_id"], result=json_result["result"]["allow"])
if cls._instance is None:
cls._instance = super(OPAAuthorization, cls).__new__(cls)
return cls._instance class OPAAuthZ(OPAAuthorization):
"""Applies OPA decisions to HTTP requests for authorization."""
async def get_decision(self, async_request: AsyncClient, opa_input: dict) -> OPAResult: 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) """Get the decision from the OPA server."""
try: return await _get_decision(self.opa_url, async_request, opa_input)
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")
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, 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]: ...@@ -37,7 +37,7 @@ def _setup_connection() -> tuple[connector.Connector, IPAMParams]:
return connector.Connector(options), oss return connector.Connector(options), oss
def _allocate_network( # noqa: PLR0917 def _allocate_network(
conn: connector.Connector, conn: connector.Connector,
dns_view: str, dns_view: str,
network_view: str, network_view: str,
......
...@@ -173,6 +173,7 @@ class SharepointParams(BaseSettings): ...@@ -173,6 +173,7 @@ class SharepointParams(BaseSettings):
class AuthParams(BaseSettings): class AuthParams(BaseSettings):
"""Parameters for the authentication service.""" """Parameters for the authentication service."""
opa_url: str opa_url: str
......
...@@ -10,17 +10,19 @@ ALL_ALIVE_STATES: list[str] = [ ...@@ -10,17 +10,19 @@ ALL_ALIVE_STATES: list[str] = [
SubscriptionLifecycle.ACTIVE, SubscriptionLifecycle.ACTIVE,
] ]
WF_USABLE_MAP.update({ WF_USABLE_MAP.update(
"redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], {
"update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_router": [SubscriptionLifecycle.PROVISIONING], "update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "activate_router": [SubscriptionLifecycle.PROVISIONING],
"modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"activate_iptrunk": [SubscriptionLifecycle.PROVISIONING], "modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"terminate_site": ALL_ALIVE_STATES, "activate_iptrunk": [SubscriptionLifecycle.PROVISIONING],
"terminate_router": ALL_ALIVE_STATES, "terminate_site": ALL_ALIVE_STATES,
"terminate_iptrunk": ALL_ALIVE_STATES, "terminate_router": ALL_ALIVE_STATES,
}) "terminate_iptrunk": ALL_ALIVE_STATES,
}
)
# IP trunk workflows # IP trunk workflows
LazyWorkflowInstance("gso.workflows.iptrunk.activate_iptrunk", "activate_iptrunk") LazyWorkflowInstance("gso.workflows.iptrunk.activate_iptrunk", "activate_iptrunk")
......
...@@ -123,10 +123,12 @@ def initialize_subscription( ...@@ -123,10 +123,12 @@ def initialize_subscription(
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append( subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member), IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member),
) )
side_names = sorted([ 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.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}" subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, geant_s_sid:{geant_s_sid}"
return {"subscription": subscription} return {"subscription": subscription}
......
...@@ -229,10 +229,12 @@ def modify_iptrunk_subscription( ...@@ -229,10 +229,12 @@ def modify_iptrunk_subscription(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member), IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member),
) )
side_names = sorted([ 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.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}" subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, geant_s_sid:{geant_s_sid}"
return { return {
......
orchestrator-core==2.1.2 orchestrator-core==2.3.0rc3
requests==2.31.0 requests==2.31.0
infoblox-client~=0.6.0 infoblox-client~=0.6.0
pycountry==23.12.11 pycountry==23.12.11
......
...@@ -11,7 +11,7 @@ setup( ...@@ -11,7 +11,7 @@ setup(
url="https://gitlab.software.geant.org/goat/gap/geant-service-orchestrator", url="https://gitlab.software.geant.org/goat/gap/geant-service-orchestrator",
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[
"orchestrator-core==2.2.1", "orchestrator-core==2.3.0rc3",
"requests==2.31.0", "requests==2.31.0",
"infoblox-client~=0.6.0", "infoblox-client~=0.6.0",
"pycountry==23.12.11", "pycountry==23.12.11",
......
...@@ -6,8 +6,14 @@ from fastapi import HTTPException, Request ...@@ -6,8 +6,14 @@ from fastapi import HTTPException, Request
from httpx import AsyncClient, NetworkError, Response from httpx import AsyncClient, NetworkError, Response
from gso.auth.oidc import ( from gso.auth.oidc import (
OIDCAuthentication,
OIDCConfig, 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 from gso.auth.settings import oauth2lib_settings
...@@ -275,7 +281,7 @@ async def test_oidc_user_call_no_token(oidc_user, mock_request): ...@@ -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_post.return_value = MagicMock(status_code=200, json=lambda: {"active": False})
mock_get.return_value = MagicMock(status_code=200, json=dict) 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 assert result is None
...@@ -288,7 +294,7 @@ async def test_oidc_user_call_token_from_request(oidc_user, mock_request, mock_a ...@@ -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.introspect_token = AsyncMock(return_value={"active": True, "sub": "123"})
oidc_user.userinfo = AsyncMock(return_value=OIDCUserModel({"sub": "123", "name": "John Doe"})) 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 isinstance(result, OIDCUserModel)
assert result["sub"] == "123" assert result["sub"] == "123"
......
...@@ -137,10 +137,12 @@ def test_successful_iptrunk_creation_with_standard_lso_result( ...@@ -137,10 +137,12 @@ def test_successful_iptrunk_creation_with_standard_lso_result(
subscription_id = state["subscription_id"] subscription_id = state["subscription_id"]
subscription = Iptrunk.from_subscription(subscription_id) subscription = Iptrunk.from_subscription(subscription_id)
sorted_sides = sorted([ 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, 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.status == "provisioning"
assert subscription.description == ( assert subscription.description == (
f"IP trunk {sorted_sides[0]} {sorted_sides[1]}, geant_s_sid:{input_form_wizard_data[0]["geant_s_sid"]}" 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( ...@@ -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 mocked_detach_interfaces_from_lag.call_count == num_lag_ifaces # 1 time per nokia side
# Assert all subscription properties have been updated correctly # Assert all subscription properties have been updated correctly
side_names = sorted([ 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.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.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.geant_s_sid == input_form_iptrunk_data[1]["geant_s_sid"]
assert subscription.iptrunk.iptrunk_description == input_form_iptrunk_data[1]["iptrunk_description"] 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.
Finish editing this message first!
Please register or to comment