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

Simplify the register app form #1228

Merged
merged 12 commits into from
Jan 30, 2024
23 changes: 22 additions & 1 deletion controlpanel/api/cluster.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Standard library
import json
import os
import secrets
from copy import deepcopy
Expand All @@ -9,6 +10,7 @@
import structlog
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.template.loader import render_to_string

# First-party/Local
from controlpanel.api import auth0, helm
Expand Down Expand Up @@ -483,8 +485,27 @@ def _add_missing_mandatory_vars(self, env_name, app_env_vars, created_var_names)
def iam_role_name(self):
return f"{settings.ENV}_app_{self.app.slug}"

@property
def oidc_provider_statement(self):
"""
Builds the assume role statement for the OIDC provider, currently Cloud Platform
"""
statement = render_to_string(
template_name="assume_roles/cloud_platform_oidc.json",
context={
"identity_provider": settings.OIDC_APP_EKS_PROVIDER,
"identity_provider_arn": iam_arn(
f"oidc-provider/{settings.OIDC_APP_EKS_PROVIDER}"
),
"app_name": self.app.slug,
}
)
return json.loads(statement)

def create_iam_role(self):
self.aws_role_service.create_role(self.iam_role_name, BASE_ASSUME_ROLE_POLICY)
assume_role_policy = deepcopy(BASE_ASSUME_ROLE_POLICY)
assume_role_policy["Statement"].append(self.oidc_provider_statement)
self.aws_role_service.create_role(self.iam_role_name, assume_role_policy)

def grant_bucket_access(self, bucket_arn, access_level, path_arns):
self.aws_role_service.grant_bucket_access(
Expand Down
14 changes: 12 additions & 2 deletions controlpanel/api/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
# First-party/Local
from controlpanel.utils import encrypt_data_by_using_public_key


log = structlog.getLogger(__name__)


class GithubAPIException(Exception):
pass


class RepositoryNotFound(GithubAPIException):
status_code = 404


def extract_repo_info_from_url(repo_url):
url_parts = repo_url.split("/")
if len(url_parts) < 4:
Expand Down Expand Up @@ -62,6 +65,11 @@ def get_repository(self, repo_name: str):
self._get_repo_api_url(repo_name=repo_name, api_call=None),
headers=self.headers,
)
if response.status_code == 404:
raise RepositoryNotFound(
f"Repository '{repo_name}' not found, it may be private"
)

return self._process_response(response)

def read_app_deploy_info(self, repo_name: str, deploy_file="deploy.json"):
Expand Down Expand Up @@ -156,7 +164,9 @@ def create_or_update_repo_env_secret(
if not public_key:
public_key = self.get_repo_env_public_key(repo_name, env_name)
secret_data = {
"encrypted_value": encrypt_data_by_using_public_key(public_key["key"], secret_value),
"encrypted_value": encrypt_data_by_using_public_key(
public_key["key"], secret_value
),
"key_id": public_key["key_id"],
}
repo_secret_url = self._get_repo_env_api_url(
Expand Down
11 changes: 6 additions & 5 deletions controlpanel/api/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,6 @@ def delete_customer_by_email(self, email, group_id):
def status(self):
return "Deployed"

def deployment_envs(self, github_token):
return cluster.App(self, github_token).get_deployment_envs()

def delete(self, *args, **kwargs):
github_api_token = None
if "github_api_token" in kwargs:
Expand Down Expand Up @@ -319,8 +316,12 @@ class DeleteCustomerError(Exception):

@receiver(post_save, sender=App)
def trigger_app_create_related_messages(sender, instance, created, **kwargs):
if created:
tasks.AppCreateRole(instance, instance.current_user).create_task()
if not created:
return
tasks.AppCreateRole(instance, instance.current_user).create_task()

# TODO this could be removed as part of a review of task queue usage
if instance.deployment_envs:
tasks.AppCreateAuth(instance, instance.current_user, extra_data=dict(
deployment_envs=instance.deployment_envs,
disable_authentication=instance.disable_authentication,
Expand Down
4 changes: 3 additions & 1 deletion controlpanel/api/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def is_app_admin(user, obj):


add_perm("api.list_app", is_authenticated)
add_perm("api.create_app", is_authenticated & is_superuser)
add_perm("api.create_app", is_authenticated)
add_perm("api.retrieve_app", is_authenticated & is_app_admin)
add_perm("api.update_app", is_authenticated & is_superuser)
add_perm("api.destroy_app", is_authenticated & is_superuser)
Expand Down Expand Up @@ -187,6 +187,8 @@ def is_self(user, other):
add_perm("api.update_ip_allowlists", is_authenticated & is_superuser)
add_perm("api.destroy_ip_allowlists", is_authenticated & is_superuser)

add_perm("api.create_connections", is_authenticated & is_superuser)


@predicate
def is_owner(user, obj):
Expand Down
8 changes: 7 additions & 1 deletion controlpanel/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# Third-party
from django.conf import settings
from django.core.exceptions import ValidationError
from rest_framework import serializers

# First-party/Local
Expand All @@ -18,6 +19,7 @@
UserApp,
UserS3Bucket,
)
from controlpanel.api import validators


class AppS3BucketSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -177,6 +179,10 @@ class Meta:

def validate_repo_url(self, value):
"""Normalise repo URLs by removing trailing .git"""
try:
validators.validate_github_repository_url(value)
except ValidationError as e:
raise serializers.ValidationError(e.message)
return value.rsplit(".git", 1)[0]


Expand Down Expand Up @@ -362,7 +368,7 @@ class AppAuthSettingsSerializer(serializers.BaseSerializer):
"edit_link": "update-app-ip-allowlists"
},
cluster.App.AUTH0_CONNECTIONS: {
"permission_flag": "api.create_app",
"permission_flag": "api.create_connections",
"edit_link": "update-auth0-connections"
}
}
Expand Down
1 change: 0 additions & 1 deletion controlpanel/api/tasks/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def _get_args_list(self):
self.extra_data.get('deployment_envs'),
self.extra_data.get('disable_authentication'),
self.extra_data.get('connections'),
# self.extra_data.get('has_ip_ranges') # NOT USED, REMOVE IT?
]

@property
Expand Down
17 changes: 17 additions & 0 deletions controlpanel/api/templates/assume_roles/cloud_platform_oidc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Sid": "AllowCloudPlatformOIDCProvider",
"Effect": "Allow",
"Principal": {
"Federated": "{{ identity_provider_arn }}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"{{ identity_provider }}:aud": "sts.amazonaws.com",
"{{ identity_provider }}:sub": [
"system:serviceaccount:data-platform-app-{{ app_name }}-dev:data-platform-app-{{ app_name }}-dev-sa",
"system:serviceaccount:data-platform-app-{{ app_name }}-prod:data-platform-app-{{ app_name }}-prod-sa"
]
}
}
}
11 changes: 10 additions & 1 deletion controlpanel/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,18 @@ def validate_github_repository_url(value):
if not value.startswith(github_base_url):
raise ValidationError("Must be a Github hosted repository")

if value[-1] == "/":
raise ValidationError("Repository URL should not include a trailing slash")

repo_name = value[len(github_base_url) :] # noqa: E203
org, _ = repo_name.split("/", 1)
repo_parts = list(filter(None, repo_name.split("/")))
if len(repo_parts) > 2:
raise ValidationError("Repository URL should not include subdirectories")

if len(repo_parts) < 2:
raise ValidationError("Repository URL is missing the repository name")

org = repo_parts[0]
if org not in settings.GITHUB_ORGS:
orgs = ", ".join(settings.GITHUB_ORGS)
raise ValidationError(
Expand Down
39 changes: 10 additions & 29 deletions controlpanel/frontend/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

# Third-party
from django import forms
from django.conf import settings
from django.contrib.postgres.forms import SimpleArrayField
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator, validate_email
Expand All @@ -12,7 +11,11 @@
from controlpanel.api import validators
from controlpanel.api.cluster import AWSRoleCategory
from controlpanel.api.cluster import S3Folder as ClusterS3Folder
from controlpanel.api.github import GithubAPI, extract_repo_info_from_url
from controlpanel.api.github import (
GithubAPI,
RepositoryNotFound,
extract_repo_info_from_url,
)
from controlpanel.api.models import App, S3Bucket, Tool, User
from controlpanel.api.models.access_to_s3bucket import S3BUCKET_PATH_REGEX
from controlpanel.api.models.iam_managed_policy import POLICY_NAME_REGEX
Expand Down Expand Up @@ -115,11 +118,6 @@ def _check_inputs_for_custom_connection(self, cleaned_data):

class CreateAppForm(AppAuth0Form):

org_names = forms.ChoiceField(
required=True,
choices=list(zip(settings.GITHUB_ORGS, settings.GITHUB_ORGS)),
)

repo_url = forms.CharField(
max_length=512,
validators=[
Expand Down Expand Up @@ -151,20 +149,6 @@ class CreateAppForm(AppAuth0Form):
required=False,
)

disable_authentication = forms.BooleanField(required=False)

app_ip_allowlists = forms.ModelMultipleChoiceField(
required=False, queryset=IPAllowlist.objects.filter(deleted=False)
)

deployment_envs = DynamicMultiChoiceField(required=False)

def __init__(self, *args, **kwargs):
super(CreateAppForm, self).__init__(*args, **kwargs)
self.fields["app_ip_allowlists"].initial = IPAllowlist.objects.filter(
is_recommended=True
)

def clean(self):
cleaned_data = super().clean()
connect_data_source = cleaned_data["connect_bucket"]
Expand All @@ -188,19 +172,16 @@ def clean(self):
if connect_data_source == "existing" and not existing_datasource:
self.add_error("existing_datasource_id", "This field is required.")

cleaned_data["auth0_connections"] = self._check_inputs_for_custom_connection(
cleaned_data
)

return cleaned_data

def clean_repo_url(self):
repo_url = self.cleaned_data["repo_url"]
org_name, repo_name = extract_repo_info_from_url(repo_url)
repo = GithubAPI(
self.request.user.github_api_token, github_org=org_name
).get_repository(repo_name)
if repo is None:
try:
GithubAPI(
self.request.user.github_api_token, github_org=org_name
).get_repository(repo_name)
except RepositoryNotFound:
raise ValidationError(
"Github repository not found - it may be private",
)
Expand Down
Loading
Loading