diff --git a/api/account/api.py b/api/account/api.py index 1cd093ef8..5e880ae5b 100644 --- a/api/account/api.py +++ b/api/account/api.py @@ -26,17 +26,15 @@ Customization, Nonce, ) +from internal.api_key import internal_api_key from scorer_weighted.models import ( BinaryWeightedScorer, WeightedScorer, get_default_weights, ) -from trusta_labs.api import CgrantsApiKey from .deduplication import Rules -secret_key = CgrantsApiKey() - log = logging.getLogger(__name__) api = NinjaExtraAPI(urls_namespace="account") @@ -597,8 +595,13 @@ def update_community_scorers(request, community_id, payload: ScorerId): return {"ok": True} +# Public version of endpoint. Also available on the internal API @api.get("/customization/credential/{provider_id}", auth=None) def get_credential_definition(request, provider_id: str): + return handle_get_credential_definition(provider_id) + + +def handle_get_credential_definition(provider_id: str): decoded_provider_id = provider_id.replace("%23", "#") return { "ruleset": get_object_or_404( @@ -676,11 +679,16 @@ def get_account_customization(request, dashboard_path: str): raise APIException("Customization not found", status.HTTP_404_NOT_FOUND) -@api.get("/allow-list/{str:list}/{str:address}", auth=secret_key) +# TODO 3280 Remove this endpoint +@api.get("/allow-list/{str:list}/{str:address}", auth=internal_api_key) def check_on_allow_list(request, list: str, address: str): """ Check if an address is on the allow list for a specific round """ + return handle_check_allow_list(list, address) + + +def handle_check_allow_list(list: str, address: str): try: is_member = AddressListMember.objects.filter( list__name=list, address=address diff --git a/api/ceramic_cache/api/v1.py b/api/ceramic_cache/api/v1.py index b707f500a..088d814b2 100644 --- a/api/ceramic_cache/api/v1.py +++ b/api/ceramic_cache/api/v1.py @@ -28,6 +28,7 @@ import tos.schema from account.models import Account, Community, Nonce from ceramic_cache.utils import get_utc_time +from internal.api_key import internal_api_key from registry.api.utils import ( is_valid_address, ) @@ -43,9 +44,8 @@ NotFoundApiException, ) from registry.models import Score -from stake.api import handle_get_gtc_stake -from stake.schema import GetSchemaResponse -from trusta_labs.api import CgrantsApiKey +from stake.api import get_gtc_stake_for_address +from stake.schema import StakeResponse from ..exceptions import ( InternalServerException, @@ -71,8 +71,6 @@ router = Router() -secret_key = CgrantsApiKey() - def get_address_from_did(did: str): return did.split(":")[-1] @@ -560,10 +558,11 @@ def calc_score( return get_detailed_score_response_for_address(address, payload.alternate_scorer_id) +# TODO 3280 Remove this endpoint @router.get( "/score/{int:scorer_id}/{str:address}", response=DetailedScoreResponse, - auth=secret_key, + auth=internal_api_key, ) def calc_score_community( request, @@ -685,15 +684,15 @@ def get_detailed_score_response_for_address( @router.get( "/stake/gtc", response={ - 200: GetSchemaResponse, + 200: StakeResponse, 400: ErrorMessageResponse, }, auth=JWTDidAuth(), ) -def get_staked_gtc(request) -> GetSchemaResponse: +def get_staked_gtc(request) -> StakeResponse: address = get_address_from_did(request.did) - get_stake_response = handle_get_gtc_stake(address) - response = GetSchemaResponse(items=get_stake_response) + get_stake_response = get_gtc_stake_for_address(address) + response = StakeResponse(items=get_stake_response) return response diff --git a/api/cgrants/api.py b/api/cgrants/api.py index 5088c7e41..88316a159 100644 --- a/api/cgrants/api.py +++ b/api/cgrants/api.py @@ -6,27 +6,22 @@ from enum import Enum -from django.conf import settings from django.db.models import Count, Q, Sum from django.http import JsonResponse -from ninja.security import APIKeyHeader from ninja_extra import NinjaExtraAPI from ninja_schema import Schema -from ninja_schema.orm.utils.converter import Decimal from pydantic import Field import api_logging as logging +from internal.api_key import internal_api_key from registry.api.v1 import is_valid_address from registry.exceptions import InvalidAddressException from .models import ( - Contribution, - Grant, GrantContributionIndex, ProtocolContributions, RoundMapping, SquelchedAccounts, - SquelchProfile, ) logger = logging.getLogger(__name__) @@ -35,17 +30,6 @@ api = NinjaExtraAPI(urls_namespace="cgrants") -class CgrantsApiKey(APIKeyHeader): - param_name = "AUTHORIZATION" - - def authenticate(self, request, key): - if key == settings.CGRANTS_API_TOKEN: - return key - - -cg_api_key = CgrantsApiKey() - - class ContributorStatistics(Schema): num_grants_contribute_to: int = Field() num_rounds_contribute_to: int = Field() @@ -127,22 +111,17 @@ def _get_contributor_statistics_for_protocol(address: str) -> dict: } +# TODO 3280 Remove this endpoint @api.get( "/contributor_statistics", response=ContributorStatistics, - auth=cg_api_key, + auth=internal_api_key, ) -def contributor_statistics( - request, address: str | None = None, github_id: str | None = None -): - if not address: - return JsonResponse( - { - "error": "Bad request, 'address' is missing or invalid. A valid address is required." - }, - status=400, - ) +def contributor_statistics(request, address: str): + return handle_get_contributor_statistics(address) + +def handle_get_contributor_statistics(address: str): if not is_valid_address(address): raise InvalidAddressException() @@ -165,117 +144,3 @@ def contributor_statistics( } return JsonResponse(combined_contributions) - - -@api.get( - "/allo/contributor_statistics", - response=ContributorStatistics, - auth=cg_api_key, -) -def allo_contributor_statistics(request, address: str | None = None): - if not address: - return JsonResponse( - {"error": "Bad request, 'address' parameter is missing or invalid"}, - status=400, - ) - - if address: - address = address.lower() - - response_for_protocol = _get_contributor_statistics_for_protocol(address) - - return JsonResponse(response_for_protocol) - - -def _get_grantee_statistics(identifier, identifier_type): - if identifier_type == IdentifierType.HANDLE: - grant_identifier_query = Q(admin_profile__handle=identifier) - - contribution_identifier_query = Q( - subscription__grant__admin_profile__handle=identifier - ) & ~Q(subscription__contributor_profile__handle=identifier) - else: - grant_identifier_query = Q(admin_profile__github_id=identifier) - - contribution_identifier_query = Q( - subscription__grant__admin_profile__github_id=identifier - ) & ~Q(subscription__contributor_profile__github_id=identifier) - - # Get number of owned grants - num_owned_grants = Grant.objects.filter( - grant_identifier_query, hidden=False, active=True, is_clr_eligible=True - ).count() - - # Get the total amount of contrinutors for one users grants that where not squelched and are not the owner himself - all_squelched = SquelchProfile.objects.filter(active=True).values_list( - "profile_id", flat=True - ) - - num_grant_contributors = ( - Contribution.objects.filter( - contribution_identifier_query, - success=True, - subscription__is_mainnet=True, - subscription__grant__hidden=False, - subscription__grant__active=True, - subscription__grant__is_clr_eligible=True, - ) - .exclude(subscription__contributor_profile_id__in=all_squelched) - .order_by("subscription__contributor_profile_id") - .values("subscription__contributor_profile_id") - .distinct() - .count() - ) - - # Get the total amount of contributions received by the owned grants (excluding the contributions made by the owner) - total_contribution_amount = Contribution.objects.filter( - contribution_identifier_query, - success=True, - subscription__is_mainnet=True, - subscription__grant__hidden=False, - subscription__grant__active=True, - subscription__grant__is_clr_eligible=True, - ).aggregate(Sum("amount_per_period_usdt"))["amount_per_period_usdt__sum"] - total_contribution_amount = ( - total_contribution_amount if total_contribution_amount is not None else 0 - ) - - # [IAM] As an IAM server, I want to issue stamps for grant owners whose project have tagged matching-eligibel in an eco-system and/or cause round - num_grants_in_eco_and_cause_rounds = Grant.objects.filter( - grant_identifier_query, - hidden=False, - active=True, - is_clr_eligible=True, - clr_calculations__grantclr__type__in=["ecosystem", "cause"], - ).count() - - return JsonResponse( - { - "num_owned_grants": num_owned_grants, - "num_grant_contributors": num_grant_contributors, - "num_grants_in_eco_and_cause_rounds": num_grants_in_eco_and_cause_rounds, - "total_contribution_amount": total_contribution_amount, - } - ) - - -@api.get( - "/grantee_statistics", - response=GranteeStatistics, - auth=cg_api_key, -) -def grantee_statistics( - request, handle: str | None = None, github_id: str | None = None -): - if not handle and not github_id: - return JsonResponse( - { - "error": "Bad request, 'handle' and 'github_id' parameter is missing or invalid. Either one is required." - }, - status=400, - ) - - if handle: - return _get_grantee_statistics(handle, IdentifierType.HANDLE) - else: - return _get_grantee_statistics(github_id, IdentifierType.GITHUB_ID) diff --git a/api/cgrants/test/test_cgrants_combined_contributions_api.py b/api/cgrants/test/test_cgrants_combined_contributions_api.py index 536547577..0db0ae332 100644 --- a/api/cgrants/test/test_cgrants_combined_contributions_api.py +++ b/api/cgrants/test/test_cgrants_combined_contributions_api.py @@ -1,6 +1,11 @@ -""" Test file for cgrants contribution API """ +"""Test file for cgrants contribution API""" import pytest +from django.conf import settings +from django.test import Client +from django.urls import reverse +from numpy import add + from account.test.conftest import scorer_account, scorer_user from cgrants.api import _get_contributor_statistics_for_protocol from cgrants.models import ( @@ -12,10 +17,6 @@ from cgrants.test.test_add_address_to_contribution_index import ( generate_bulk_cgrant_data, ) -from django.conf import settings -from django.test import Client -from django.urls import reverse -from numpy import add pytestmark = pytest.mark.django_db @@ -41,7 +42,7 @@ def test_combined_contributor_statistics( contrib.save() response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": scorer_account.address}, **headers, ) @@ -53,7 +54,7 @@ def test_combined_contributor_statistics( def test_combined_contributor_statistics_no_contributions(self): response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"}, **headers, ) @@ -66,7 +67,7 @@ def test_combined_contributor_statistics_no_contributions(self): def test_invalid_address(self): response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": "0x9965507D1a55bcC2695C58ba16FB37d819BAAAAA"}, **headers, ) @@ -76,7 +77,7 @@ def test_invalid_address(self): def test_combined_contributor_invalid_token(self): response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"}, **{"HTTP_AUTHORIZATION": "invalidtoken"}, ) @@ -85,16 +86,12 @@ def test_combined_contributor_invalid_token(self): def test_missing_address(self): response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {}, **headers, ) - assert response.status_code == 400 - - assert response.json() == { - "error": "Bad request, 'address' is missing or invalid. A valid address is required." - } + assert response.status_code == 422 def test_contribution_below_threshold(self, protocol_contributions, scorer_account): for contrib in ProtocolContributions.objects.filter( @@ -104,7 +101,7 @@ def test_contribution_below_threshold(self, protocol_contributions, scorer_accou contrib.save() response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": scorer_account.address}, **headers, ) @@ -117,7 +114,7 @@ def test_contribution_below_threshold(self, protocol_contributions, scorer_accou def test_only_protocol_contributions(self, protocol_contributions, scorer_account): response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": scorer_account.address}, **headers, ) @@ -138,7 +135,7 @@ def test_depegged_protocol_contribution(self, scorer_account): ) response = client.get( - reverse("cgrants:contributor_statistics"), + reverse("internal:cgrants_contributor_statistics"), {"address": scorer_account.address}, **headers, ) diff --git a/api/embed/api.py b/api/embed/api.py index 058ebf610..7b4ee171e 100644 --- a/api/embed/api.py +++ b/api/embed/api.py @@ -12,6 +12,7 @@ ) from ceramic_cache.api.v1 import handle_add_stamps_only, handle_get_scorer_weights from ceramic_cache.models import CeramicCache +from internal.api_key import internal_api_key from registry.api.schema import ( ErrorMessageResponse, ) @@ -22,7 +23,6 @@ from registry.exceptions import ( InvalidAddressException, ) -from trusta_labs.api import CgrantsApiKey from v2.api import handle_scoring api_router = Router() @@ -39,15 +39,13 @@ log = logging.getLogger(__name__) -internal_api_key = CgrantsApiKey() - - class AddStampsPayload(Schema): scorer_id: int stamps: List[Any] -# TODO: check authentication for these endpoints. Ideally the embed service requires an internal API key, but the lambda one is only exposed on internal LB +# Endpoint for this defined in internal module +# TODO 3280 Remove this endpoint @api_router.post( "/stamps/{str:address}", auth=internal_api_key, @@ -101,6 +99,7 @@ class AccountAPIKeySchema(Schema): rate_limit: str +# TODO 3280 Remove this endpoint @api_router.get( "/validate-api-key", # Here we want to authenticate the partners key, hence this ApiKey auth class @@ -114,6 +113,10 @@ class AccountAPIKeySchema(Schema): summary="Add Stamps and get the new score", ) def validate_api_key(request) -> AccountAPIKeySchema: + return handle_validate_embed_api_key(request) + + +def handle_validate_embed_api_key(request) -> AccountAPIKeySchema: """ Return the capabilities allocated to this API key. This API is intended to be used in the embed service in the passport repo @@ -121,6 +124,7 @@ def validate_api_key(request) -> AccountAPIKeySchema: return AccountAPIKeySchema.from_orm(request.api_key) +# TODO 3280 Remove this endpoint @api_router.get("/weights", response=Dict[str, float]) def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, float]: """ diff --git a/api/embed/test/test_api_stamps.py b/api/embed/test/test_api_stamps.py index 3837ca7ad..e335833d3 100644 --- a/api/embed/test/test_api_stamps.py +++ b/api/embed/test/test_api_stamps.py @@ -124,7 +124,7 @@ def test_internal_api_key_is_required(self): """Test that the internal API key is required for the stamps request""" stamps_response = self.client.post( - f"/embed/stamps/{mock_addresse}", + f"/internal/embed/stamps/{mock_addresse}", json.dumps({"scorer_id": self.community.id, "stamps": mock_stamps}), content_type="application/json", **{"HTTP_AUTHORIZATION": "BAD_API_KEY"}, @@ -136,7 +136,7 @@ def test_submit_valid_stamps(self, _test_submit_valid_stamps): """Test that the newly submitted stamps are correctly saved and scored""" stamps_response = self.client.post( - f"/embed/stamps/{mock_addresse}", + f"/internal/embed/stamps/{mock_addresse}", json.dumps({"scorer_id": self.community.id, "stamps": mock_stamps}), content_type="application/json", **{"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN}, @@ -191,7 +191,7 @@ def test_submit_additional_valid_stamps(self, _test_submit_valid_stamps): # Create the rest of the stamps via the POST request stamps_response = self.client.post( - f"/embed/stamps/{mock_addresse}", + f"/internal/embed/stamps/{mock_addresse}", json.dumps({"scorer_id": self.community.id, "stamps": mock_stamps[1:]}), content_type="application/json", **{"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN}, @@ -250,7 +250,7 @@ def test_storing_stamps_and_score(self, _test_submit_valid_stamps): # Create stamps with the same provider using the POST request stamps_response = self.client.post( - f"/embed/stamps/{mock_addresse}", + f"/internal/embed/stamps/{mock_addresse}", json.dumps({"scorer_id": self.community.id, "stamps": mock_stamps}), content_type="application/json", **{"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN}, @@ -301,7 +301,7 @@ def test_submitted_stamps_are_saved_properly(self, _test_submit_valid_stamps): # Create the rest of the stamps via the POST request stamps_response = self.client.post( - f"/embed/stamps/{mock_addresse}", + f"/internal/embed/stamps/{mock_addresse}", json.dumps({"scorer_id": self.community.id, "stamps": mock_stamps}), content_type="application/json", **{"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN}, diff --git a/api/embed/test/test_api_validate_api_key.py b/api/embed/test/test_api_validate_api_key.py index 2c8c9a3d2..e011c741f 100644 --- a/api/embed/test/test_api_validate_api_key.py +++ b/api/embed/test/test_api_validate_api_key.py @@ -26,7 +26,7 @@ def test_rate_limit_bad_api_key(self): """Test that the rate limit API returns error when an invalid API key is provided""" rate_limit_response = self.client.get( - "/embed/validate-api-key", + "/internal/embed/validate-api-key", **{"HTTP_X-API-KEY": f"api_id.some_api_key"}, ) assert rate_limit_response.status_code == 401 @@ -42,7 +42,7 @@ def test_rate_limit_success(self): ) rate_limit_response = self.client.get( - "/embed/validate-api-key", + "/internal/embed/validate-api-key", **{"HTTP_X-API-KEY": api_key}, ) diff --git a/api/embed/test/test_api_weights.py b/api/embed/test/test_api_weights.py index 15c1d402f..2ef1f27ec 100644 --- a/api/embed/test/test_api_weights.py +++ b/api/embed/test/test_api_weights.py @@ -3,27 +3,27 @@ from django.test import TestCase from ninja.testing import TestClient -from embed.api import api_router +from internal.api import api_router as internal_api_router class TestGetEmbedWeights(TestCase): def setUp(self): - self.client = TestClient(api_router) + self.client = TestClient(internal_api_router) - @patch("embed.api.handle_get_scorer_weights") + @patch("internal.api.handle_get_scorer_weights") def test_get_embed_weights_no_community(self, mock_handle_get_scorer_weights): mock_handle_get_scorer_weights.return_value = {"weight1": 0.5, "weight2": 1.0} - response = self.client.get("/weights") + response = self.client.get("/embed/weights") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"weight1": 0.5, "weight2": 1.0}) mock_handle_get_scorer_weights.assert_called_once_with(None) - @patch("embed.api.handle_get_scorer_weights") + @patch("internal.api.handle_get_scorer_weights") def test_get_embed_weights_with_community(self, mock_handle_get_scorer_weights): mock_handle_get_scorer_weights.return_value = {"weightA": 0.7, "weightB": 0.3} - response = self.client.get("/weights?community_id=community123") + response = self.client.get("/embed/weights?community_id=community123") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"weightA": 0.7, "weightB": 0.3}) mock_handle_get_scorer_weights.assert_called_once_with("community123") diff --git a/api/internal/api.py b/api/internal/api.py index 89c7ec7b4..d5cd13d81 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -1,16 +1,30 @@ -from typing import List +from typing import Dict, List, Optional -from django.conf import settings from ninja import Router from ninja_extra import NinjaExtraAPI -from ninja_extra.exceptions import APIException import api_logging as logging -from ceramic_cache.exceptions import InternalServerException, TooManyStampsException -from ceramic_cache.models import Ban, Revocation -from trusta_labs.api import CgrantsApiKey - -from .exceptions import InvalidBanQueryException +from account.api import handle_check_allow_list, handle_get_credential_definition +from ceramic_cache.api.schema import GetStampsWithV2ScoreResponse +from ceramic_cache.api.v1 import handle_get_scorer_weights, handle_get_ui_score +from cgrants.api import ( + ContributorStatistics, + handle_get_contributor_statistics, +) +from embed.api import ( + AccountAPIKeySchema, + AddStampsPayload, + handle_embed_add_stamps, + handle_validate_embed_api_key, +) +from registry.api.schema import DetailedScoreResponse, GtcEventsResponse +from registry.api.utils import ApiKey +from registry.api.v1 import handle_get_gtc_stake_legacy +from stake.api import handle_get_gtc_stake +from stake.schema import ErrorMessageResponse, StakeResponse + +from .api_key import internal_api_key +from .bans_revocations import handle_check_bans, handle_check_revocations from .schema import ( CheckBanResult, Credential, @@ -32,58 +46,9 @@ log = logging.getLogger(__name__) -internal_api_key = CgrantsApiKey() - - @api_router.post("/check-bans", response=List[CheckBanResult], auth=internal_api_key) def check_bans(request, payload: List[Credential]) -> List[CheckBanResult]: - """ - Check for active bans matching the given address and/or hashes. - Returns list of relevant active bans. - """ - unique_ids = list(set([c.credentialSubject.id for c in payload])) - - if len(unique_ids) < 1: - raise InvalidBanQueryException("Must provide valid credential(s)") - - if len(unique_ids) > 1: - raise InvalidBanQueryException( - "All credentials must be issued to the same address" - ) - - address = unique_ids[0].split(":")[-1] - - hashes = list( - set([c.credentialSubject.hash for c in payload if c.credentialSubject.hash]) - ) - - try: - bans = Ban.get_bans(address=address, hashes=hashes) - - credential_ban_results = [ - Ban.check_bans_for( - bans, address, c.credentialSubject.hash, c.credentialSubject.provider - ) - for c in payload - ] - - return [ - CheckBanResult( - hash=c.credentialSubject.hash, - is_banned=is_banned, - ban_type=ban_type, - end_time=ban.end_time if ban else None, - reason=ban.reason if ban else None, - ) - for c, (is_banned, ban_type, ban) in zip(payload, credential_ban_results) - ] - - except APIException: - # re-raise API exceptions - raise - except Exception as e: - log.error("Failed to check bans", exc_info=True) - raise InternalServerException("Failed to check bans") from e + return handle_check_bans(payload) @api_router.post( @@ -92,32 +57,111 @@ def check_bans(request, payload: List[Credential]) -> List[CheckBanResult]: def check_revocations( request, payload: RevocationCheckPayload ) -> List[RevocationCheckResponse]: - """ - Check if stamps with given proof values have been revoked. - Returns revocation status for each proof value. - """ - if len(payload.proof_values) > settings.MAX_BULK_CACHE_SIZE: - raise TooManyStampsException() - - try: - # Query for revocations matching any of the proof values - revoked_proof_values = set( - Revocation.objects.filter(proof_value__in=payload.proof_values).values_list( - "proof_value", flat=True - ) - ) - - # Return status for each requested proof value - return [ - RevocationCheckResponse( - proof_value=proof_value, is_revoked=proof_value in revoked_proof_values - ) - for proof_value in payload.proof_values - ] - - except APIException: - # re-raise API exceptions - raise - except Exception as e: - log.error("Failed to check revocations", exc_info=True) - raise InternalServerException("Failed to check revocations") from e + return handle_check_revocations(payload) + + +@api_router.get( + "/stake/gtc/{str:address}", + auth=internal_api_key, + response={ + 200: StakeResponse, + 400: ErrorMessageResponse, + }, + summary="Retrieve GTC stake amounts for the GTC Staking stamp", + description="Get self and community GTC stakes for an address", +) +def get_gtc_stake(request, address: str) -> StakeResponse: + return handle_get_gtc_stake(address) + + +@api_router.get( + "/stake/legacy-gtc/{str:address}/{int:round_id}", + auth=internal_api_key, + response=GtcEventsResponse, + summary="Retrieve GTC stake amounts from legacy staking contract", + description="Get self and community GTC staking amounts based on address and round ID", +) +def get_gtc_stake_legacy(request, address: str, round_id: int) -> GtcEventsResponse: + return handle_get_gtc_stake_legacy(address, round_id) + + +@api_router.get( + "/cgrants/contributor_statistics", + response=ContributorStatistics, + auth=internal_api_key, +) +def cgrants_contributor_statistics(request, address: str): + return handle_get_contributor_statistics(address) + + +@api_router.get( + "/score/{int:scorer_id}/{str:address}", + response=DetailedScoreResponse, + auth=internal_api_key, +) +def calc_score_community( + request, + scorer_id: int, + address: str, +) -> DetailedScoreResponse: + return handle_get_ui_score(address, scorer_id) + + +# TODO: check authentication for these endpoints. Ideally the embed service requires an internal +# API key, but the lambda for this one is only exposed on internal LB +@api_router.post( + "/embed/stamps/{str:address}", + auth=internal_api_key, + response={ + 200: GetStampsWithV2ScoreResponse, + 401: ErrorMessageResponse, + 400: ErrorMessageResponse, + 404: ErrorMessageResponse, + }, + summary="Add Stamps and get the new score", +) +def add_stamps( + request, address: str, payload: AddStampsPayload +) -> GetStampsWithV2ScoreResponse: + return handle_embed_add_stamps(address, payload) + + +@api_router.get( + "/embed/weights", + response={ + 200: Dict[str, float], + }, + summary="Retrieve the embed weights", +) +def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, float]: + return handle_get_scorer_weights(community_id) + + +@api_router.get( + "/embed/validate-api-key", + # Here we want to authenticate the partners key, hence this ApiKey auth class + auth=ApiKey(), + response={ + 200: AccountAPIKeySchema, + 401: ErrorMessageResponse, + 400: ErrorMessageResponse, + 404: ErrorMessageResponse, + }, + summary="Add Stamps and get the new score", +) +def validate_api_key(request) -> AccountAPIKeySchema: + return handle_validate_embed_api_key(request) + + +@api.get( + "/allow-list/{str:list}/{str:address}", + auth=internal_api_key, + summary="Check if an address is on the allow list for a specific round", +) +def check_on_allow_list(request, list: str, address: str): + return handle_check_allow_list(list, address) + + +@api.get("/customization/credential/{provider_id}", auth=internal_api_key) +def get_credential_definition(request, provider_id: str): + return handle_get_credential_definition(provider_id) diff --git a/api/internal/api_key.py b/api/internal/api_key.py new file mode 100644 index 000000000..18beb167c --- /dev/null +++ b/api/internal/api_key.py @@ -0,0 +1,13 @@ +from django.conf import settings +from ninja.security import APIKeyHeader + + +class InternalApiKey(APIKeyHeader): + param_name = "AUTHORIZATION" + + def authenticate(self, request, key): + if key == settings.CGRANTS_API_TOKEN: + return key + + +internal_api_key = InternalApiKey() diff --git a/api/internal/bans_revocations.py b/api/internal/bans_revocations.py new file mode 100644 index 000000000..1063d0c46 --- /dev/null +++ b/api/internal/bans_revocations.py @@ -0,0 +1,102 @@ +from typing import List + +from django.conf import settings +from ninja_extra.exceptions import APIException + +import api_logging as logging +from ceramic_cache.exceptions import InternalServerException, TooManyStampsException +from ceramic_cache.models import Ban, Revocation + +from .exceptions import InvalidBanQueryException +from .schema import ( + CheckBanResult, + Credential, + RevocationCheckPayload, + RevocationCheckResponse, +) + +log = logging.getLogger(__name__) + + +def handle_check_bans(payload: List[Credential]) -> List[CheckBanResult]: + """ + Check for active bans matching the given address and/or hashes. + Returns list of relevant active bans. + """ + unique_ids = list(set([c.credentialSubject.id for c in payload])) + + if len(unique_ids) < 1: + raise InvalidBanQueryException("Must provide valid credential(s)") + + if len(unique_ids) > 1: + raise InvalidBanQueryException( + "All credentials must be issued to the same address" + ) + + address = unique_ids[0].split(":")[-1] + + hashes = list( + set([c.credentialSubject.hash for c in payload if c.credentialSubject.hash]) + ) + + try: + bans = Ban.get_bans(address=address, hashes=hashes) + + credential_ban_results = [ + Ban.check_bans_for( + bans, address, c.credentialSubject.hash, c.credentialSubject.provider + ) + for c in payload + ] + + return [ + CheckBanResult( + hash=c.credentialSubject.hash, + is_banned=is_banned, + ban_type=ban_type, + end_time=ban.end_time if ban else None, + reason=ban.reason if ban else None, + ) + for c, (is_banned, ban_type, ban) in zip(payload, credential_ban_results) + ] + + except APIException: + # re-raise API exceptions + raise + except Exception as e: + log.error("Failed to check bans", exc_info=True) + raise InternalServerException("Failed to check bans") from e + + +def handle_check_revocations( + payload: RevocationCheckPayload, +) -> List[RevocationCheckResponse]: + """ + Check if stamps with given proof values have been revoked. + Returns revocation status for each proof value. + """ + if len(payload.proof_values) > settings.MAX_BULK_CACHE_SIZE: + raise TooManyStampsException() + + try: + # Query for revocations matching any of the proof values + revoked_proof_values = set( + Revocation.objects.filter(proof_value__in=payload.proof_values).values_list( + "proof_value", flat=True + ) + ) + + # Return status for each requested proof value + return [ + RevocationCheckResponse( + proof_value=proof_value, is_revoked=proof_value in revoked_proof_values + ) + for proof_value in payload.proof_values + ] + + except APIException: + # re-raise API exceptions + raise + except Exception as e: + log.error("Failed to check revocations", exc_info=True) + raise InternalServerException("Failed to check revocations") from e diff --git a/api/registry/api/v1.py b/api/registry/api/v1.py index a6199d882..ceb10a994 100644 --- a/api/registry/api/v1.py +++ b/api/registry/api/v1.py @@ -680,6 +680,10 @@ def stamp_display(request) -> List[StampDisplayResponse]: deprecated=True, ) def get_gtc_stake_legacy(request, address: str, round_id: int) -> GtcEventsResponse: + return handle_get_gtc_stake_legacy(address, round_id) + + +def handle_get_gtc_stake_legacy(address: str, round_id: int) -> GtcEventsResponse: """ Get GTC stake amount by address and round ID (from legacy contract) """ diff --git a/api/registry/management/commands/get_unmonitored_urls.py b/api/registry/management/commands/get_unmonitored_urls.py index 4919912f4..2615fc3c5 100644 --- a/api/registry/management/commands/get_unmonitored_urls.py +++ b/api/registry/management/commands/get_unmonitored_urls.py @@ -6,7 +6,7 @@ from django.core.management.base import BaseCommand, CommandError from ninja.openapi.schema import OpenAPISchema -from scorer.api import apis +from scorer.api import public_apis from .get_unmonitored_urls_config import get_config @@ -96,7 +96,7 @@ def get_unmonitored_urls(self, kwargs, auto_check_monitors): num_skipped_namespaces = 0 num_skipped_endpoints = 0 config = get_config(kwargs["base_url"], kwargs["base_url_xyz"]) - for api in apis: + for api in public_apis: openapi = OpenAPISchema(api=api, path_prefix="") paths = openapi.get("paths", {}) @@ -168,7 +168,7 @@ def get_unmonitored_urls(self, kwargs, auto_check_monitors): def get_all_urls_with_methods(self): combined_data = {} - for api in apis: + for api in public_apis: openapi = OpenAPISchema(api=api, path_prefix="") paths = openapi.get("paths", {}) diff --git a/api/registry/test/test_get_staking_results.py b/api/registry/test/test_get_staking_results.py index 0ed8f837d..4180894c2 100644 --- a/api/registry/test/test_get_staking_results.py +++ b/api/registry/test/test_get_staking_results.py @@ -1,5 +1,7 @@ import pytest +from django.conf import settings from django.test import Client + from registry.models import GTCStakeEvent pytestmark = pytest.mark.django_db @@ -23,6 +25,21 @@ def test_successful_get_staking_results(self, scorer_api_key, gtc_staking_respon # an extra stake event was added that is below the filtered amount, hence the minus 1 assert len(stakes) - 1 == len(response_data) + def test_internal_endpoint(self, scorer_api_key, gtc_staking_response): + stakes = list(GTCStakeEvent.objects.all()) + + client = Client() + response = client.get( + f"/internal/stake/legacy-gtc/{user_address}/1", + HTTP_AUTHORIZATION=settings.CGRANTS_API_TOKEN, + ) + + response_data = response.json()["results"] + assert response.status_code == 200 + + # an extra stake event was added that is below the filtered amount, hence the minus 1 + assert len(stakes) - 1 == len(response_data) + def test_item_in_filter_condition_is_not_present( self, scorer_api_key, gtc_staking_response ): diff --git a/api/scorer/api.py b/api/scorer/api.py index 7dc6baa1d..7271b1ef6 100644 --- a/api/scorer/api.py +++ b/api/scorer/api.py @@ -100,11 +100,11 @@ def service_unavailable(request, _): passport_admin_api = NinjaAPI(urls_namespace="passport-admin", docs_url=None) passport_admin_api.add_router("/passport-admin", passport_admin_router) +public_apis = [registry_api_v1, ceramic_cache_api_v1, passport_admin_api] apis = [ - registry_api_v1, - ceramic_cache_api_v1, - passport_admin_api, + feature_flag_api, + *public_apis, ] diff --git a/api/scorer/urls.py b/api/scorer/urls.py index 6e33a5c0a..c09ec0e02 100644 --- a/api/scorer/urls.py +++ b/api/scorer/urls.py @@ -22,13 +22,7 @@ from account.api import health from scorer.api import apis as api_list -from .api import ( - feature_flag_api, -) - urlpatterns = [ - path("registry/feature/", feature_flag_api.urls), - path("cgrants/", include("cgrants.urls")), path("health/", health, {}, "health-check"), path( "admin/login/", @@ -38,7 +32,9 @@ path("admin/", admin.site.urls), path("account/", include("account.urls")), path("social/", include("social_django.urls", namespace="social")), - path("trusta_labs/", include("trusta_labs.urls")), + # TODO 3280 Remove cgrants URLs entry + path("cgrants/", include("cgrants.urls")), + # TODO 3280 Remove stake URLs entry path("stake/", include("stake.urls")), path("passport/", include("passport.urls")), path("internal/", include("internal.urls")), diff --git a/api/stake/api.py b/api/stake/api.py index 8269c05d3..4a45b2bba 100644 --- a/api/stake/api.py +++ b/api/stake/api.py @@ -4,42 +4,46 @@ from ninja_extra import NinjaExtraAPI import api_logging as logging +from internal.api_key import internal_api_key from registry.api.utils import is_valid_address, with_read_db from registry.exceptions import InvalidAddressException, StakingRequestError from stake.models import Stake -from stake.schema import ErrorMessageResponse, GetSchemaResponse, StakeSchema -from trusta_labs.api import CgrantsApiKey - -secret_key = CgrantsApiKey() +from stake.schema import ErrorMessageResponse, StakeResponse, StakeSchema log = logging.getLogger(__name__) +# TODO 3280 Remove this api api = NinjaExtraAPI(urls_namespace="stake") +# Currently no public enabled endpoint for this +# TODO 3280 Remove this endpoint @api.get( "/gtc/{str:address}", - auth=secret_key, + auth=internal_api_key, response={ - 200: GetSchemaResponse, + 200: StakeResponse, 400: ErrorMessageResponse, }, summary="Retrieve GTC stake amounts for the GTC Staking stamp", description="Get self and community GTC stakes for an address", ) -def get_gtc_stake(request, address: str) -> GetSchemaResponse: +def get_gtc_stake(request, address: str) -> StakeResponse: """ Get relevant GTC stakes for an address """ + return handle_get_gtc_stake(address) + + +def handle_get_gtc_stake(address: str) -> StakeResponse: if not is_valid_address(address): raise InvalidAddressException() - get_stake_response = handle_get_gtc_stake(address) - response = GetSchemaResponse(items=get_stake_response) - return response + items = get_gtc_stake_for_address(address) + return StakeResponse(items=items) -def handle_get_gtc_stake(address: str) -> List[StakeSchema]: +def get_gtc_stake_for_address(address: str) -> List[StakeSchema]: address = address.lower() try: diff --git a/api/stake/schema.py b/api/stake/schema.py index 87b18b0b1..503e8be09 100644 --- a/api/stake/schema.py +++ b/api/stake/schema.py @@ -23,5 +23,5 @@ def serialize_amount(self, amount: Decimal, _info): return format(amount, ".18f") -class GetSchemaResponse(Schema): +class StakeResponse(Schema): items: List[StakeSchema] diff --git a/api/stake/test/test_get_stake.py b/api/stake/test/test_get_stake.py index e3762b368..17d7dff62 100644 --- a/api/stake/test/test_get_stake.py +++ b/api/stake/test/test_get_stake.py @@ -67,7 +67,7 @@ class TestGetStakingResults: def test_successful_get_staking_results(self, mock_stakes, sample_address): client = Client() response = client.get( - f"/stake/gtc/{sample_address.lower()}", + f"/internal/stake/gtc/{sample_address.lower()}", HTTP_AUTHORIZATION=settings.CGRANTS_API_TOKEN, ) response_data = response.json()["items"] @@ -114,7 +114,9 @@ def test_successful_get_staking_results(self, mock_stakes, sample_address): def test_failed_auth(self, mock_stakes, sample_address): client = Client() - response = client.get(f"/stake/gtc/{sample_address}", HTTP_AUTHORIZATION="None") + response = client.get( + f"/internal/stake/gtc/{sample_address}", HTTP_AUTHORIZATION="None" + ) assert response.status_code == 401 diff --git a/api/tos/api.py b/api/tos/api.py index 4d6788019..9b78d3c97 100644 --- a/api/tos/api.py +++ b/api/tos/api.py @@ -1,11 +1,9 @@ -import api_logging as logging from ninja_extra import NinjaExtraAPI, status from ninja_extra.exceptions import APIException + +import api_logging as logging from tos.models import Tos, TosAcceptanceProof from tos.schema import TosAccepted, TosSigned, TosToSign -from trusta_labs.api import CgrantsApiKey - -secret_key = CgrantsApiKey() log = logging.getLogger(__name__) @@ -27,5 +25,5 @@ def accept_tos(payload: TosSigned) -> None: if Tos.accept(payload.tos_type, payload.nonce, payload.signature): return raise APIException( - "Failed to process the tos acceptance proof", status.HTTP_400_BAD_REQUESTÍ + "Failed to process the tos acceptance proof", status.HTTP_400_BAD_REQUEST ) diff --git a/api/trusta_labs/api.py b/api/trusta_labs/api.py deleted file mode 100644 index 68280a568..000000000 --- a/api/trusta_labs/api.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.conf import settings -from ninja import Schema -from ninja.security import APIKeyHeader -from ninja_extra import NinjaExtraAPI, status -from ninja_extra.exceptions import APIException -from registry.models import Event - -api = NinjaExtraAPI(urls_namespace="trusta_labs") - - -class CgrantsApiKey(APIKeyHeader): - param_name = "AUTHORIZATION" - - def authenticate(self, request, key): - if key == settings.CGRANTS_API_TOKEN: - return key - - -cg_api_key = CgrantsApiKey() - - -class TrustaLabsScorePayload(Schema): - address: str - scoreData: dict - - -class TrustaLabsScoreResponse(Schema): - address: str - score: int - - -class TrustaLabsScoreHasNoPayload(APIException): - status_code = status.HTTP_422_UNPROCESSABLE_ENTITY - default_detail = "There is no payload with this request" - - -class TrustaLabsScoreHasNoAddress(APIException): - status_code = status.HTTP_422_UNPROCESSABLE_ENTITY - default_detail = "A Trusta Lab score must be accompanied by an address" - - -class TrustaLabsScoreHasNoScoreData(APIException): - status_code = status.HTTP_422_UNPROCESSABLE_ENTITY - default_detail = "A Trusta Lab request must include score data" - - -@api.post("/trusta-labs-score", auth=cg_api_key) -def create_trusta_labs_score_db(request, payload: TrustaLabsScorePayload): - if payload == None: - raise TrustaLabsScoreHasNoPayload() - - if payload.address == None: - raise TrustaLabsScoreHasNoAddress() - - if payload.scoreData == None: - raise TrustaLabsScoreHasNoScoreData() - - Event.objects.create( - action=Event.Action.TRUSTALAB_SCORE, - address=payload.address.lower(), - data=payload.scoreData, - ) diff --git a/api/trusta_labs/test/test_trusta_labs_score.py b/api/trusta_labs/test/test_trusta_labs_score.py deleted file mode 100644 index 1a85f5e1b..000000000 --- a/api/trusta_labs/test/test_trusta_labs_score.py +++ /dev/null @@ -1,78 +0,0 @@ -import json - -from django.conf import settings -from django.test import Client, TestCase -from registry.models import Event - -mock_trusta_labs_score_body = { - "address": "0x8u3eu3ydh3rydh3irydhu", - "scoreData": { - "address": "0x8u3eu3ydh3rydh3irydhu", - "sybilRiskScore": 20, - "sybilRiskLevel": "No Risk", - "subScore": { - "bulkOperationRisk": 10, - "starlikeAssetsNetworkRisk": 0, - "chainlikeAssetsNetworkRisk": 0, - "similarBehaviorSequenceRisk": 10, - "blacklistRisk": 0, - }, - }, -} - - -class TrustaLabsScoreTestCase(TestCase): - def test_create_trusta_labs_score(self): - """Test that creation of a trusta lab score works and saved correctly""" - self.headers = {"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN} - client = Client() - trusta_labs_response = client.post( - "/trusta_labs/trusta-labs-score", - json.dumps(mock_trusta_labs_score_body), - content_type="application/json", - **self.headers - ) - self.assertEqual(trusta_labs_response.status_code, 200) - - # Check that the trusta lab score was created - all_trusta_labs_scores = list( - Event.objects.filter(action=Event.Action.TRUSTALAB_SCORE) - ) - self.assertEqual(len(all_trusta_labs_scores), 1) - score = all_trusta_labs_scores[0] - self.assertEqual(score.address, mock_trusta_labs_score_body["address"]) - self.assertEqual(score.data["sybilRiskScore"], 20) - - def test_error_creating_trusta_lab_score(self): - self.headers = {"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN} - client = Client() - trusta_labs_response = client.post( - "/trusta_labs/trusta-labs-score", - "{}", - content_type="application/json", - **self.headers - ) - self.assertEqual(trusta_labs_response.status_code, 422) - - # Check that the trusta lab score was not created - all_trusta_labs_scores = list( - Event.objects.filter(action=Event.Action.TRUSTALAB_SCORE) - ) - self.assertEqual(len(all_trusta_labs_scores), 0) - - def test_bad_auth(self): - self.headers = {"HTTP_AUTHORIZATION": "bad_auth"} - client = Client() - trusta_labs_response = client.post( - "/trusta_labs/trusta-labs-score", - "{}", - content_type="application/json", - **self.headers - ) - self.assertEqual(trusta_labs_response.status_code, 401) - - # Check that the trusta lab score was not created - all_trusta_labs_scores = list( - Event.objects.filter(action=Event.Action.TRUSTALAB_SCORE) - ) - self.assertEqual(len(all_trusta_labs_scores), 0) diff --git a/api/trusta_labs/urls.py b/api/trusta_labs/urls.py deleted file mode 100644 index 724f6a45a..000000000 --- a/api/trusta_labs/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from .api import api - -urlpatterns = [ - path("", api.urls), -] diff --git a/infra/aws/embed/index.ts b/infra/aws/embed/index.ts index 21718d244..ee18b9707 100644 --- a/infra/aws/embed/index.ts +++ b/infra/aws/embed/index.ts @@ -1,12 +1,6 @@ import * as pulumi from "@pulumi/pulumi"; -import * as archive from "@pulumi/archive"; import * as aws from "@pulumi/aws"; -import { ListenerRule } from "@pulumi/aws/lb"; -import { Listener } from "@pulumi/aws/alb"; -import { secretsManager } from "infra-libs"; -import { defaultTags, stack } from "../../lib/tags"; -import { createLambdaFunction } from "../../lib/lambda"; import { createEmbedLambdaGeneric } from "./lambda_generic"; export function createEmbedLambdaFunctions(config: { @@ -27,7 +21,7 @@ export function createEmbedLambdaFunctions(config: { lbRuleConditions: [ { pathPattern: { - values: ["/embed/stamps/*"], + values: ["/internal/embed/stamps/*"], }, }, { @@ -41,12 +35,12 @@ export function createEmbedLambdaFunctions(config: { }); createEmbedLambdaGeneric({ ...config, - description: "Retreive the rate limit for an API key", + description: "Retrieve the rate limit for an API key", name: "embed-rl", lbRuleConditions: [ { pathPattern: { - values: ["/embed/validate-api-key"], + values: ["/internal/embed/validate-api-key"], }, }, { diff --git a/infra/aws/index.ts b/infra/aws/index.ts index d573a421b..ccf2bcacc 100644 --- a/infra/aws/index.ts +++ b/infra/aws/index.ts @@ -92,6 +92,13 @@ const coreInfraStack = new pulumi.StackReference( `passportxyz/core-infra/${stack}` ); +const privateAlbHttpListenerArn = coreInfraStack.getOutput( + "privateAlbHttpListenerArn" +); +const privatprivateAlbArnSuffixeAlbHttpListenerArn = coreInfraStack.getOutput( + "privateAlbArnSuffix" +); + const RDS_SECRET_ARN = coreInfraStack.getOutput("rdsSecretArn"); const vpcID = coreInfraStack.getOutput("vpcId"); @@ -287,7 +294,10 @@ type EcsTaskConfigurationType = { desiredCount: number; }; -type EcsServiceNameType = "scorer-api-default-1" | "scorer-api-reg-1"; +type EcsServiceNameType = + | "scorer-api-default-1" + | "scorer-api-reg-1" + | "scorer-api-internal-1"; const ecsTaskConfigurations: Record< EcsServiceNameType, Record @@ -325,6 +335,23 @@ const ecsTaskConfigurations: Record< cpu: 2048, desiredCount: 2, }, + "scorer-api-internal-1": { + review: { + memory: 1024, + cpu: 512, + desiredCount: 1, + }, + staging: { + memory: 1024, + cpu: 512, + desiredCount: 1, + }, + production: { + memory: 2048, + cpu: 512, + desiredCount: 2, + }, + }, }, }; @@ -335,6 +362,8 @@ if (PROVISION_STAGING_FOR_LOADTEST) { ecsTaskConfigurations["scorer-api-default-1"]["production"]; ecsTaskConfigurations["scorer-api-reg-1"]["staging"] = ecsTaskConfigurations["scorer-api-reg-1"]["production"]; + ecsTaskConfigurations["scorer-api-internal-1"]["staging"] = + ecsTaskConfigurations["scorer-api-internal-1"]["production"]; } // This matches the default security group that awsx previously created when creating the Cluster. @@ -513,6 +542,7 @@ const httpListener = new aws.alb.Listener("scorer-http-listener", { // Target group with the port of the Docker image const targetGroupDefault = createTargetGroup("scorer-api-default", vpcID); const targetGroupRegistry = createTargetGroup("scorer-api-reg", vpcID); +const targetGroupInternal = createTargetGroup("scorer-api-internal", vpcID); ////////////////////////////////////////////////////////////// // Create the HTTPS listener, and set the default target group @@ -957,6 +987,30 @@ const scorerServiceRegistry = createScorerECSService({ loadBalancerAlarmThresholds: alarmConfigurations, }); +const scorerServiceInternal = createScorerECSService({ + name: "scorer-api-internal-1", + config: { + ...baseScorerServiceConfig, + listenerRulePriority: 2102, + httpListenerArn: privateAlbHttpListenerArn, + httpListenerRulePaths: [ + { + pathPattern: { + values: ["/internal/*"], + }, + }, + ], + targetGroup: targetGroupInternal, + memory: ecsTaskConfigurations["scorer-api-internal-1"][stack].memory, + cpu: ecsTaskConfigurations["scorer-api-internal-1"][stack].cpu, + desiredCount: + ecsTaskConfigurations["scorer-api-internal-1"][stack].desiredCount, + }, + environment: apiEnvironment, + secrets: apiSecrets, + loadBalancerAlarmThresholds: alarmConfigurations, +}); + ////////////////////////////////////////////////////////////// // Set up the worker role ////////////////////////////////////////////////////////////// @@ -1529,13 +1583,6 @@ buildQueueLambdaFn({ }); // VERIFIER -const privateAlbHttpListenerArn = coreInfraStack.getOutput( - "privateAlbHttpListenerArn" -); -const privatprivateAlbArnSuffixeAlbHttpListenerArn = coreInfraStack.getOutput( - "privateAlbArnSuffix" -); - const verifier = pulumi .all([verifierDockerImage]) .apply(([_verifierDockerImage]) =>