-
Notifications
You must be signed in to change notification settings - Fork 257
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
T7860 credit card v2 implementation (#586)
* Add Credit Card token backend * . * . * . * . * . * . * .
- Loading branch information
Showing
8 changed files
with
306 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import boto3 | ||
import botocore | ||
import socket | ||
import json | ||
|
||
from canarytokens.settings import FrontendSettings | ||
from canarytokens.models import Canarytoken | ||
from dataclasses import dataclass | ||
from typing import Optional, Tuple | ||
from enum import Enum | ||
from pydantic import BaseModel | ||
|
||
|
||
frontend_settings = FrontendSettings() | ||
|
||
|
||
_CACHED_LAMBDA_CLIENT = None | ||
_RETRY_COUNT = 2 | ||
|
||
|
||
class _Api(Enum): | ||
CARD_CREATE = "/card/create" | ||
CUSTOMER_DETAILS = "/customer/details" | ||
|
||
|
||
class Status(Enum): | ||
SUCCESS = "success" | ||
CUSTOMER_NOT_FOUND = "customer_not_found" | ||
NO_AVAILABLE_CARDS = "no_cards" | ||
NO_MORE_CREDITS = "no_more_credits" | ||
ERROR = "error" | ||
FORBIDDEN = "forbidden" | ||
|
||
|
||
@dataclass(frozen=True) | ||
class CreditCard: | ||
card_id: str | ||
card_number: str | ||
cvv: str | ||
expiry_month: int | ||
expiry_year: int | ||
name_on_card: str | ||
|
||
|
||
@dataclass(frozen=True) | ||
class Customer: | ||
guid: str | ||
created: str | ||
canarytoken_domain: str | ||
cards_quota: int | ||
cards_assigned: int | ||
|
||
|
||
class CreditCardTrigger(BaseModel): | ||
canarytoken: Canarytoken | ||
auth_code: Optional[str] | ||
billing_amount: Optional[str] | ||
billing_currency: Optional[str] | ||
card_id: Optional[str] | ||
card_nickname: Optional[str] | ||
client_data: Optional[str] | ||
failure_reason: Optional[str] | ||
masked_card_number: Optional[str] | ||
merchant: Optional[dict] | ||
network_transaction_id: Optional[str] | ||
posted_date: Optional[str] | ||
retrieval_ref: Optional[str] | ||
status: Optional[str] | ||
transaction_amount: Optional[str] | ||
transaction_currency: Optional[str] | ||
transaction_date: Optional[str] | ||
transaction_id: Optional[str] | ||
transaction_type: Optional[str] | ||
risk_details: Optional[dict] | ||
|
||
|
||
def _get_lambda_client(refresh_client: bool = False): | ||
"""Creates a botocore session and grabs sts client. | ||
This allows for getting assumed role creds without polluting | ||
or misusing the default boto3 session. | ||
Using these sts creds a client is returned for a `service_name` service. | ||
Returns: | ||
boto3.Client: boto3 client of the Lambda service. | ||
""" | ||
global _CACHED_LAMBDA_CLIENT | ||
|
||
if _CACHED_LAMBDA_CLIENT is not None and not refresh_client: | ||
return _CACHED_LAMBDA_CLIENT | ||
|
||
botocore_session = botocore.session.get_session() | ||
botocore_session.set_config_variable("CREDENTIALS_FILE".lower(), "") | ||
botocore_session.set_config_variable("SHARED_CREDENTIALS_FILE".lower(), "") | ||
botocore_session.set_config_variable("CONFIG_FILE".lower(), "") | ||
session = boto3.Session(botocore_session=botocore_session) | ||
client = session.client("sts").assume_role( | ||
RoleArn=f"arn:aws:iam::{frontend_settings.CREDIT_CARD_INFRA_ACCOUNT_ID}:role/{frontend_settings.CREDIT_CARD_INFRA_ACCESS_ROLE}", | ||
RoleSessionName=socket.gethostname(), | ||
) | ||
client_session = boto3.session.Session() | ||
_CACHED_LAMBDA_CLIENT = client_session.client( | ||
"lambda", | ||
region_name=frontend_settings.CREDIT_CARD_INFRA_REGION, | ||
aws_access_key_id=client["Credentials"]["AccessKeyId"], | ||
aws_secret_access_key=client["Credentials"]["SecretAccessKey"], | ||
aws_session_token=client["Credentials"]["SessionToken"], | ||
) | ||
|
||
return _CACHED_LAMBDA_CLIENT | ||
|
||
|
||
def _invoke_lambda(payload: dict) -> dict: | ||
client = _get_lambda_client() | ||
|
||
for attempt in range(_RETRY_COUNT): | ||
try: | ||
return client.invoke( | ||
FunctionName=frontend_settings.CREDIT_CARD_INFRA_LAMBDA, | ||
InvocationType="RequestResponse", | ||
Payload=json.dumps(payload), | ||
) | ||
except botocore.exceptions.ClientError as err: | ||
if ( | ||
err.response["Error"]["Code"] == "ExpiredTokenException" | ||
and attempt < _RETRY_COUNT - 1 | ||
): | ||
client = _get_lambda_client(refresh_client=True) | ||
continue | ||
raise err | ||
|
||
|
||
def create_card(canarytoken: str) -> Tuple[Status, Optional[CreditCard]]: | ||
if not frontend_settings.CREDIT_CARD_TOKEN_ENABLED: | ||
return (Status.ERROR, None) | ||
|
||
payload = { | ||
"api": _Api.CARD_CREATE.value, | ||
"guid": frontend_settings.CREDIT_CARD_INFRA_CUSTOMER_GUID, | ||
"secret": frontend_settings.CREDIT_CARD_INFRA_CUSTOMER_SECRET, | ||
"canarytoken": canarytoken, | ||
} | ||
|
||
response = _invoke_lambda(payload) | ||
response_payload = json.loads(response["Payload"].read()) | ||
|
||
status = Status(response_payload.get("status")) | ||
|
||
if status == Status.SUCCESS: | ||
return (Status.SUCCESS, CreditCard(**response_payload["body"]["card"])) | ||
|
||
return (status, None) | ||
|
||
|
||
def get_customer_details() -> Tuple[Status, Optional[Customer]]: | ||
if not frontend_settings.CREDIT_CARD_TOKEN_ENABLED: | ||
return (Status.ERROR, None) | ||
|
||
payload = { | ||
"api": _Api.CUSTOMER_DETAILS.value, | ||
"guid": frontend_settings.CREDIT_CARD_INFRA_CUSTOMER_GUID, | ||
"secret": frontend_settings.CREDIT_CARD_INFRA_CUSTOMER_SECRET, | ||
} | ||
|
||
response = _invoke_lambda(payload) | ||
response_payload = json.loads(response["Payload"].read()) | ||
|
||
status = Status(response_payload.get("status")) | ||
|
||
if status == Status.SUCCESS: | ||
return (Status.SUCCESS, Customer(**response_payload["body"]["customer"])) | ||
|
||
return (status, None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.