Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Commit

Permalink
fix: provider as tenant login (#820)
Browse files Browse the repository at this point in the history
* draft

* bump version

* update env variables usage
  • Loading branch information
sbasan authored Sep 11, 2024
1 parent a768218 commit 1949b59
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 79 deletions.
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ Always write a clear log message for your commits.
### Environment Variables
- `catalystwan_devel` when set: loggers will be configured according to `./logging.conf` and `urllib3.exceptions.InsecureRequestWarning` will be suppressed
- `catalystwan_export_endpoints` when set `endpoints-md` pre-commit step will generate `ENDPOINTS.md` file in addition to perform definition checks. This should be set only when creating version bump commit with new release tag.
- `catalystwan_auth_trace` when set: authentication requests will not be anonymized in logs
- `catalystwan_export_endpoints` when set: `endpoints-md` pre-commit step will generate `ENDPOINTS.md` file in addition to perform definition checks. This should be set only when creating version bump commit with new release tag.
## Code guidelines
Expand Down
12 changes: 6 additions & 6 deletions ENDPOINTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
**THIS FILE WAS AUTO-GENERATED DO NOT EDIT**

Generated for: catalystwan-0.35.1
Generated for: catalystwan-0.35.3

All URIs are relative to */dataservice*
HTTP request | Supported Versions | Method | Payload Type | Return Type | Tenancy Mode
Expand All @@ -25,11 +25,11 @@ PUT /admin/user/password/{username}||[**AdministrationUserAndGroup.update_passwo
PUT /admin/user/profile/password||[**AdministrationUserAndGroup.update_profile_password**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L327)|[**ProfilePasswordUpdateRequest**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L167)|None|
PUT /admin/user/{username}||[**AdministrationUserAndGroup.update_user**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L331)|[**UserUpdateRequest**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L34)|None|
PUT /admin/usergroup/{group_name}||[**AdministrationUserAndGroup.update_user_group**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L335)|[**UserGroup**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/administration_user_and_group.py#L84)|None|
DELETE /certificate/{uuid}||[**CertificateManagementDevice.delete_configuration**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L112)||[**DeviceDeletionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L15)|
POST /certificate/generate/csr||[**CertificateManagementDevice.generate_csr**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L116)|[**TargetDevice**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L22)|DataSequence[[**DeviceCsrGenerationResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L28)]|
POST /certificate/save/vedge/list||[**CertificateManagementDevice.change_vedge_list_validity**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L120)|list[[**VedgeListValidityPayload**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L99)]|[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L107)|
POST /certificate/vedge/list?action={action}||[**CertificateManagementDevice.send_to_controllers**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L124)||[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L107)|
POST /certificate/vsmart/list||[**CertificateManagementDevice.send_to_vbond**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L128)||[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L107)|
DELETE /certificate/{uuid}||[**CertificateManagementDevice.delete_configuration**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L113)||[**DeviceDeletionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L15)|
POST /certificate/generate/csr||[**CertificateManagementDevice.generate_csr**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L117)|[**TargetDevice**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L22)|DataSequence[[**DeviceCsrGenerationResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L28)]|
POST /certificate/save/vedge/list||[**CertificateManagementDevice.change_vedge_list_validity**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L121)|list[[**VedgeListValidityPayload**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L100)]|[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L108)|
POST /certificate/vedge/list?action={action}||[**CertificateManagementDevice.send_to_controllers**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L125)||[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L108)|
POST /certificate/vsmart/list||[**CertificateManagementDevice.send_to_vbond**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L129)||[**CertActionResponse**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_device.py#L108)|
GET /setting/configuration/webserver/certificate||[**CertificateManagementVManage.show_info**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_vmanage.py#L46)||[**WebServerCertificateInfo**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/certificate_management_vmanage.py#L11)|
GET /client/server||[**Client.server**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/client.py#L86)||[**ServerInfo**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/client.py#L23)|
GET /client/server/ready||[**Client.server_ready**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/client.py#L90)||[**ServerReady**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/client.py#L80)|
Expand Down
14 changes: 13 additions & 1 deletion catalystwan/abstractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def request(self, method: str, url: str, **kwargs) -> APIEndpointClientResponse:
...

@property
def api_version(self) -> Optional[Version]:
def api_version(self) -> Version:
...

@property
Expand All @@ -56,3 +56,15 @@ def session_type(self) -> Optional[SessionType]:
@property
def validate_responses(self) -> bool:
...


class AuthProtocol(Protocol):
"""
Additional interface for Auth to handle login/logout for multiple auth types by common ManagerSession
"""

def logout(self, client: APIEndpointClient) -> None:
...

def clear(self) -> None:
...
27 changes: 14 additions & 13 deletions catalystwan/apigw_auth.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import logging
from typing import TYPE_CHECKING, Literal, Optional
from typing import Literal, Optional
from urllib.parse import urlparse

from pydantic import BaseModel, Field, PositiveInt
from requests import HTTPError, PreparedRequest, post
from requests.auth import AuthBase
from requests.exceptions import JSONDecodeError

from catalystwan.abstractions import APIEndpointClient, AuthProtocol
from catalystwan.exceptions import CatalystwanException
from catalystwan.response import ManagerResponse

if TYPE_CHECKING:
from catalystwan.session import ManagerSession
from catalystwan.response import auth_response_debug

LoginMode = Literal["machine", "user", "session"]

Expand All @@ -27,17 +25,18 @@ class ApiGwLogin(BaseModel):
token_duration: PositiveInt = Field(default=10, description="in minutes")


class ApiGwAuth(AuthBase):
class ApiGwAuth(AuthBase, AuthProtocol):
"""Attaches ApiGateway Authentication to the given Requests object.
1. Get a bearer token by sending a POST request to the /apigw/login endpoint.
2. Use the token in the Authorization header for subsequent requests.
"""

def __init__(self, login: ApiGwLogin, logger: Optional[logging.Logger] = None):
def __init__(self, login: ApiGwLogin, logger: Optional[logging.Logger] = None, verify=False):
self.login = login
self.token = ""
self.logger = logger or logging.getLogger(__name__)
self.verify = verify

def __call__(self, request: PreparedRequest) -> PreparedRequest:
self.handle_auth(request)
Expand All @@ -51,8 +50,8 @@ def handle_auth(self, request: PreparedRequest) -> None:
def authenticate(self, request: PreparedRequest):
assert request.url is not None
url = urlparse(request.url)
base_url = f"{url.scheme}://{url.netloc}"
self.token = self.get_token(base_url, self.login)
base_url = f"{url.scheme}://{url.netloc}" # noqa: E231
self.token = self.get_token(base_url, self.login, self.logger, self.verify)

def build_digest_header(self, request: PreparedRequest) -> None:
header = {
Expand All @@ -62,14 +61,16 @@ def build_digest_header(self, request: PreparedRequest) -> None:
request.headers.update(header)

@staticmethod
def get_token(base_url: str, apigw_login: ApiGwLogin) -> str:
def get_token(base_url: str, apigw_login: ApiGwLogin, logger: Optional[logging.Logger] = None, verify=False) -> str:
try:
response = post(
url=f"{base_url}/apigw/login",
verify=False,
verify=verify,
json=apigw_login.model_dump(exclude_none=True),
timeout=10,
)
if logger is not None:
logger.debug(auth_response_debug(response))
response.raise_for_status()
token = response.json()["token"]
except JSONDecodeError:
Expand All @@ -86,8 +87,8 @@ def get_token(base_url: str, apigw_login: ApiGwLogin) -> str:
def __str__(self) -> str:
return f"ApiGatewayAuth(mode={self.login.mode})"

def logout(self, session: "ManagerSession") -> Optional[ManagerResponse]:
def logout(self, client: APIEndpointClient) -> None:
return None

def clear_tokens_and_cookies(self) -> None:
def clear(self) -> None:
self.token = ""
9 changes: 9 additions & 0 deletions catalystwan/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import datetime
from email.utils import parsedate_to_datetime
from functools import wraps
from os import environ
from pprint import pformat
from typing import Any, Callable, Dict, Optional, Sequence, Type, TypeVar, Union, cast
from urllib.parse import urlparse
Expand Down Expand Up @@ -104,6 +105,14 @@ def response_history_debug(response: Optional[Response], request: Union[Request,
return "\n".join(response_debugs)


def auth_response_debug(response: Response, title: str = "Auth") -> str:
if environ.get("catalystwan_auth_trace") is not None:
return response_history_debug(response, None)
return ", ".join(
[f"{title}: {r.request.method} {r.request.url} <{r.status_code}>" for r in response.history + [response]]
)


def parse_cookies_to_dict(cookies: str) -> Dict[str, str]:
"""Utility method to parse cookie string into dict"""
result: Dict[str, str] = {}
Expand Down
25 changes: 3 additions & 22 deletions catalystwan/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,6 @@ class ManagerSession(ManagerResponseAdapter, APIEndpointClient):
logger: override default module logger
Attributes:
enable_relogin (bool): defaults to True, in case that session is not properly logged-in, session will try to
relogin and try the same request again
api: APIContainer: container for API methods
endpoints: APIEndpointContainter: container for API endpoints
state: ManagerSessionState: current state of the session can be used to control session flow
Expand All @@ -237,7 +235,6 @@ def __init__(
self._session_type = SessionType.NOT_DEFINED
self.server_name: Optional[str] = None
self.logger = logger or logging.getLogger(__name__)
self.enable_relogin: bool = True
self.response_trace: Callable[
[Optional[Response], Union[Request, PreparedRequest, None]], str
] = response_history_debug
Expand Down Expand Up @@ -314,7 +311,7 @@ def login(self) -> ManagerSession:
ManagerSession: (self)
"""
self.cookies.clear_session_cookies()
self._auth.clear_tokens_and_cookies()
self._auth.clear()
self.auth = self._auth
if self.subdomain:
tenant_id = self.get_tenant_id()
Expand Down Expand Up @@ -346,14 +343,8 @@ def login(self) -> ManagerSession:
self.logger.info(
f"Logged to vManage({self.platform_version}) as {self.auth}. The session type is {self.session_type}"
)
self._set_jsessionid(self.auth)
return self

def _set_jsessionid(self, auth: Union[vManageAuth, ApiGwAuth]) -> None:
if isinstance(auth, vManageAuth):
if jsessionid := auth.jsessionid:
self.cookies.set("JSESSIONID", jsessionid)

def wait_server_ready(self, timeout: int, poll_period: int = 10) -> None:
"""Waits until server is ready for API requests with given timeout in seconds"""

Expand Down Expand Up @@ -424,16 +415,6 @@ def request(self, method, url, *args, **kwargs) -> ManagerResponse:
self.logger.debug(exception)
raise ManagerRequestException(*exception.args, request=exception.request, response=exception.response)

if self.enable_relogin and response.jsessionid_expired and self.state == ManagerSessionState.OPERATIVE:
self.logger.warning("Logging to session. Reason: expired JSESSIONID detected in response headers")
self.state = ManagerSessionState.LOGIN
return self.request(method, url, *args, **_kwargs)

if self.enable_relogin and response.api_gw_unauthorized and self.state == ManagerSessionState.OPERATIVE:
self.logger.warning("Logging to API GW session. Reason: unauthorized detected in response headers")
self.state = ManagerSessionState.LOGIN
return self.request(method, url, *args, **_kwargs)

if response.request.url and "passwordReset.html" in response.request.url:
raise DefaultPasswordError("Password must be changed to use this session.")

Expand Down Expand Up @@ -511,8 +492,8 @@ def get_virtual_session_id(self, tenant_id: str) -> str:
response = self.post(url_path)
return response.json()["VSessionId"]

def logout(self) -> Optional[ManagerResponse]:
return self._auth.logout(self)
def logout(self) -> None:
self._auth.logout(self)

def close(self) -> None:
"""Closes the ManagerSession.
Expand Down
Loading

0 comments on commit 1949b59

Please sign in to comment.