generated from ministryofjustice/template-repository
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Code to exchange entra token for aws token to test identity propogation * Added commands to get local debugging working against apc dev. Added JWT as dependency * Added quicksight embedding * Add initial database access app * Move quicksight template, use namespace url * Use management account profile when running locally * Initial aws service for Quicksight * Add code to ensure single boto3 session created * Add glueservice and more refactoring * Refactor base AWS service * Fix tests and linting * Remove static analysis feature from devcontainer Static analysis includes checkov, which installed a version of botocore that conflicted with the version pinned in the requirements. It also installed packages as the root user, which meant when trying to upgrade botocore, a permission error was raised as the vscode user that runs the dev container does not have permission to remove packages installed as root. We dont need to run checkov locally so removing, which resolves the issue. * Add instructions to retreive .env file * Attempt to appease superlinter * Update the home and login failure pages * Tidy up the frontend * Prompt user to login when authenticating * Update default behaviour when retreiving STS creds We only want to default to using STS to retrieve AWS credentials when not using a service role that already has permission to call services. These changes attempt to make that more explicit. For now, we should only default to using STS when running locally. --------- Co-authored-by: jamesstottmoj <[email protected]>
- Loading branch information
1 parent
49d3646
commit 3abe4e6
Showing
37 changed files
with
664 additions
and
54 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
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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
.env | ||
.env.bak | ||
.terraform/ | ||
coverage/ | ||
venv/ | ||
|
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
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,5 @@ | ||
from .base import AWSService | ||
from .glue import GlueService | ||
from .quicksight import QuicksightService | ||
|
||
__all__ = ["AWSService", "QuicksightService", "GlueService"] |
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,44 @@ | ||
from django.conf import settings | ||
|
||
import boto3 | ||
import botocore | ||
import sentry_sdk | ||
|
||
from . import session | ||
|
||
|
||
class AWSService: | ||
aws_service_name: str = "" | ||
|
||
def __init__(self, assume_role_name=None, profile_name=None, region_name=None): | ||
self.assume_role_name = assume_role_name | ||
self.profile_name = profile_name | ||
self.region_name = region_name or settings.AWS_DEFAULT_REGION | ||
|
||
@property | ||
def credential_session_set(self) -> session.AWSCredentialSessionSet: | ||
return session.AWSCredentialSessionSet() | ||
|
||
@property | ||
def boto3_session(self) -> boto3.Session: | ||
return self.credential_session_set.get_or_create_session( | ||
profile_name=self.profile_name, | ||
assume_role_name=self.assume_role_name, | ||
region_name=self.region_name, | ||
) | ||
|
||
@property | ||
def client(self): | ||
return self.boto3_session.client(self.aws_service_name) | ||
|
||
def _request(self, method_name, **kwargs): | ||
""" | ||
Make a request to the AWS service client. Handles exceptions and logs them to Sentry. | ||
""" | ||
try: | ||
return getattr(self.client, method_name)(**kwargs) | ||
except botocore.exceptions.ClientError as e: | ||
if settings.DEBUG: | ||
raise e | ||
sentry_sdk.capture_exception(e) | ||
return 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from django.conf import settings | ||
|
||
from . import base | ||
|
||
|
||
class GlueService(base.AWSService): | ||
aws_service_name = "glue" | ||
|
||
def __init__(self, catalog_id=None): | ||
super().__init__() | ||
self.catalog_id = catalog_id or settings.GLUE_CATALOG_ID | ||
|
||
def get_database_list(self): | ||
databases = self._request("get_databases") | ||
if not databases: | ||
return [] | ||
return databases["DatabaseList"] | ||
|
||
def get_table_list(self, database_name): | ||
tables = self._request("get_tables", CatalogId=self.catalog_id, DatabaseName=database_name) | ||
if not tables: | ||
return [] | ||
return tables["TableList"] | ||
|
||
def get_table_detail(self, database_name, table_name): | ||
table = self._request( | ||
"get_table", CatalogId=self.catalog_id, DatabaseName=database_name, Name=table_name | ||
) | ||
if not table: | ||
return {} | ||
return table["Table"] |
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,25 @@ | ||
from django.conf import settings | ||
|
||
from . import base | ||
|
||
|
||
class QuicksightService(base.AWSService): | ||
aws_service_name = "quicksight" | ||
|
||
def get_embed_url(self, user): | ||
response = self._request( | ||
"generate_embed_url_for_registered_user", | ||
AwsAccountId=settings.COMPUTE_ACCOUNT_ID, | ||
UserArn=user.quicksight_arn, | ||
ExperienceConfiguration={ | ||
"QuickSightConsole": { | ||
"InitialPath": "/start", | ||
"FeatureConfigurations": {"StatePersistence": {"Enabled": True}}, | ||
}, | ||
}, | ||
AllowedDomains=settings.QUICKSIGHT_DOMAINS, | ||
) | ||
if response: | ||
return response["EmbedUrl"] | ||
|
||
return response |
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,102 @@ | ||
from django.conf import settings | ||
|
||
import boto3 | ||
import structlog | ||
from botocore import credentials | ||
from botocore.session import get_session | ||
|
||
log = structlog.getLogger(__name__) | ||
|
||
TTL = 1500 | ||
|
||
|
||
class BotoSession: | ||
def __init__(self, assume_role_name=None, profile_name=None, region_name=None): | ||
self.assume_role_name = assume_role_name | ||
if self.assume_role_name is None and settings.DEFAULT_STS_ROLE_TO_ASSUME is not None: | ||
self.assume_role_name = settings.DEFAULT_STS_ROLE_TO_ASSUME | ||
|
||
self.region_name = region_name or settings.AWS_DEFAULT_REGION | ||
self.profile_name = profile_name | ||
|
||
def refreshable_credentials(self): | ||
log.info("Loading AWS credentials") | ||
if not self.assume_role_name: | ||
# boto3 refreshes the credentials automatically if no role is assumed | ||
return self.get_default_credentials() | ||
|
||
return credentials.RefreshableCredentials.create_from_metadata( | ||
metadata=self.get_sts_credentials(), | ||
refresh_using=self.get_sts_credentials, | ||
method="sts-assume-role", | ||
) | ||
|
||
def get_sts_credentials(self) -> dict: | ||
log.info("Getting credentials using STS") | ||
boto3_ini_session = boto3.Session(region_name=settings.AWS_DEFAULT_REGION) | ||
sts = boto3_ini_session.client("sts") | ||
response = sts.assume_role( | ||
RoleArn=self.assume_role_name, | ||
RoleSessionName=f"analytical-platform-ui-{settings.ENV}", | ||
DurationSeconds=TTL, | ||
) | ||
return { | ||
"access_key": response["Credentials"]["AccessKeyId"], | ||
"secret_key": response["Credentials"]["SecretAccessKey"], | ||
"token": response["Credentials"]["SessionToken"], | ||
"expiry_time": response["Credentials"]["Expiration"].isoformat(), | ||
} | ||
|
||
def get_default_credentials(self) -> credentials.Credentials: | ||
log.info("Getting credentials using default boto3 method") | ||
boto3_ini_session = boto3.Session( | ||
region_name=self.region_name, profile_name=self.profile_name | ||
) | ||
return boto3_ini_session.get_credentials() | ||
|
||
def get_boto3_session(self) -> boto3.Session: | ||
log.info("Creating a new boto3 session") | ||
botocore_session = get_session() | ||
botocore_session.set_config_variable("region", self.region_name) | ||
botocore_session._credentials = self.refreshable_credentials() | ||
return boto3.Session(botocore_session=botocore_session) | ||
|
||
|
||
class SingletonMeta(type): | ||
_instances: dict = {} | ||
|
||
def __call__(cls, *args, **kwargs): | ||
""" | ||
Possible changes to the value of the `__init__` argument do not affect | ||
the returned instance. | ||
""" | ||
if cls in cls._instances: | ||
return cls._instances[cls] | ||
|
||
instance = super().__call__(*args, **kwargs) | ||
cls._instances[cls] = instance | ||
return instance | ||
|
||
|
||
class AWSCredentialSessionSet(metaclass=SingletonMeta): | ||
def __init__(self): | ||
self.credential_sessions = {} | ||
|
||
def get_or_create_session( | ||
self, | ||
profile_name: str | None = None, | ||
assume_role_name: str | None = None, | ||
region_name: str | None = None, | ||
) -> boto3.Session: | ||
credential_session_key = "{}_{}_{}".format(profile_name, assume_role_name, region_name) | ||
if credential_session_key in self.credential_sessions: | ||
log.info(f"Returning existing session for {credential_session_key}") | ||
return self.credential_sessions[credential_session_key] | ||
|
||
log.warn(f"(for monitoring purpose) Initialising session ({credential_session_key})") | ||
self.credential_sessions[credential_session_key] = BotoSession( | ||
region_name=region_name, | ||
profile_name=profile_name, | ||
assume_role_name=assume_role_name, | ||
).get_boto3_session() | ||
return self.credential_sessions[credential_session_key] |
Oops, something went wrong.