Skip to content

Commit 45b1e19

Browse files
committed
Add authetication for API
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent c062c71 commit 45b1e19

9 files changed

Lines changed: 180 additions & 4 deletions

File tree

vulnerabilities/api.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from vulnerabilities.models import Vulnerability
2323
from vulnerabilities.models import VulnerabilityReference
2424
from vulnerabilities.models import VulnerabilitySeverity
25+
from vulnerablecode.auth import ConditionalLoginRequired
2526

2627

2728
class VulnerabilitySeveritySerializer(serializers.ModelSerializer):
@@ -30,6 +31,10 @@ class Meta:
3031
fields = ["value", "scoring_system"]
3132

3233

34+
class AuthenticatedAPIViewSet(ConditionalLoginRequired, viewsets.ReadOnlyModelViewSet):
35+
pass
36+
37+
3338
class VulnerabilityReferenceSerializer(serializers.ModelSerializer):
3439
scores = VulnerabilitySeveritySerializer(many=True, source="vulnerabilityseverity_set")
3540
reference_url = serializers.CharField(source="url")
@@ -206,7 +211,7 @@ def filter_purl(self, queryset, name, value):
206211
return self.queryset.filter(**attrs)
207212

208213

209-
class PackageViewSet(viewsets.ReadOnlyModelViewSet):
214+
class PackageViewSet(AuthenticatedAPIViewSet):
210215
queryset = Package.objects.all()
211216
serializer_class = PackageSerializer
212217
filter_backends = (filters.DjangoFilterBackend,)
@@ -253,7 +258,7 @@ class Meta:
253258
fields = ["vulnerability_id"]
254259

255260

256-
class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
261+
class VulnerabilityViewSet(AuthenticatedAPIViewSet):
257262
def get_fixed_packages_qs(self):
258263
"""
259264
Filter the packages that fixes a vulnerability
@@ -295,7 +300,7 @@ def filter_cpe(self, queryset, name, value):
295300
return self.queryset.filter(vulnerabilityreference__reference_id__startswith=cpe).distinct()
296301

297302

298-
class CPEViewSet(viewsets.ReadOnlyModelViewSet):
303+
class CPEViewSet(AuthenticatedAPIViewSet):
299304
queryset = Vulnerability.objects.filter(
300305
vulnerabilityreference__reference_id__startswith="cpe"
301306
).distinct()
@@ -336,7 +341,7 @@ def filter_alias(self, queryset, name, value):
336341
return self.queryset.filter(aliases__alias__icontains=alias)
337342

338343

339-
class AliasViewSet(viewsets.ReadOnlyModelViewSet):
344+
class AliasViewSet(AuthenticatedAPIViewSet):
340345
queryset = Vulnerability.objects.all()
341346
serializer_class = VulnerabilitySerializer
342347
filter_backends = (filters.DjangoFilterBackend,)

vulnerabilities/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
import logging
1313
import uuid
1414

15+
from django.conf import settings
1516
from django.core.exceptions import ValidationError
1617
from django.core.validators import MaxValueValidator
1718
from django.core.validators import MinValueValidator
1819
from django.db import models
20+
from django.dispatch import receiver
1921
from django.utils.http import int_to_base36
2022
from packageurl import PackageURL
2123
from packageurl.contrib.django.models import PackageURLMixin
24+
from rest_framework.authtoken.models import Token
2225

2326
from vulnerabilities.importer import AdvisoryData
2427
from vulnerabilities.importer import AffectedPackage
@@ -418,3 +421,12 @@ def to_advisory_data(self) -> AdvisoryData:
418421
references=[Reference.from_dict(ref) for ref in self.references],
419422
date_published=self.date_published,
420423
)
424+
425+
426+
@receiver(models.signals.post_save, sender=settings.AUTH_USER_MODEL)
427+
def create_auth_token(sender, instance=None, created=False, **kwargs):
428+
"""
429+
Creates an API key token on user creation, using the signal system.
430+
"""
431+
if created:
432+
Token.objects.create(user_id=instance.pk)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
<div class="container is-max-desktop">
5+
6+
<section class="section pt-0">
7+
<div class="is-flex is-justify-content-space-between mb-2">
8+
<nav class="breadcrumb is-medium mb-1" aria-label="breadcrumbs">
9+
<ul>
10+
<li class="is-active"><a href="#" aria-current="page">Profile settings</a></li>
11+
</ul>
12+
</nav>
13+
</div>
14+
15+
<article class="message is-warning">
16+
<div class="message-body">
17+
<strong>An API key is like a password and should be treated with the same care.</strong>
18+
</div>
19+
</article>
20+
21+
<div class="field">
22+
<label class="label">API Key</label>
23+
<div class="control has-icons-left">
24+
<input class="input" type="text" value="{{ request.user.auth_token.key|default:'Not available' }}" readonly>
25+
</div>
26+
</div>
27+
28+
</section>
29+
</div>
30+
{% endblock %}

vulnerabilities/templates/base.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@
2323
<a class="navbar-item" href="{% url 'vulnerability_search' %}">
2424
Vulnerabilities
2525
</a>
26+
{% if user.is_authenticated %}
27+
<div class="navbar-item has-dropdown is-hoverable">
28+
<a class="navbar-link">
29+
{{ user.username }}
30+
</a>
31+
<div class="navbar-dropdown is-right">
32+
<a class="navbar-item" href="{% url 'account_profile' %}">
33+
Profile settings
34+
</a>
35+
<a class="navbar-item" href="{% url 'logout' %}">
36+
Sign out
37+
</a>
38+
</div>
39+
{% endif %}
2640
</div>
2741
</div>
2842
</nav>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
<div class="container is-max-desktop">
5+
{% if form.errors %}
6+
<article class="message is-danger">
7+
<div class="message-body">
8+
Your username and password didn't match. Please try again.
9+
</div>
10+
</article>
11+
{% endif %}
12+
13+
<section class="section mt-6 pt-0 columns is-mobile">
14+
<div class="column is-half is-offset-one-quarter">
15+
<form class="box" method="post" action="{% url 'login' %}">
16+
{% csrf_token %}
17+
18+
<div class="field">
19+
<label class="label">Username</label>
20+
<div class="control has-icons-left">
21+
<input class="input" type="text" name="username" autofocus autocomplete="username" maxlength="150" required id="id_username">
22+
</div>
23+
</div>
24+
25+
<div class="field mb-5">
26+
<label class="label">Password</label>
27+
<div class="control has-icons-left">
28+
<input class="input" type="password" name="password" autocomplete="current-password" required id="id_password">
29+
</div>
30+
</div>
31+
32+
<input type="hidden" name="next" value="{{ next }}">
33+
<div class="control">
34+
<input type="submit" class="button is-link is-fullwidth has-text-weight-bold" value="Sign in">
35+
</div>
36+
</form>
37+
</div>
38+
39+
</section>
40+
</div>
41+
{% endblock %}

vulnerabilities/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from urllib.parse import urlencode
1111

12+
from django.contrib.auth.mixins import LoginRequiredMixin
1213
from django.core.paginator import PageNotAnInteger
1314
from django.core.paginator import Paginator
1415
from django.db.models import Count
@@ -17,6 +18,7 @@
1718
from django.shortcuts import render
1819
from django.urls import reverse
1920
from django.views import View
21+
from django.views import generic
2022
from django.views.generic.edit import UpdateView
2123
from django.views.generic.list import ListView
2224

@@ -167,3 +169,7 @@ def schema_view(request):
167169
if request.method != "GET":
168170
return HttpResponseNotAllowed()
169171
return render(request, "api_doc.html")
172+
173+
174+
class AccountProfileView(LoginRequiredMixin, generic.TemplateView):
175+
template_name = "accounts/profile.html"

vulnerablecode/auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
# This is copied from https://github.com/nexB/scancode.io/commit/eab8eeb13989c26a1600cc64e8b054f171341063
9+
#
10+
11+
from django.conf import settings
12+
from django.contrib.auth.decorators import user_passes_test
13+
from django.contrib.auth.mixins import UserPassesTestMixin
14+
15+
16+
def is_authenticated_when_required(user):
17+
"""
18+
Returns True if the `user` is authenticated when the
19+
`VULNERABLECODEIO_REQUIRE_AUTHENTICATION` setting is enabled.
20+
Always True when the Authentication is not enabled.
21+
"""
22+
if not settings.VULNERABLECODEIO_REQUIRE_AUTHENTICATION:
23+
return True
24+
25+
if user.is_authenticated:
26+
return True
27+
28+
return False
29+
30+
31+
def conditional_login_required(function=None):
32+
"""
33+
Decorator for views that checks that the current user is authenticated when
34+
authentication is enabled.
35+
"""
36+
actual_decorator = user_passes_test(is_authenticated_when_required)
37+
if function:
38+
return actual_decorator(function)
39+
return actual_decorator
40+
41+
42+
class ConditionalLoginRequired(UserPassesTestMixin):
43+
"""
44+
CBV mixin for views that checks that the current user is authenticated when
45+
authentication is enabled.
46+
"""
47+
48+
def test_func(self):
49+
return is_authenticated_when_required(self.request.user)

vulnerablecode/settings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@
129129

130130
USE_I18N = True
131131

132+
VULNERABLECODEIO_REQUIRE_AUTHENTICATION = env.bool(
133+
"VULNERABLECODEIO_REQUIRE_AUTHENTICATION", default=False
134+
)
135+
136+
LOGIN_REDIRECT_URL = "/"
137+
LOGOUT_REDIRECT_URL = "/"
138+
132139
USE_L10N = True
133140

134141
USE_TZ = True
@@ -163,3 +170,6 @@
163170
# Limit the load on the Database returning a small number of records by default. https://github.com/nexB/vulnerablecode/issues/819
164171
"PAGE_SIZE": 10,
165172
}
173+
174+
if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION:
175+
REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",)

vulnerablecode/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#
99

1010
from django.contrib import admin
11+
from django.contrib.auth import views as auth_views
1112
from django.urls import include
1213
from django.urls import path
1314
from rest_framework.routers import DefaultRouter
@@ -16,6 +17,7 @@
1617
from vulnerabilities.api import CPEViewSet
1718
from vulnerabilities.api import PackageViewSet
1819
from vulnerabilities.api import VulnerabilityViewSet
20+
from vulnerabilities.views import AccountProfileView
1921
from vulnerabilities.views import HomePage
2022
from vulnerabilities.views import PackageSearchView
2123
from vulnerabilities.views import PackageUpdate
@@ -47,5 +49,12 @@ def __init__(self, *args, **kwargs):
4749
path("vulnerabilities/<int:pk>", VulnerabilityDetails.as_view(), name="vulnerability_view"),
4850
path("vulnerabilities/search", VulnerabilitySearchView.as_view(), name="vulnerability_search"),
4951
path("", HomePage.as_view(), name="home"),
52+
path("accounts/profile/", AccountProfileView.as_view(), name="account_profile"),
53+
path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
54+
path(
55+
"accounts/logout/",
56+
auth_views.LogoutView.as_view(next_page="login"),
57+
name="logout",
58+
),
5059
path(r"api/", include(api_router.urls)),
5160
]

0 commit comments

Comments
 (0)