diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a98ee15c..ef332705 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -19,3 +19,10 @@ jobs: with: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies + run: | + pip install poetry + poetry install + - name: Run tests + run: | + task test diff --git a/gallagher/cc/__init__.py b/gallagher/cc/__init__.py index 4a9a0543..7be38100 100644 --- a/gallagher/cc/__init__.py +++ b/gallagher/cc/__init__.py @@ -23,11 +23,10 @@ from typing import Optional from ..const import URL -from ..dto.discover import DiscoveryResponse -from .core import ( - APIEndpoint, - EndpointConfig +from ..dto.discover import ( + DiscoveryResponse, + FeaturesDetail, ) # Follow the instructions in the Gallagher documentation @@ -45,27 +44,19 @@ # should you wish to use a proxy, set this to the proxy URL proxy: Optional[str] = None - -class APIFeatureDiscovery( - APIEndpoint -): - """ The Command Centre root API endpoint - - Much of Gallagher's API documentation suggests that we don't - hard code the URL, but instead use the discovery endpoint by - calling the root endpoint. - - This should be a singleton which is instantiated upon initialisation - and then used across the other endpoints. - - For example features.events.events.href is the endpoint for the events - where as features.events.events.updates is the endpoint for getting - updates to the changes to events. - - This differs per endpoint that we work with. - - """ - __config__ = EndpointConfig( - endpoint="", # The root endpoint is the discovery endpoint - dto_list=DiscoveryResponse, - ) +# Discover response object, each endpoint will reference +# one of the instance variable Href property to get the +# path to the endpoint. +# +# Gallagher recommends that the endpoints not be hardcoded +# into the client and instead be discovered at runtime. +# +# Note that if a feature has not been licensed by a client +# then the path will be set to None, if the client attempts +# to access the endpoint then the library will throw an exception +# +# This value is memoized and should perform +CAPABILITIES = DiscoveryResponse( + version="0.0.0", # Indicates that it's not been discovered + features=FeaturesDetail() +) diff --git a/gallagher/cc/alarms/items.py b/gallagher/cc/alarms/items.py index 6b0cc830..fc1655c4 100644 --- a/gallagher/cc/alarms/items.py +++ b/gallagher/cc/alarms/items.py @@ -1,8 +1,11 @@ """ """ + +from gallagher.cc import CAPABILITIES + from ..core import ( APIEndpoint, - EndpointConfig + EndpointConfig, ) from ...dto.items import ( @@ -18,7 +21,7 @@ class ItemsTypes(APIEndpoint): """ __config__ = EndpointConfig( - endpoint=cls.paths.features.alarms.alarms.href, + endpoint="items/types", dto_list=ItemTypesResponse, ) diff --git a/gallagher/cc/core.py b/gallagher/cc/core.py index 93a5de64..afc2a8cc 100644 --- a/gallagher/cc/core.py +++ b/gallagher/cc/core.py @@ -16,9 +16,12 @@ import httpx +from gallagher.exception import ( + UnlicensedFeatureException +) + from ..dto.discover import ( DiscoveryResponse, - FeaturesDetail, ) @@ -82,6 +85,18 @@ class EndpointConfig: sort: Optional[str] = "id" # Can be set to id or -id # fields: list[str] = [] # Optional list of fields + @classmethod + def validate_endpoint(cls): + """ Check to see if the feature is licensed and available + + Gallagher REST API is licensed per feature, if a feature is not + the endpoint is set to none and we should throw an exception + """ + if not cls.endpoint: + raise UnlicensedFeatureException( + "Endpoint not defined" + ) + class APIEndpoint(): """ Base class for all API objects @@ -94,42 +109,42 @@ class APIEndpoint(): """ - # Discover response object, each endpoint will reference - # one of the instance variable Href property to get the - # path to the endpoint. - # - # Gallagher recommends that the endpdoints not be hardcoded - # into the client and instead be discovered at runtime. - # - # Note that if a feature has not been licensed by a client - # then the path will be set to None, if the client attempts - # to access the endpoint then the library will throw an exception - # - # This value is memoized and should perform - paths = DiscoveryResponse( - version="0.0.0", # Indicates that it's not been discovered - features=FeaturesDetail() - ) - # This must be overridden by each child class that inherits # from this base class. __config__ = None @classmethod def _discover(cls): - """ Discovers the endpoints for the given API + """ The Command Centre root API endpoint + + Much of Gallagher's API documentation suggests that we don't + hard code the URL, but instead use the discovery endpoint by + calling the root endpoint. + + This should be a singleton which is instantiated upon initialisation + and then used across the other endpoints. + + For example features.events.events.href is the endpoint for the events + where as features.events.events.updates is the endpoint for getting + updates to the changes to events. - This is a memoized function that will only be called once - on the first operation that is accessed, subsequent calls - will return the cached result. + This differs per endpoint that we work with. """ + # Auto-discovery of the API endpoints, this will # be called as part of the bootstrapping process - from . import ( - APIFeatureDiscovery + from . import api_base + response = httpx.get( + api_base, + headers=get_authorization_headers(), ) - cls.paths = APIFeatureDiscovery.list() + parsed_obj = DiscoveryResponse.model_validate( + response.json() + ) + + from . import CAPABILITIES + CAPABILITIES = parsed_obj @classmethod def list(cls, skip=0): @@ -138,6 +153,8 @@ def list(cls, skip=0): Most resources can be searched which is exposed by this method. Resources also allow pagination which can be controlled by the skip """ + cls._discover() + from . import api_base response = httpx.get( f'{api_base}{cls.__config__.endpoint}', @@ -158,6 +175,8 @@ def retrieve(cls, id): Each resource also provides a href and pagination for children. """ + cls._discover() + from . import api_base response = httpx.get( f'{api_base}{cls.__config__.endpoint}/{id}', diff --git a/gallagher/const.py b/gallagher/const.py index 550b66b0..f45764dd 100644 --- a/gallagher/const.py +++ b/gallagher/const.py @@ -1,11 +1,29 @@ """ Constants for the Gallagher Cloud ecosystem + +Gallagher publishes a set of networking constants for their cloud gateways +these should be updated as the documentation is updated. """ + + class URL: + """ DNS names for the cloud gateways + + These are published by Gallagher on Github + https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html + """ + + CLOUD_GATEWAY_AU: str = \ + "https://commandcentre-api-au.security.gallagher.cloud/api/" + CLOUD_GATEWAY_US: str = \ + "https://commandcentre-api-us.security.gallagher.cloud/api/" - CLOUD_GATEWAY_AU: str = "https://commandcentre-api-au.security.gallagher.cloud/api/" - CLOUD_GATEWAY_US: str = "https://commandcentre-api-us.security.gallagher.cloud/api/" class IP_ADDR: + """ IP addresses for the cloud gateways + + These are published by Gallagher on Github + https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html + """ CLOUD_GATEWAY_AU = [ "3.106.1.6", diff --git a/gallagher/dto/utils.py b/gallagher/dto/utils.py index 811e47d0..be162a15 100644 --- a/gallagher/dto/utils.py +++ b/gallagher/dto/utils.py @@ -3,6 +3,10 @@ """ +from typing import ( + Optional +) +from datetime import datetime from pydantic import ( BaseModel, @@ -37,6 +41,13 @@ class AppBaseModel(BaseModel): alias_generator=to_lower_camel, ) + # Set to the last time each response was retrieved + # If it's set to None then the response was either created + # by the API client or it wasn't retrieved from the server + # + # This is generally used for caching + good_known_since: Optional[datetime] = None + class IdentityMixin(BaseModel): """ Identifier diff --git a/gallagher/exception.py b/gallagher/exception.py index 56292993..84efab4d 100644 --- a/gallagher/exception.py +++ b/gallagher/exception.py @@ -8,7 +8,7 @@ """ -class FeatureNotLicensedException(Exception): +class UnlicensedFeatureException(Exception): """ Raised when a feature is not licensed This exception is raised when the client attempts to access