Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🛂 Add statement for cross account assumption #1392

Merged
merged 16 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion controlpanel/api/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,13 +501,31 @@ def oidc_provider_statement(self):
)
return json.loads(statement)

@property
def xacct_trust_statement(self):
"""
Builds an assume role statement for a Cloud Platform IAM role
"""
statement = render_to_string(
template_name="assume_roles/cloud_platform_xacct.json",
context={"app_role": self.app.cloud_platform_role_arn},
)
return json.loads(statement)

def create_iam_role(self):
statement = self._get_statement()
assume_role_policy = deepcopy(BASE_ASSUME_ROLE_POLICY)
assume_role_policy["Statement"].append(self.oidc_provider_statement)
assume_role_policy["Statement"].append(statement)
self.aws_role_service.create_role(self.iam_role_name, assume_role_policy)
for env in self.get_deployment_envs():
self._create_secrets(env_name=env)

def _get_statement(self):
if self.app.cloud_platform_role_arn:
return self.xacct_trust_statement

return self.oidc_provider_statement

def grant_bucket_access(self, bucket_arn, access_level, path_arns):
self.aws_role_service.grant_bucket_access(
self.iam_role_name, bucket_arn, access_level, path_arns
Expand Down
24 changes: 24 additions & 0 deletions controlpanel/api/migrations/0047_app_cloud_platform_role_arn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2024-11-27 15:46

# Third-party
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0046_alter_user_options"),
]

operations = [
migrations.AddField(
model_name="app",
name="cloud_platform_role_arn",
field=models.CharField(
default=None,
help_text="The cloud platform arn for the app",
max_length=130,
null=True,
),
),
]
7 changes: 7 additions & 0 deletions controlpanel/api/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


class App(TimeStampedModel):

name = models.CharField(max_length=100, blank=False)
description = models.TextField(blank=True)
slug = AutoSlugField(populate_from="_repo_name", slugify_function=s3_slugify)
Expand All @@ -32,6 +33,12 @@ class App(TimeStampedModel):
related_query_name="app",
blank=True,
)
cloud_platform_role_arn = models.CharField(
help_text="The cloud platform arn for the app",
max_length=130,
null=True,
default=None,
)
res_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
is_bedrock_enabled = models.BooleanField(default=False)
is_textract_enabled = models.BooleanField(default=False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Sid": "AllowCloudPlatformCrossAccountIAM",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {
"AWS": "{{ app_role }}"
},
"Condition": {}
}
8 changes: 8 additions & 0 deletions controlpanel/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ def __call__(self, value):
"can only contain alphanumeric, underscores and hyphens)",
)

validate_aws_role_arn = RegexValidator(
regex=r"^arn:aws:iam::[0-9]{12}:role/[a-zA-Z0-9-_]+$",
message=(
"ARN is invalid. Check AWS ARN format "
"(for example, 'arn:aws:iam::123456789012:role/role_name')"
),
)


def validate_github_repository_url(value):
github_base_url = "https://github.com/"
Expand Down
4 changes: 4 additions & 0 deletions controlpanel/frontend/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ class CreateAppForm(forms.Form):
required=False,
)
namespace = forms.CharField(required=True, max_length=63)
allow_cloud_platform_assume_role = forms.BooleanField(initial=False, required=False)
cloud_platform_role_arn = forms.CharField(
required=False, max_length=130, validators=[validators.validate_aws_role_arn]
)

def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
Expand Down
42 changes: 40 additions & 2 deletions controlpanel/frontend/jinja2/webapp-create.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
}) }}
{% endif %}

<input type="text" class="govuk-input" id="display_result_repo" name="repo_url" required />
<input type="text" class="govuk-input" id="display_result_repo" name="repo_url" {% if form.repo_url.value() %} value="{{ form.repo_url.value() }}" {% endif %} required />

</div>
<div class="govuk-form-group" id="container-element">
Expand All @@ -90,7 +90,7 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
}) }}
{% endif %}
<span class="govuk-hint">Enter namespace with the -env suffix removed</span>
<input type="text" class="govuk-input" id="id_namespace" name="namespace" required />
<input type="text" class="govuk-input" id="id_namespace" name="namespace" {% if form.namespace.value() %} value="{{ form.namespace.value() }}" {% endif %} required />

</div>

Expand Down Expand Up @@ -128,6 +128,44 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
]
}) }}

{% if show_cloud_platform_assume_role %}
{{ govukCheckboxes({
"name": "allow_cloud_platform_assume_role",
"fieldset": {
"legend": {
"text": "Access via Cloud Platform",
"classes": "govuk-fieldset__legend--m",
},
},
"hint": {
"text": "Allows Cloud Platform role to assume role in the Analytical Platform"
},
"items": [
{
"value": "True",
"text": "Access via Cloud Platform",
"checked": form.allow_cloud_platform_assume_role.value() == True
}
]
}) }}

<div class="govuk-grid-row checkbox-subform" data-show-if-selected="True">
<div class="govuk-grid-column-full">
<br/>
{{ govukInput({
"name": "cloud_platform_role_arn",
"classes": "govuk-!-width-one-half",
"label": {
"text": "Cloud Platform Role ARN",
"classes": "govuk-label--s",
},
"errorMessage": {"text": form.errors.get("cloud_platform_role_arn")} if form.errors.get("cloud_platform_role_arn") else {},
"value": form.cloud_platform_role_arn.value()
}) }}
</div>
</div>
{% endif %}

<div class="govuk-form-group">
<button class="govuk-button">Register app</button>
</div>
Expand Down
16 changes: 16 additions & 0 deletions controlpanel/frontend/views/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,28 @@ def get_success_url(self):

def form_valid(self, form):
try:
assume_role = form.cleaned_data.get("allow_cloud_platform_assume_role")
role_arn = form.cleaned_data.get("cloud_platform_role_arn")
if assume_role and not role_arn:
form.add_error("cloud_platform_role_arn", "Role ARN is required")
return FormMixin.form_invalid(self, form)
jamesstottmoj marked this conversation as resolved.
Show resolved Hide resolved

if not assume_role and role_arn:
form.cleaned_data.pop("cloud_platform_role_arn")

self.object = AppManager().register_app(self.request.user, form.cleaned_data)
except Exception as ex:
form.add_error("repo_url", str(ex))
return FormMixin.form_invalid(self, form)
return FormMixin.form_valid(self, form)

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["show_cloud_platform_assume_role"] = (
settings.features.cloud_platform_assume_role.enabled
)
return context


class UpdateAppAuth0Connections(OIDCLoginRequiredMixin, PermissionRequiredMixin, UpdateView):

Expand Down
2 changes: 2 additions & 0 deletions controlpanel/frontend/views/apps_mng.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ class AppManager:
def register_app(self, user, app_data):
repo_url = app_data["repo_url"]
_, name = repo_url.rsplit("/", 1)
cloud_platform_role_arn = app_data.get("cloud_platform_role_arn", None)

# Create app and all the related sources
new_app = self._create_app(
name=name,
repo_url=repo_url,
current_user=user,
namespace=app_data["namespace"],
cloud_platform_role_arn=cloud_platform_role_arn,
)
self._add_app_to_users(new_app, user)
self._create_or_link_datasource(new_app, user, app_data)
Expand Down
5 changes: 5 additions & 0 deletions settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ enabled_features:
_HOST_dev: true
_HOST_test: true
_HOST_prod: true
cloud_platform_assume_role:
_DEFAULT: false
_HOST_dev: false
jamesstottmoj marked this conversation as resolved.
Show resolved Hide resolved
_HOST_prod: false
_HOST_alpha: false

AWS_SERVICE_URL:
_HOST_dev: "https://aws.services.dev.analytical-platform.service.justice.gov.uk"
Expand Down
20 changes: 20 additions & 0 deletions tests/frontend/views/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,26 @@ def test_app_settings_permission(client, app, users, repos_with_auth, user, can_
assert "Edit" not in auth_item_ui.text


def test_register_app_with_xacct_policy(client, users):
test_app_name = "test_app_with_xacct_policy"
assert App.objects.filter(name=test_app_name).count() == 0
client.force_login(users["superuser"])
data = dict(
repo_url=f"https://github.com/ministryofjustice/{test_app_name}",
namespace="test-app-namespace",
connect_bucket="later",
allow_cloud_platform_assume_role=True,
cloud_platform_role_arn="arn:aws:iam::123456789012:role/test_role",
)
response = client.post(reverse("create-app"), data)

assert response.status_code == 302
assert App.objects.filter(name=test_app_name).count() == 1
created_app = App.objects.filter(name=test_app_name).first()
assert created_app.cloud_platform_role_arn == "arn:aws:iam::123456789012:role/test_role"
assert response.url == reverse("manage-app", kwargs={"pk": created_app.pk})


def test_register_app_with_creating_datasource(client, users):
test_app_name = "test_app_with_creating_datasource"
test_bucket_name = "test-bucket"
Expand Down
Loading