Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ DATA_UPLOAD_MAX_MEMORY_SIZE=2621440
# (typically representing the main web interface or API client).
OAUTH_CLIENT_APP_NAME=local_web_client

# OIDC Provider. Generate the signing key with:
# python manage.py generate_oidc_rsa_key --output oidc_key.pem
OIDC_RSA_KEY_FILE=
# OIDC_RSA_PRIVATE_KEY=
# Comma-separated paths to rotated-out keys; still published in JWKS during
# the rotation overlap window so RPs can verify tokens signed under them.
# OIDC_RSA_INACTIVE_KEY_FILES=
# Required when a key is configured. Becomes the 'iss' claim in every ID token.
OIDC_ISS_ENDPOINT=http://localhost:8000

# Geographic restriction
# **IMPORTANT NOTE:
# Geographic restrictions are only applied to projects where the `georestricted` flag is `True`.
Expand Down
2 changes: 2 additions & 0 deletions physionet-django/console/navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ def get_menu_items(self, request):
NavLink(_('Administrators'), 'users', view_args=['admin']),
]),

NavLink(_('Partners'), 'partner_list', 'id-card'),

NavLink(_('Featured Content'), 'featured_content', 'star'),

NavSubmenu(_('Guidelines'), 'guidelines', 'book', [
Expand Down
96 changes: 96 additions & 0 deletions physionet-django/console/templates/console/partners/detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{% extends "console/base_console.html" %}

{% block title %}Partner: {{ partner.organization_name }}{% endblock %}

{% block content %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>{{ partner.organization_name }}</h2>
<a href="{% url 'partner_list' %}" class="btn btn-link">Back to list</a>
</div>

{% if one_time_secret %}
<div class="alert alert-warning">
<strong>Client secret (shown once)</strong>
<p>Copy this now. It will not be shown again. Store it securely.</p>
<pre id="one-time-secret">{{ one_time_secret }}</pre>
<small>Hint: select the value above and copy to clipboard.</small>
</div>
{% endif %}

<div class="card mb-3">
<div class="card-header"><strong>Organisation</strong></div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">Name</dt><dd class="col-sm-9">{{ partner.organization_name }}</dd>
<dt class="col-sm-3">Contact name</dt><dd class="col-sm-9">{{ partner.contact_name|default:"-" }}</dd>
<dt class="col-sm-3">Contact email</dt><dd class="col-sm-9">{{ partner.contact_email|default:"-" }}</dd>
<dt class="col-sm-3">Agreement signed</dt><dd class="col-sm-9">{{ partner.agreement_signed_date|default:"-" }}</dd>
<dt class="col-sm-3">Requires PKCE</dt><dd class="col-sm-9">{{ partner.requires_pkce|yesno:"Yes,No" }}</dd>
</dl>
<a href="{% url 'partner_edit' partner.pk %}" class="btn btn-sm btn-outline-primary mt-2">Edit organisation</a>
</div>
</div>

<div class="card mb-3">
<div class="card-header"><strong>Credentials</strong></div>
<div class="card-body">
<p><strong>Client ID:</strong> <code>{{ partner.application.client_id }}</code></p>
<form method="post" action="{% url 'partner_rotate_secret' partner.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-warning"
onclick="return confirm('Rotating will invalidate the current secret and revoke active access tokens. Continue?');">
Rotate secret
</button>
</form>
</div>
</div>

<div class="card mb-3">
<div class="card-header"><strong>Allowed scopes</strong></div>
<div class="card-body">
{% if partner.allowed_scopes %}
<ul class="mb-2">
{% for scope in partner.allowed_scopes %}<li><code>{{ scope }}</code></li>{% endfor %}
</ul>
{% else %}
<p class="text-muted">No scope allow-list set (legacy: all configured scopes allowed).</p>
{% endif %}
<a href="{% url 'partner_scopes' partner.pk %}" class="btn btn-sm btn-outline-primary">Edit scopes</a>
</div>
</div>

<div class="card mb-3">
<div class="card-header"><strong>Redirect URIs</strong></div>
<div class="card-body">
<p><strong>Login (redirect_uris):</strong></p>
<pre>{{ partner.application.redirect_uris }}</pre>
<p><strong>Post-logout:</strong></p>
<pre>{{ partner.post_logout_redirect_uris|default:"(none)" }}</pre>
<a href="{% url 'partner_redirect_uris' partner.pk %}" class="btn btn-sm btn-outline-primary">Edit redirect URIs</a>
</div>
</div>

<div class="card mb-3">
<div class="card-header"><strong>Lifecycle</strong></div>
<div class="card-body">
<p><strong>Status:</strong> {{ partner.get_status_display }}</p>
{% if partner.status_reason %}<p><strong>Reason:</strong> {{ partner.status_reason }}</p>{% endif %}
{% if partner.status_changed_at %}<p><strong>Changed at:</strong> {{ partner.status_changed_at }}</p>{% endif %}

{% if partner.status == Status.ACTIVE %}
<a href="{% url 'partner_suspend' partner.pk %}" class="btn btn-warning">Suspend</a>
<a href="{% url 'partner_revoke' partner.pk %}" class="btn btn-danger">Revoke</a>
{% elif partner.status == Status.SUSPENDED %}
<form method="post" action="{% url 'partner_reactivate' partner.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-success">Reactivate</button>
</form>
<a href="{% url 'partner_revoke' partner.pk %}" class="btn btn-danger">Revoke</a>
{% elif partner.status == Status.REVOKED %}
<p class="text-muted">Revoked partners cannot be reactivated.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
31 changes: 31 additions & 0 deletions physionet-django/console/templates/console/partners/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "console/base_console.html" %}

{% block title %}Edit Partner: {{ partner.organization_name }}{% endblock %}

{% block content %}
<div class="container">
<h2>Edit organisation: {{ partner.organization_name }}</h2>

<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
{% if field.field.widget.input_type == 'checkbox' %}
<div class="form-check">
{{ field }}
<label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
</div>
{% else %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endif %}
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'partner_detail' partner.pk %}" class="btn btn-link">Cancel</a>
</form>
</div>
{% endblock %}
48 changes: 48 additions & 0 deletions physionet-django/console/templates/console/partners/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{% extends "console/base_console.html" %}

{% block title %}OAuth Partners{% endblock %}

{% block content %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>OAuth Partners</h2>
<a href="{% url 'partner_new' %}" class="btn btn-primary">New Partner</a>
</div>

<form method="get" class="form-inline mb-3">
<label class="mr-2" for="status">Filter by status:</label>
<select name="status" id="status" class="form-control mr-2" onchange="this.form.submit()">
<option value="">All</option>
{% for value, label in Status.choices %}
<option value="{{ value }}" {% if status_filter == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<noscript><button type="submit" class="btn btn-secondary">Filter</button></noscript>
</form>

<table class="table table-striped">
<thead>
<tr>
<th>Organisation</th>
<th>Client ID</th>
<th>Contact</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for partner in partners %}
<tr>
<td>{{ partner.organization_name }}</td>
<td><code>{{ partner.application.client_id }}</code></td>
<td>{{ partner.contact_email|default:"-" }}</td>
<td>{{ partner.get_status_display }}</td>
<td><a class="btn btn-sm btn-outline-primary" href="{% url 'partner_detail' partner.pk %}">View</a></td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-muted">No partners found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
31 changes: 31 additions & 0 deletions physionet-django/console/templates/console/partners/new.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "console/base_console.html" %}

{% block title %}New OAuth Partner{% endblock %}

{% block content %}
<div class="container">
<h2>New OAuth Partner</h2>

<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
{% if field.field.widget.input_type == 'checkbox' %}
<div class="form-check">
{{ field }}
<label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
</div>
{% else %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endif %}
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Create Partner</button>
<a href="{% url 'partner_list' %}" class="btn btn-link">Cancel</a>
</form>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "console/base_console.html" %}

{% block title %}Edit Redirect URIs: {{ partner.organization_name }}{% endblock %}

{% block content %}
<div class="container">
<h2>Redirect URIs: {{ partner.organization_name }}</h2>

<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'partner_detail' partner.pk %}" class="btn btn-link">Cancel</a>
</form>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "console/base_console.html" %}

{% block title %}Edit Scopes: {{ partner.organization_name }}{% endblock %}

{% block content %}
<div class="container">
<h2>Allowed scopes: {{ partner.organization_name }}</h2>

<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
<label>{{ field.label }}</label>
{{ field }}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'partner_detail' partner.pk %}" class="btn btn-link">Cancel</a>
</form>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% extends "console/base_console.html" %}

{% block title %}{{ action|title }} Partner: {{ partner.organization_name }}{% endblock %}

{% block content %}
<div class="container">
<h2>{{ action|title }} partner: {{ partner.organization_name }}</h2>

{% if action == "revoke" %}
<div class="alert alert-danger">
<strong>Warning:</strong> Revocation is terminal. The partner cannot be reactivated.
</div>
{% else %}
<div class="alert alert-warning">
Suspending the partner blocks new authorisation requests. You may also revoke active access tokens.
</div>
{% endif %}

<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit" class="btn {% if action == 'revoke' %}btn-danger{% else %}btn-warning{% endif %}">
Confirm {{ action }}
</button>
<a href="{% url 'partner_detail' partner.pk %}" class="btn btn-link">Cancel</a>
</form>
</div>
{% endblock %}
Loading
Loading