Skip to content

Commit

Permalink
[WIP] Add views and forms for verifying AWS user identity.
Browse files Browse the repository at this point in the history
In order for a person to verify their AWS identity, they need to
provide a digital signature, in the form of a signed URL that includes
their account ID and user ID in the path.  We further require the URL
to include the domain name of the site, and the user's primary email
address, to prevent misuse.

This signed URL can be generated using the AWS CLI.  However, the URL
must be exactly correct; if it is wrong, it is difficult to tell why.
In order to hopefully avoid confusion, we first ask the person to run
'aws sts get-caller-identity'; based on that, we tell them the exact
'aws s3 presign' command they need to run.
  • Loading branch information
Benjamin Moody committed Oct 30, 2023
1 parent e655251 commit b177957
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 8 deletions.
92 changes: 88 additions & 4 deletions physionet-django/user/forms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import json
import time

from django import forms
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 "
"<code>aws sts get-caller-identity</code> 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
Expand Down
50 changes: 48 additions & 2 deletions physionet-django/user/templates/user/edit_cloud.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,58 @@ <h1 class="form-signin-heading">Edit Cloud Details</h1>
<li>Follow the instructions to request access. If instructions for cloud access are not shown, the project is not currently available on the cloud.</li>
</ul>

<h2 id="gcp">Google Cloud Platform</h2>

<form action="{% url 'edit_cloud' %}" method="post" class="form-signin no-pd">
<hr>
{% csrf_token %}
{% include "inline_form_snippet.html" %}
<button class="btn btn-primary btn-custom btn-rsp" type="submit">Save</button>
{% include "inline_form_snippet.html" with form=gcp_form %}
<button class="btn btn-primary btn-custom btn-rsp"
type="submit" name="save-gcp">Save</button>
</form>

{% if aws_verification_available %}
<h2 id="aws">Amazon Web Services</h2>
<form action="" method="post">
{% csrf_token %}
{% if user.cloud_information.aws_verification_datetime %}
<div class="card">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<button class="float-right btn btn-sm btn-danger"
type="submit" title="Remove" name="delete-aws">
<span class="fas fa-trash-alt">
<span class="visually-hidden">Remove</span>
</span>
</button>
<dl class="row">
<dt class="col-3">Account</dt>
<dd class="col-9">{{ user.cloud_information.aws_id }}</dd>
<dt class="col-3">User ID</dt>
<dd class="col-9">{{ user.cloud_information.aws_userid }}</dd>
</dl>
</li>
</ul>
</div>
{% else %}
<hr>
<p>To link your Amazon Web Services account using the
<a href="https://aws.amazon.com/cli/"><abbr title="Amazon Web Services">AWS</abbr> Command Line Interface</a>:
</p>
<ol>
<li>
Open a terminal and run the following command:
<pre><code>aws sts get-caller-identity</code></pre>
</li>
<li>
Copy and paste the output into the box below.
{% include "form_snippet_no_labels.html" with form=aws_form %}
</li>
</ol>
<button class="btn btn-primary btn-custom btn-rsp"
type="submit" name="save-aws">Submit</button>
{% endif %}
</form>
{% endif %}

{% endblock %}
28 changes: 28 additions & 0 deletions physionet-django/user/templates/user/edit_cloud_aws.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends "user/settings.html" %}

{% block title %}Verify AWS Account{% endblock %}

{% block main_content %}
<h1 class="form-signin-heading">Verify <abbr title="Amazon Web Services">AWS</abbr> Account</h1>
<hr>

<form action="" method="post">
{% csrf_token %}
<p>
To verify your Amazon Web Services account using the
<a href="https://aws.amazon.com/cli/"><abbr title="Amazon Web Services">AWS</abbr> Command Line Interface</a>:
</p>
<ol>
<li>
Open a terminal and run the following command (one line):
<pre><code>{{ form.aws_verification_command }}</code></pre>
</li>
<li>
Copy and paste the output into the box below.
{% include "form_snippet_no_labels.html" %}
</li>
</ol>
<button class="btn btn-primary btn-custom btn-rsp"
type="submit">Save</button>
</form>
{% endblock %}
1 change: 1 addition & 0 deletions physionet-django/user/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
59 changes: 57 additions & 2 deletions physionet-django/user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -914,15 +915,69 @@ 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()
messages.success(request, 'Your cloud information has been saved.')
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):
Expand Down

0 comments on commit b177957

Please sign in to comment.