diff --git a/physionet-django/user/forms.py b/physionet-django/user/forms.py index 5ca6e18c13..5623866419 100644 --- a/physionet-django/user/forms.py +++ b/physionet-django/user/forms.py @@ -1,4 +1,5 @@ import datetime +import json import time from django import forms @@ -14,6 +15,11 @@ from django.utils.html import mark_safe from django.utils.translation import gettext_lazy from physionet.utility import validate_pdf_file_type +from user.awsverification import ( + AWSVerificationFailed, + get_aws_verification_command, + check_aws_verification_url, +) from user.models import ( AssociatedEmail, CloudInformation, @@ -28,8 +34,14 @@ ) from user.trainingreport import TrainingCertificateError, find_training_report_url from user.userfiles import UserFiles -from user.validators import UsernameValidator, validate_name, validate_training_file_size -from user.validators import validate_institutional_email +from user.validators import ( + UsernameValidator, + validate_aws_id, + validate_aws_userid, + validate_institutional_email, + validate_name, + validate_training_file_size, +) from user.widgets import ProfilePhotoInput from django.db.models import OuterRef, Exists @@ -675,10 +687,9 @@ class CloudForm(forms.ModelForm): """ class Meta: model = CloudInformation - fields = ('gcp_email','aws_id',) + fields = ('gcp_email',) labels = { 'gcp_email': 'Google (Email)', - 'aws_id': 'Amazon (ID)', } def __init__(self, *args, **kwargs): # Email choices are those belonging to a user @@ -688,6 +699,79 @@ def __init__(self, *args, **kwargs): self.fields['gcp_email'].required = False +class AWSIdentityForm(forms.Form): + aws_identity = forms.CharField( + label="Caller identity", max_length=2000, + widget=forms.Textarea(attrs={ + 'rows': 5, + 'placeholder': '{"UserId": "...", "Account": "...", "Arn": "..."}' + }) + ) + + def clean(self): + try: + identity = super().clean()['aws_identity'] + data = json.loads(identity) + aws_account = data['Account'] + aws_userid = data['UserId'] + except (TypeError, KeyError, ValueError): + raise forms.ValidationError( + mark_safe("Copy and paste the output of the " + "aws sts get-caller-identity command.")) + validate_aws_id(aws_account) + validate_aws_userid(aws_userid) + return { + 'aws_account': aws_account, + 'aws_userid': aws_userid + } + + +class AWSVerificationForm(forms.Form): + signed_url = forms.CharField( + label="Signed URL", max_length=2000, + widget=forms.Textarea(attrs={ + 'rows': 6, + 'placeholder': 'https://...' + }) + ) + + def __init__(self, user, site_domain, aws_account, aws_userid, + **kwargs): + super().__init__(**kwargs) + self.user = user + self.site_domain = site_domain + self.aws_account = aws_account + self.aws_userid = aws_userid + + def aws_verification_command(self): + return get_aws_verification_command(site_domain=self.site_domain, + user_email=self.user.email, + aws_account=self.aws_account, + aws_userid=self.aws_userid) + + def clean(self): + data = super().clean() + signed_url = data['signed_url'].strip() + try: + info = check_aws_verification_url(site_domain=self.site_domain, + user_email=self.user.email, + signed_url=signed_url) + except AWSVerificationFailed: + raise forms.ValidationError("Invalid verification URL") + + validate_aws_id(info['account']) + validate_aws_userid(info['userid']) + data.update(info) + return data + + def save(self): + cloud_info = CloudInformation.objects.get_or_create(user=self.user)[0] + cloud_info.aws_id = self.cleaned_data['account'] + cloud_info.aws_userid = self.cleaned_data['userid'] + cloud_info.aws_verification_datetime = timezone.now() + cloud_info.save() + + # class ActivationForm(forms.ModelForm): class ActivationForm(forms.Form): """A form for creating new users. Includes all the required diff --git a/physionet-django/user/templates/user/edit_cloud.html b/physionet-django/user/templates/user/edit_cloud.html index 9056e0eb40..0928da94e5 100644 --- a/physionet-django/user/templates/user/edit_cloud.html +++ b/physionet-django/user/templates/user/edit_cloud.html @@ -12,12 +12,58 @@

Edit Cloud Details

  • Follow the instructions to request access. If instructions for cloud access are not shown, the project is not currently available on the cloud.
  • +

    Google Cloud Platform

    +

    {% csrf_token %} - {% include "inline_form_snippet.html" %} - + {% include "inline_form_snippet.html" with form=gcp_form %} +
    +{% if aws_verification_available %} +

    Amazon Web Services

    +
    + {% csrf_token %} + {% if user.cloud_information.aws_verification_datetime %} +
    + +
    + {% else %} +
    +

    To link your Amazon Web Services account using the + AWS Command Line Interface: +

    +
      +
    1. + Open a terminal and run the following command: +
      aws sts get-caller-identity
      +
    2. +
    3. + Copy and paste the output into the box below. + {% include "form_snippet_no_labels.html" with form=aws_form %} +
    4. +
    + + {% endif %} +
    +{% endif %} {% endblock %} diff --git a/physionet-django/user/templates/user/edit_cloud_aws.html b/physionet-django/user/templates/user/edit_cloud_aws.html new file mode 100644 index 0000000000..0985807f9b --- /dev/null +++ b/physionet-django/user/templates/user/edit_cloud_aws.html @@ -0,0 +1,28 @@ +{% extends "user/settings.html" %} + +{% block title %}Verify AWS Account{% endblock %} + +{% block main_content %} +

    Verify AWS Account

    +
    + +
    + {% csrf_token %} +

    + To verify your Amazon Web Services account using the + AWS Command Line Interface: +

    +
      +
    1. + Open a terminal and run the following command (one line): +
      {{ form.aws_verification_command }}
      +
    2. +
    3. + Copy and paste the output into the box below. + {% include "form_snippet_no_labels.html" %} +
    4. +
    + +
    +{% endblock %} diff --git a/physionet-django/user/urls.py b/physionet-django/user/urls.py index 8723f37e62..0fae8e5b58 100644 --- a/physionet-django/user/urls.py +++ b/physionet-django/user/urls.py @@ -15,6 +15,7 @@ path("settings/emails/", views.edit_emails, name="edit_emails"), path("settings/username/", views.edit_username, name="edit_username"), path("settings/cloud/", views.edit_cloud, name="edit_cloud"), + path("settings/cloud/aws/", views.edit_cloud_aws, name="edit_cloud_aws"), path("settings/orcid/", views.edit_orcid, name="edit_orcid"), path("authorcid/", views.auth_orcid, name="auth_orcid"), path( diff --git a/physionet-django/user/views.py b/physionet-django/user/views.py index 05628b8699..dc1d2215b6 100644 --- a/physionet-django/user/views.py +++ b/physionet-django/user/views.py @@ -44,6 +44,7 @@ from project.models import Author, DUASignature, DUA, PublishedProject from requests_oauthlib import OAuth2Session from user import forms, validators +from user.awsverification import aws_verification_available from user.models import ( AssociatedEmail, CodeOfConduct, @@ -914,7 +915,7 @@ def edit_cloud(request): user = request.user cloud_info = CloudInformation.objects.get_or_create(user=user)[0] form = forms.CloudForm(instance=cloud_info) - if request.method == 'POST': + if request.method == 'POST' and 'save-gcp' in request.POST: form = forms.CloudForm(instance=cloud_info, data=request.POST) if form.is_valid(): form.save() @@ -922,7 +923,61 @@ def edit_cloud(request): else: messages.error(request, 'Invalid submission. See errors below.') - return render(request, 'user/edit_cloud.html', {'form':form, 'user':user}) + if request.method == 'POST' and 'delete-aws' in request.POST: + cloud_info.aws_account = None + cloud_info.aws_userid = None + cloud_info.aws_verification_datetime = None + cloud_info.save() + + aws_form = forms.AWSIdentityForm() + if request.method == 'POST' and 'save-aws' in request.POST: + aws_form = forms.AWSIdentityForm(data=request.POST) + if aws_form.is_valid(): + request.session['new_aws_account'] = \ + aws_form.cleaned_data['aws_account'] + request.session['new_aws_userid'] = \ + aws_form.cleaned_data['aws_userid'] + return redirect('edit_cloud_aws') + else: + messages.error(request, 'Invalid submission. See errors below.') + + return render(request, 'user/edit_cloud.html', { + 'user': user, + 'gcp_form': form, + 'aws_form': aws_form, + 'aws_verification_available': aws_verification_available(), + }) + + +@login_required +def edit_cloud_aws(request): + if not aws_verification_available(): + return redirect('edit_cloud') + + site_domain = get_current_site(request).domain + aws_account = request.session.get('new_aws_account', '') + aws_userid = request.session.get('new_aws_userid', '') + form = forms.AWSVerificationForm(user=request.user, + site_domain=site_domain, + aws_account=aws_account, + aws_userid=aws_userid) + if request.method == 'POST' and 'signed_url' in request.POST: + form = forms.AWSVerificationForm(user=request.user, + site_domain=site_domain, + aws_account=aws_account, + aws_userid=aws_userid, + data=request.POST) + if form.is_valid(): + form.save() + request.session.pop('new_aws_account') + request.session.pop('new_aws_userid') + messages.success(request, 'Your cloud information has been saved.') + return redirect('edit_cloud') + else: + messages.error(request, 'Invalid submission. See errors below.') + + return render(request, 'user/edit_cloud_aws.html', {'form': form}) + @login_required def view_agreements(request):