From 5f3d3ce854b6613a6c5ed34902a18f9b2b0a65e0 Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Thu, 20 Feb 2025 16:43:56 -0700 Subject: [PATCH 1/8] feat(api): moving endpoints only used by IAM to the internal module --- api/ceramic_cache/api/v1.py | 21 +- api/cgrants/api.py | 252 +++++++++--------- ...test_cgrants_combined_contributions_api.py | 33 ++- api/internal/api.py | 54 ++++ api/scorer/api.py | 1 + api/scorer/urls.py | 9 +- api/stake/api.py | 54 ++-- api/stake/schema.py | 2 +- api/stake/test/test_get_stake.py | 6 +- 9 files changed, 233 insertions(+), 199 deletions(-) diff --git a/api/ceramic_cache/api/v1.py b/api/ceramic_cache/api/v1.py index b707f500a..4f33ad1b7 100644 --- a/api/ceramic_cache/api/v1.py +++ b/api/ceramic_cache/api/v1.py @@ -44,7 +44,7 @@ ) from registry.models import Score from stake.api import handle_get_gtc_stake -from stake.schema import GetSchemaResponse +from stake.schema import StakeResponse from trusta_labs.api import CgrantsApiKey from ..exceptions import ( @@ -560,19 +560,6 @@ def calc_score( return get_detailed_score_response_for_address(address, payload.alternate_scorer_id) -@router.get( - "/score/{int:scorer_id}/{str:address}", - response=DetailedScoreResponse, - auth=secret_key, -) -def calc_score_community( - request, - scorer_id: int, - address: str, -) -> DetailedScoreResponse: - return handle_get_ui_score(address, scorer_id) - - class FailedVerificationException(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = "Unable to authorize request" @@ -685,15 +672,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) + response = StakeResponse(items=get_stake_response) return response diff --git a/api/cgrants/api.py b/api/cgrants/api.py index 5088c7e41..6e7ce4994 100644 --- a/api/cgrants/api.py +++ b/api/cgrants/api.py @@ -127,22 +127,17 @@ def _get_contributor_statistics_for_protocol(address: str) -> dict: } -@api.get( - "/contributor_statistics", - response=ContributorStatistics, - auth=cg_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, - ) +# @api.get( +# "/contributor_statistics", +# response=ContributorStatistics, +# auth=cg_api_key, +# ) +# def contributor_statistics( +# request, address: str +# ): + +def handle_get_contributor_statistics(address: str): if not is_valid_address(address): raise InvalidAddressException() @@ -167,115 +162,118 @@ 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) +# Unused +# @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) + + +# Unused +# 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, +# } +# ) + + +# Unused +# @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/internal/api.py b/api/internal/api.py index 89c7ec7b4..1d98ff05c 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -6,8 +6,18 @@ from ninja_extra.exceptions import APIException import api_logging as logging +from ceramic_cache.api.v1 import handle_get_ui_score from ceramic_cache.exceptions import InternalServerException, TooManyStampsException from ceramic_cache.models import Ban, Revocation +from cgrants.api import ( + ContributorStatistics, + handle_get_contributor_statistics, +) +from registry.api.schema import DetailedScoreResponse +from registry.api.utils import is_valid_address +from registry.exceptions import InvalidAddressException +from stake.api import handle_get_gtc_stake +from stake.schema import ErrorMessageResponse, StakeResponse from trusta_labs.api import CgrantsApiKey from .exceptions import InvalidBanQueryException @@ -121,3 +131,47 @@ def check_revocations( except Exception as e: log.error("Failed to check revocations", exc_info=True) raise InternalServerException("Failed to check revocations") from e + + +@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: + """ + Get relevant GTC stakes for an address + """ + if not is_valid_address(address): + raise InvalidAddressException() + + get_stake_response = handle_get_gtc_stake(address) + response = StakeResponse(items=get_stake_response) + return response + + +@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) diff --git a/api/scorer/api.py b/api/scorer/api.py index 7dc6baa1d..5f268e69d 100644 --- a/api/scorer/api.py +++ b/api/scorer/api.py @@ -102,6 +102,7 @@ def service_unavailable(request, _): apis = [ + feature_flag_api, registry_api_v1, ceramic_cache_api_v1, passport_admin_api, diff --git a/api/scorer/urls.py b/api/scorer/urls.py index 6e33a5c0a..6964d50fb 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/", @@ -39,7 +33,8 @@ path("account/", include("account.urls")), path("social/", include("social_django.urls", namespace="social")), path("trusta_labs/", include("trusta_labs.urls")), - path("stake/", include("stake.urls")), + # path("cgrants/", include("cgrants.urls")), + # path("stake/", include("stake.urls")), path("passport/", include("passport.urls")), path("internal/", include("internal.urls")), path("embed/", include("embed.urls")), diff --git a/api/stake/api.py b/api/stake/api.py index 8269c05d3..27ee7a9a5 100644 --- a/api/stake/api.py +++ b/api/stake/api.py @@ -1,42 +1,42 @@ from typing import List from django.db.models import Q -from ninja_extra import NinjaExtraAPI import api_logging as logging -from registry.api.utils import is_valid_address, with_read_db -from registry.exceptions import InvalidAddressException, StakingRequestError +from registry.api.utils import with_read_db +from registry.exceptions import StakingRequestError from stake.models import Stake -from stake.schema import ErrorMessageResponse, GetSchemaResponse, StakeSchema +from stake.schema import StakeSchema from trusta_labs.api import CgrantsApiKey secret_key = CgrantsApiKey() log = logging.getLogger(__name__) -api = NinjaExtraAPI(urls_namespace="stake") - - -@api.get( - "/gtc/{str:address}", - auth=secret_key, - response={ - 200: GetSchemaResponse, - 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: - """ - Get relevant GTC stakes for an address - """ - if not is_valid_address(address): - raise InvalidAddressException() - - get_stake_response = handle_get_gtc_stake(address) - response = GetSchemaResponse(items=get_stake_response) - return response +# api = NinjaExtraAPI(urls_namespace="stake") + + +# Currently no public enabled endpoint for this +# @api.get( +# "/gtc/{str:address}", +# auth=???, +# 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: +# """ +# Get relevant GTC stakes for an address +# """ +# if not is_valid_address(address): +# raise InvalidAddressException() + +# get_stake_response = handle_get_gtc_stake(address) +# response = StakeResponse(items=get_stake_response) +# return response def handle_get_gtc_stake(address: str) -> List[StakeSchema]: 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 From 108224a349d0c562d8a63b9c25af1da27ae0ca69 Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Thu, 20 Feb 2025 16:50:03 -0700 Subject: [PATCH 2/8] feat(infra): adding ECS task for internal module --- infra/aws/index.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/infra/aws/index.ts b/infra/aws/index.ts index d573a421b..e245e11f7 100644 --- a/infra/aws/index.ts +++ b/infra/aws/index.ts @@ -287,7 +287,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 +328,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 +355,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 +535,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 +980,29 @@ const scorerServiceRegistry = createScorerECSService({ loadBalancerAlarmThresholds: alarmConfigurations, }); +const scorerServiceInternal = createScorerECSService({ + name: "scorer-api-internal-1", + config: { + ...baseScorerServiceConfig, + listenerRulePriority: 3001, + 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 ////////////////////////////////////////////////////////////// From 61287dd0760c3bb6a3b73b1eb495bac264e7d21e Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Mon, 24 Feb 2025 07:39:42 -0700 Subject: [PATCH 3/8] chore(api,infra): moved internal embed endpoints to /internal; using private listener --- api/ceramic_cache/api/v1.py | 4 +- api/embed/api.py | 34 +------- api/embed/test/test_api_stamps.py | 10 +-- api/embed/test/test_api_weights.py | 12 +-- api/internal/api.py | 135 +++++++++-------------------- api/internal/bans_revocations.py | 102 ++++++++++++++++++++++ api/stake/api.py | 21 +++-- infra/aws/embed/index.ts | 12 +-- infra/aws/index.ts | 17 ++-- 9 files changed, 182 insertions(+), 165 deletions(-) create mode 100644 api/internal/bans_revocations.py diff --git a/api/ceramic_cache/api/v1.py b/api/ceramic_cache/api/v1.py index 4f33ad1b7..b235224e4 100644 --- a/api/ceramic_cache/api/v1.py +++ b/api/ceramic_cache/api/v1.py @@ -43,7 +43,7 @@ NotFoundApiException, ) from registry.models import Score -from stake.api import handle_get_gtc_stake +from stake.api import get_gtc_stake_for_address from stake.schema import StakeResponse from trusta_labs.api import CgrantsApiKey @@ -679,7 +679,7 @@ def get_detailed_score_response_for_address( ) def get_staked_gtc(request) -> StakeResponse: address = get_address_from_did(request.did) - get_stake_response = handle_get_gtc_stake(address) + get_stake_response = get_gtc_stake_for_address(address) response = StakeResponse(items=get_stake_response) return response diff --git a/api/embed/api.py b/api/embed/api.py index 058ebf610..d652c1b07 100644 --- a/api/embed/api.py +++ b/api/embed/api.py @@ -10,7 +10,7 @@ CacheStampPayload, GetStampsWithV2ScoreResponse, ) -from ceramic_cache.api.v1 import handle_add_stamps_only, handle_get_scorer_weights +from ceramic_cache.api.v1 import handle_add_stamps_only from ceramic_cache.models import CeramicCache from registry.api.schema import ( ErrorMessageResponse, @@ -47,24 +47,7 @@ class AddStampsPayload(Schema): 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 -@api_router.post( - "/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) - - +# Endpoint for this defined in internal module def handle_embed_add_stamps( address: str, payload: AddStampsPayload ) -> GetStampsWithV2ScoreResponse: @@ -119,16 +102,3 @@ def validate_api_key(request) -> AccountAPIKeySchema: This API is intended to be used in the embed service in the passport repo """ return AccountAPIKeySchema.from_orm(request.api_key) - - -@api_router.get("/weights", response=Dict[str, float]) -def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, float]: - """ - --- - get: - description: Get embed weights - responses: - 200: - description: Returns the embed weights - """ - return handle_get_scorer_weights(community_id) 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_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 1d98ff05c..da260125e 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -1,26 +1,25 @@ -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.api.v1 import handle_get_ui_score -from ceramic_cache.exceptions import InternalServerException, TooManyStampsException -from ceramic_cache.models import Ban, Revocation +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 ( + AddStampsPayload, + handle_embed_add_stamps, +) from registry.api.schema import DetailedScoreResponse -from registry.api.utils import is_valid_address -from registry.exceptions import InvalidAddressException from stake.api import handle_get_gtc_stake from stake.schema import ErrorMessageResponse, StakeResponse from trusta_labs.api import CgrantsApiKey -from .exceptions import InvalidBanQueryException +from .bans_revocations import handle_check_bans, handle_check_revocations from .schema import ( CheckBanResult, Credential, @@ -47,53 +46,7 @@ @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( @@ -102,35 +55,7 @@ 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( @@ -144,15 +69,7 @@ def check_revocations( description="Get self and community GTC stakes for an address", ) def get_gtc_stake(request, address: str) -> StakeResponse: - """ - Get relevant GTC stakes for an address - """ - if not is_valid_address(address): - raise InvalidAddressException() - - get_stake_response = handle_get_gtc_stake(address) - response = StakeResponse(items=get_stake_response) - return response + return handle_get_gtc_stake(address) @api_router.get( @@ -175,3 +92,33 @@ def calc_score_community( 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) 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/stake/api.py b/api/stake/api.py index 27ee7a9a5..090f313e9 100644 --- a/api/stake/api.py +++ b/api/stake/api.py @@ -3,10 +3,10 @@ from django.db.models import Q import api_logging as logging -from registry.api.utils import with_read_db -from registry.exceptions import StakingRequestError +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 StakeSchema +from stake.schema import StakeResponse, StakeSchema from trusta_labs.api import CgrantsApiKey secret_key = CgrantsApiKey() @@ -31,15 +31,18 @@ # """ # Get relevant GTC stakes for an address # """ -# if not is_valid_address(address): -# raise InvalidAddressException() +# return handle_get_gtc_stake(address) -# get_stake_response = handle_get_gtc_stake(address) -# response = StakeResponse(items=get_stake_response) -# return response +def handle_get_gtc_stake(address: str) -> StakeResponse: + if not is_valid_address(address): + raise InvalidAddressException() -def handle_get_gtc_stake(address: str) -> List[StakeSchema]: + items = get_gtc_stake_for_address(address) + return StakeResponse(items=items) + + +def get_gtc_stake_for_address(address: str) -> List[StakeSchema]: address = address.lower() try: 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 e245e11f7..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"); @@ -984,7 +991,8 @@ const scorerServiceInternal = createScorerECSService({ name: "scorer-api-internal-1", config: { ...baseScorerServiceConfig, - listenerRulePriority: 3001, + listenerRulePriority: 2102, + httpListenerArn: privateAlbHttpListenerArn, httpListenerRulePaths: [ { pathPattern: { @@ -1575,13 +1583,6 @@ buildQueueLambdaFn({ }); // VERIFIER -const privateAlbHttpListenerArn = coreInfraStack.getOutput( - "privateAlbHttpListenerArn" -); -const privatprivateAlbArnSuffixeAlbHttpListenerArn = coreInfraStack.getOutput( - "privateAlbArnSuffix" -); - const verifier = pulumi .all([verifierDockerImage]) .apply(([_verifierDockerImage]) => From 0f217aea3e4b79114e833de5d6271a6e7f3cf768 Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Mon, 24 Feb 2025 09:05:09 -0700 Subject: [PATCH 4/8] chore(api): temporarily restore legacy endpoints --- api/ceramic_cache/api/v1.py | 14 ++++ api/cgrants/api.py | 133 +++--------------------------------- api/embed/api.py | 35 +++++++++- api/scorer/urls.py | 6 +- api/stake/api.py | 37 +++++----- 5 files changed, 80 insertions(+), 145 deletions(-) diff --git a/api/ceramic_cache/api/v1.py b/api/ceramic_cache/api/v1.py index b235224e4..b51df36c1 100644 --- a/api/ceramic_cache/api/v1.py +++ b/api/ceramic_cache/api/v1.py @@ -560,6 +560,20 @@ 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, +) +def calc_score_community( + request, + scorer_id: int, + address: str, +) -> DetailedScoreResponse: + return handle_get_ui_score(address, scorer_id) + + class FailedVerificationException(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = "Unable to authorize request" diff --git a/api/cgrants/api.py b/api/cgrants/api.py index 6e7ce4994..c241d4d89 100644 --- a/api/cgrants/api.py +++ b/api/cgrants/api.py @@ -127,14 +127,14 @@ def _get_contributor_statistics_for_protocol(address: str) -> dict: } -# @api.get( -# "/contributor_statistics", -# response=ContributorStatistics, -# auth=cg_api_key, -# ) -# def contributor_statistics( -# request, address: str -# ): +# TODO 3280 Remove this endpoint +@api.get( + "/contributor_statistics", + response=ContributorStatistics, + auth=cg_api_key, +) +def contributor_statistics(request, address: str): + return handle_get_contributor_statistics(address) def handle_get_contributor_statistics(address: str): @@ -160,120 +160,3 @@ def handle_get_contributor_statistics(address: str): } return JsonResponse(combined_contributions) - - -# Unused -# @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) - - -# Unused -# 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, -# } -# ) - - -# Unused -# @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/embed/api.py b/api/embed/api.py index d652c1b07..55e6fa18d 100644 --- a/api/embed/api.py +++ b/api/embed/api.py @@ -10,7 +10,7 @@ CacheStampPayload, GetStampsWithV2ScoreResponse, ) -from ceramic_cache.api.v1 import handle_add_stamps_only +from ceramic_cache.api.v1 import handle_add_stamps_only, handle_get_scorer_weights from ceramic_cache.models import CeramicCache from registry.api.schema import ( ErrorMessageResponse, @@ -48,6 +48,24 @@ class AddStampsPayload(Schema): # Endpoint for this defined in internal module +# TODO 3280 Remove this endpoint +@api_router.post( + "/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) + + def handle_embed_add_stamps( address: str, payload: AddStampsPayload ) -> GetStampsWithV2ScoreResponse: @@ -102,3 +120,18 @@ def validate_api_key(request) -> AccountAPIKeySchema: This API is intended to be used in the embed service in the passport repo """ return AccountAPIKeySchema.from_orm(request.api_key) + + +# Endpoint for this defined in internal module +# 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]: + """ + --- + get: + description: Get embed weights + responses: + 200: + description: Returns the embed weights + """ + return handle_get_scorer_weights(community_id) diff --git a/api/scorer/urls.py b/api/scorer/urls.py index 6964d50fb..3a8e1204d 100644 --- a/api/scorer/urls.py +++ b/api/scorer/urls.py @@ -33,8 +33,10 @@ path("account/", include("account.urls")), path("social/", include("social_django.urls", namespace="social")), path("trusta_labs/", include("trusta_labs.urls")), - # path("cgrants/", include("cgrants.urls")), - # path("stake/", include("stake.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")), path("embed/", include("embed.urls")), diff --git a/api/stake/api.py b/api/stake/api.py index 090f313e9..de5c93905 100644 --- a/api/stake/api.py +++ b/api/stake/api.py @@ -1,37 +1,40 @@ from typing import List from django.db.models import Q +from ninja_extra import NinjaExtraAPI import api_logging as logging 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 StakeResponse, StakeSchema +from stake.schema import ErrorMessageResponse, StakeResponse, StakeSchema from trusta_labs.api import CgrantsApiKey secret_key = CgrantsApiKey() log = logging.getLogger(__name__) -# api = NinjaExtraAPI(urls_namespace="stake") +# TODO 3280 Remove this api +api = NinjaExtraAPI(urls_namespace="stake") # Currently no public enabled endpoint for this -# @api.get( -# "/gtc/{str:address}", -# auth=???, -# 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: -# """ -# Get relevant GTC stakes for an address -# """ -# return handle_get_gtc_stake(address) +# TODO 3280 Remove this endpoint +@api.get( + "/gtc/{str:address}", + auth=secret_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: + """ + Get relevant GTC stakes for an address + """ + return handle_get_gtc_stake(address) def handle_get_gtc_stake(address: str) -> StakeResponse: From 0246916210ae95497cc5aa2387a0256962bfea8a Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Mon, 24 Feb 2025 09:44:55 -0700 Subject: [PATCH 5/8] fix(api): fixing issue introduced to unmonitored URLs script --- api/registry/management/commands/get_unmonitored_urls.py | 6 +++--- api/scorer/api.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) 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/scorer/api.py b/api/scorer/api.py index 5f268e69d..7271b1ef6 100644 --- a/api/scorer/api.py +++ b/api/scorer/api.py @@ -100,12 +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 = [ feature_flag_api, - registry_api_v1, - ceramic_cache_api_v1, - passport_admin_api, + *public_apis, ] From 04a93a3f0f1bc3447c4a41ec2fbf84a6e0eb4f1f Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Tue, 25 Feb 2025 07:03:53 -0700 Subject: [PATCH 6/8] chore(api): removed trusta API --- api/account/api.py | 11 ++- api/ceramic_cache/api/v1.py | 6 +- api/cgrants/api.py | 20 +---- api/embed/api.py | 5 +- api/internal/api.py | 15 +++- api/internal/api_key.py | 13 ++++ api/scorer/urls.py | 1 - api/stake/api.py | 6 +- api/tos/api.py | 8 +- api/trusta_labs/api.py | 62 --------------- .../test/test_trusta_labs_score.py | 78 ------------------- api/trusta_labs/urls.py | 7 -- 12 files changed, 41 insertions(+), 191 deletions(-) create mode 100644 api/internal/api_key.py delete mode 100644 api/trusta_labs/api.py delete mode 100644 api/trusta_labs/test/test_trusta_labs_score.py delete mode 100644 api/trusta_labs/urls.py diff --git a/api/account/api.py b/api/account/api.py index 1cd093ef8..9dc5cee12 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") @@ -676,11 +674,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 b51df36c1..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, ) @@ -45,7 +46,6 @@ from registry.models import Score from stake.api import get_gtc_stake_for_address from stake.schema import StakeResponse -from trusta_labs.api import CgrantsApiKey from ..exceptions import ( InternalServerException, @@ -71,8 +71,6 @@ router = Router() -secret_key = CgrantsApiKey() - def get_address_from_did(did: str): return did.split(":")[-1] @@ -564,7 +562,7 @@ def calc_score( @router.get( "/score/{int:scorer_id}/{str:address}", response=DetailedScoreResponse, - auth=secret_key, + auth=internal_api_key, ) def calc_score_community( request, diff --git a/api/cgrants/api.py b/api/cgrants/api.py index c241d4d89..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() @@ -131,7 +115,7 @@ def _get_contributor_statistics_for_protocol(address: str) -> dict: @api.get( "/contributor_statistics", response=ContributorStatistics, - auth=cg_api_key, + auth=internal_api_key, ) def contributor_statistics(request, address: str): return handle_get_contributor_statistics(address) diff --git a/api/embed/api.py b/api/embed/api.py index 55e6fa18d..8b9e364c4 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,9 +39,6 @@ log = logging.getLogger(__name__) -internal_api_key = CgrantsApiKey() - - class AddStampsPayload(Schema): scorer_id: int stamps: List[Any] diff --git a/api/internal/api.py b/api/internal/api.py index da260125e..421541e5d 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -4,6 +4,7 @@ from ninja_extra import NinjaExtraAPI import api_logging as logging +from account.api import handle_check_allow_list 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 ( @@ -17,8 +18,8 @@ from registry.api.schema import DetailedScoreResponse from stake.api import handle_get_gtc_stake from stake.schema import ErrorMessageResponse, StakeResponse -from trusta_labs.api import CgrantsApiKey +from .api_key import internal_api_key from .bans_revocations import handle_check_bans, handle_check_revocations from .schema import ( CheckBanResult, @@ -41,9 +42,6 @@ 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]: return handle_check_bans(payload) @@ -122,3 +120,12 @@ def add_stamps( ) def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, float]: return handle_get_scorer_weights(community_id) + + +@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) 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/scorer/urls.py b/api/scorer/urls.py index 3a8e1204d..c09ec0e02 100644 --- a/api/scorer/urls.py +++ b/api/scorer/urls.py @@ -32,7 +32,6 @@ 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 diff --git a/api/stake/api.py b/api/stake/api.py index de5c93905..4a45b2bba 100644 --- a/api/stake/api.py +++ b/api/stake/api.py @@ -4,13 +4,11 @@ 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, StakeResponse, StakeSchema -from trusta_labs.api import CgrantsApiKey - -secret_key = CgrantsApiKey() log = logging.getLogger(__name__) @@ -22,7 +20,7 @@ # TODO 3280 Remove this endpoint @api.get( "/gtc/{str:address}", - auth=secret_key, + auth=internal_api_key, response={ 200: StakeResponse, 400: ErrorMessageResponse, 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), -] From ba86fc54fd6c72af3359bec546cb37a9308dd8f9 Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Tue, 25 Feb 2025 17:45:30 -0700 Subject: [PATCH 7/8] chore: moved a couple more endpoints --- api/account/api.py | 5 ++++ api/embed/api.py | 6 ++++- api/embed/test/test_api_validate_api_key.py | 4 ++-- api/internal/api.py | 26 ++++++++++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/api/account/api.py b/api/account/api.py index 9dc5cee12..5e880ae5b 100644 --- a/api/account/api.py +++ b/api/account/api.py @@ -595,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( diff --git a/api/embed/api.py b/api/embed/api.py index 8b9e364c4..7b4ee171e 100644 --- a/api/embed/api.py +++ b/api/embed/api.py @@ -99,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 @@ -112,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 @@ -119,7 +124,6 @@ def validate_api_key(request) -> AccountAPIKeySchema: return AccountAPIKeySchema.from_orm(request.api_key) -# Endpoint for this defined in internal module # 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_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/internal/api.py b/api/internal/api.py index 421541e5d..c3c81a955 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -4,7 +4,7 @@ from ninja_extra import NinjaExtraAPI import api_logging as logging -from account.api import handle_check_allow_list +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 ( @@ -12,10 +12,13 @@ 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 +from registry.api.utils import ApiKey from stake.api import handle_get_gtc_stake from stake.schema import ErrorMessageResponse, StakeResponse @@ -122,6 +125,22 @@ def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, 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, @@ -129,3 +148,8 @@ def get_embed_weights(request, community_id: Optional[str] = None) -> Dict[str, ) 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) From 8e4489950545ec181b49a1a5cbd2a9b48305a1ff Mon Sep 17 00:00:00 2001 From: Lucian Hymer Date: Wed, 26 Feb 2025 07:36:36 -0700 Subject: [PATCH 8/8] chore(api): adding legacy stake endpoint to internal API --- api/internal/api.py | 14 +++++++++++++- api/registry/api/v1.py | 4 ++++ api/registry/test/test_get_staking_results.py | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/api/internal/api.py b/api/internal/api.py index c3c81a955..d5cd13d81 100644 --- a/api/internal/api.py +++ b/api/internal/api.py @@ -17,8 +17,9 @@ handle_embed_add_stamps, handle_validate_embed_api_key, ) -from registry.api.schema import DetailedScoreResponse +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 @@ -73,6 +74,17 @@ 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, 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/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 ):