Skip to content

Commit 12b1b0a

Browse files
committed
Add Tier 0 Mirror Access support
Support generating an access token for access to our tier0 mirror and other services which are behind a nginx using ngx_http_auth_request_module for authentication. This works as following, nginx requires authentication for a route which is protected and redirects the request to archweb which handles the authentication. Closes: #347
1 parent 7b73d2c commit 12b1b0a

File tree

11 files changed

+218
-7
lines changed

11 files changed

+218
-7
lines changed

devel/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def clean_pgp_key(self):
3636

3737
class Meta:
3838
model = UserProfile
39-
exclude = ('allowed_repos', 'user', 'latin_name')
39+
exclude = ('allowed_repos', 'user', 'latin_name', 'repos_auth_token')
4040

4141

4242
class NewUserForm(forms.ModelForm):

devel/migrations/0007_auto_20210523_2038.py

Lines changed: 23 additions & 0 deletions
Large diffs are not rendered by default.

devel/models.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class UserProfile(models.Model):
5353
max_length=255, null=True, blank=True, help_text="Latin-form name; used only for non-Latin full names")
5454
rebuilderd_updates = models.BooleanField(
5555
default=False, help_text='Receive reproducible build package updates')
56+
repos_auth_token = models.CharField(max_length=32, null=True, blank=True)
5657
last_modified = models.DateTimeField(editable=False)
5758

5859
class Meta:
@@ -179,8 +180,8 @@ def create_feed_model(sender, **kwargs):
179180
website_rss=obj.website_rss)
180181

181182

182-
def delete_feed_model(sender, **kwargs):
183-
'''When a user is set to inactive remove his feed model'''
183+
def delete_user_model(sender, **kwargs):
184+
'''When a user is set to inactive remove his feed model and repository token'''
184185

185186
obj = kwargs['instance']
186187

@@ -194,11 +195,13 @@ def delete_feed_model(sender, **kwargs):
194195
if not userprofile:
195196
return
196197

198+
userprofile.repos_auth_token = ''
199+
197200
Feed.objects.filter(website_rss=userprofile.website_rss).delete()
198201

199202

200203
pre_save.connect(create_feed_model, sender=UserProfile, dispatch_uid="devel.models")
201204

202-
post_save.connect(delete_feed_model, sender=User, dispatch_uid='main.models')
205+
post_save.connect(delete_user_model, sender=User, dispatch_uid='main.models')
203206

204207
# vim: set ts=4 sw=4 et:

devel/templatetags/group.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@
66
@register.filter(name='in_group')
77
def in_group(user, group_name):
88
return user.groups.filter(name=group_name).exists()
9+
10+
11+
@register.filter(name='in_groups')
12+
def in_groups(user, group_names):
13+
group_names = group_names.split(':')
14+
return user.groups.filter(name__in=group_names).exists()

devel/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
url(r'^admin_log/$', views.admin_log),
88
url(r'^admin_log/(?P<username>.*)/$', views.admin_log),
99
url(r'^clock/$', views.clock, name='devel-clocks'),
10+
url(r'^tier0mirror/$', views.tier0_mirror, name='tier0-mirror'),
11+
url(r'^mirrorauth/$', views.tier0_mirror_auth, name='tier0-mirror-atuh'),
1012
url(r'^$', views.index, name='devel-index'),
1113
url(r'^stats/$', views.stats, name='devel-stats'),
1214
url(r'^newuser/$', views.new_user_form),

devel/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import secrets
23

34
from django.contrib.auth.models import User
45
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
@@ -192,4 +193,8 @@ def clear_cache(self):
192193
self.email_cache = {}
193194
self.pgp_cache = {}
194195

196+
197+
def generate_repo_auth_token():
198+
return secrets.token_hex(16)
199+
195200
# vim: set ts=4 sw=4 et:

devel/views.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import base64
12
import operator
23
import time
34
from datetime import timedelta
45

6+
from django.conf import settings
57
from django.contrib import admin
68
from django.contrib.admin.models import ADDITION, LogEntry
79
from django.contrib.auth.decorators import (login_required,
@@ -13,12 +15,12 @@
1315
from django.core.cache.utils import make_template_fragment_key
1416
from django.db import transaction
1517
from django.db.models import Count, Max
16-
from django.http import Http404, HttpResponseRedirect
18+
from django.http import Http404, HttpResponse, HttpResponseRedirect
1719
from django.shortcuts import get_object_or_404, render
1820
from django.utils.encoding import force_text
1921
from django.utils.http import http_date
2022
from django.utils.timezone import now
21-
from django.views.decorators.cache import never_cache
23+
from django.views.decorators.cache import cache_control, never_cache
2224
from main.models import Arch, Package, Repo
2325
from news.models import News
2426
from packages.models import FlagRequest, PackageRelation, Signoff
@@ -29,7 +31,7 @@
2931
from .forms import NewUserForm, ProfileForm, UserProfileForm
3032
from .models import UserProfile
3133
from .reports import available_reports
32-
from .utils import get_annotated_maintainers
34+
from .utils import get_annotated_maintainers, generate_repo_auth_token
3335

3436

3537
@login_required
@@ -159,6 +161,68 @@ def clock(request):
159161
return response
160162

161163

164+
@login_required
165+
def tier0_mirror(request):
166+
username = request.user.username
167+
profile, _ = UserProfile.objects.get_or_create(user=request.user)
168+
169+
if request.POST:
170+
profile.repos_auth_token = generate_repo_auth_token()
171+
profile.save()
172+
173+
token = profile.repos_auth_token
174+
mirror_domain = getattr(settings, 'TIER0_MIRROR_DOMAIN', None)
175+
mirror_url = ''
176+
177+
if mirror_domain and token:
178+
mirror_url = f'https://{username}:{token}@{mirror_domain}/$repo/os/$arch'
179+
180+
page_dict = {
181+
'mirror_url': mirror_url,
182+
}
183+
return render(request, 'devel/tier0_mirror.html', page_dict)
184+
185+
186+
@cache_control(max_age=300)
187+
def tier0_mirror_auth(request):
188+
unauthorized = HttpResponse('Unauthorized', status=401)
189+
unauthorized['WWW-Authenticate'] = 'Basic realm="Protected", charset="UTF-8"'
190+
191+
send_from_header = request.headers.get('X-Sent-From', '')
192+
193+
mirror_sent_from_secret = getattr(settings, 'TIER0_MIRROR_SECRET', None)
194+
if mirror_sent_from_secret and send_from_header == mirror_sent_from_secret:
195+
return unauthorized
196+
197+
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
198+
if not auth_header:
199+
return unauthorized
200+
201+
parts = auth_header.split()
202+
if len(parts) != 2:
203+
return unauthorized
204+
205+
if parts[0].lower() != 'basic':
206+
return unauthorized
207+
208+
credentials = base64.b64decode(parts[1]).decode().split(':')
209+
if len(credentials) != 2:
210+
return unauthorized
211+
212+
username = credentials[0]
213+
token = credentials[1]
214+
215+
groups = Group.objects.filter(name__in=SELECTED_GROUPS)
216+
user = User.objects.filter(username=username, is_active=True, groups__in=groups).select_related('userprofile').first()
217+
if not user:
218+
return unauthorized
219+
220+
if user and token == user.userprofile.repos_auth_token:
221+
return HttpResponse('Authorized')
222+
else:
223+
return unauthorized
224+
225+
162226
@login_required
163227
@never_cache
164228
def change_profile(request):

docs/mirror_access.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Mirror Access
2+
3+
Archweb can be used as external authentication provider in combination with
4+
[ngx_http_auth_request_module](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html).
5+
A user with a Developer, Trusted User and Support Staff role can generate an
6+
access token used in combination with his username on the `/devel/tier0mirror`
7+
url. The mirror authentication is done against `/devel/mirrorauth` using HTTP
8+
Basic authentication.
9+
10+
## Configuration
11+
12+
There are two configuration options for this feature of which one is optional:
13+
14+
* **TIER0_MIRROR_DOMAIN** - the mirror domain used to display the mirror url with authentication.
15+
* **TIER0_MIRROR_SECRET** - an optional secret send by nginx in the `X-Sent-From` header, all requests without this secret value are ignored. This can be used to not allow anyone to bruteforce guess the http basic auth pass/token.
16+
17+
## nginx configuration
18+
19+
Example configuration with optional caching of the authentication request to
20+
reduce hammering archweb when for example using this feature for a mirror. By
21+
default archweb caches `/devel/mirrorauth` for 5 minutes.
22+
23+
```
24+
http {
25+
proxy_cache_path /var/lib/nginx/cache/auth_cache levels=1:2 keys_zone=auth_cache:5m;
26+
27+
server {
28+
location /protected {
29+
auth_request /devel/mirrorauth;
30+
31+
root /usr/share/nginx/html;
32+
index index.html index.htm;
33+
}
34+
35+
location = /devel/mirrorauth {
36+
internal;
37+
38+
# Do not pass the request body, only http authorisation header is required
39+
proxy_pass_request_body off;
40+
proxy_set_header Content-Length "";
41+
42+
# Proxy headers
43+
proxy_set_header Host $host;
44+
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
45+
proxy_set_header X-Original-Method $request_method;
46+
proxy_set_header X-Auth-Request-Redirect $request_uri;
47+
proxy_set_header X-Sent-From "arch-nginx";
48+
49+
# Cache responses from the auth proxy
50+
proxy_cache auth_cache;
51+
proxy_cache_key "$scheme$proxy_host$request_uri$http_authorization";
52+
53+
# Authentication to archweb
54+
proxy_pass https://archlinux.org;
55+
}
56+
}
57+
}
58+
```

settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@
199199
# Rebuilderd API endpoint
200200
REBUILDERD_URL = 'https://reproducible.archlinux.org/api/v0/pkgs/list'
201201

202+
# Protected TIER0 Mirror
203+
TIER0_MIRROR_DOMAIN = 'repos.archlinux.org'
204+
# TIER0_MIRROR_SECRET = ''
205+
202206
# Import local settings
203207
try:
204208
from local_settings import * # noqa

templates/base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
<li><a href="{% url 'admin:index' %}" title="Django Admin Interface">Django Admin</a></li>
5252
{% endif %}
5353
<li><a href="/devel/profile/" title="Modify your account profile">Profile</a></li>
54+
{% if user|in_groups:'Developers:Trusted Users:Support Staff' %}
55+
<li><a href="/devel/tier0mirror/" title="Your Tier 0 Mirror information">Tier0 mirror</a></li>
56+
{% endif %}
5457
<li><a href="/logout/" title="Logout of the developer interface">Logout</a></li>
5558
</ul>
5659
{% endif %}

0 commit comments

Comments
 (0)