Skip to content

Commit 0ff68fc

Browse files
authored
Merge pull request #848 from TG1999/auth
Add authentication
2 parents c062c71 + 82839fc commit 0ff68fc

6 files changed

Lines changed: 135 additions & 33 deletions

File tree

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ Version v30.0.0
5959

6060
- Add bulk search support for CPEs.
6161

62+
- Add authentication for REST API endpoint.
63+
The autentication is disabled by default and can be enabled using the
64+
SCANCODEIO_REQUIRE_AUTHENTICATION settings.
65+
When enabled, users have to authenticate using
66+
their API Key in the REST API.
67+
Users can be created using the Django "createsuperuser" management command.
68+
6269
Other:
6370

6471
- we dropped calver to use a plain semver.

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)

vulnerabilities/templates/base.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<!DOCTYPE html class="has-navbar-fixed-top">
1+
<!DOCTYPE html>
2+
<html lang="en">
23
{% load static %}
34
<head>
45
<title>VulnerableCode</title>

vulnerabilities/tests/test_auth.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
12+
from django.conf import settings
13+
from django.contrib.auth import get_user_model
14+
from django.contrib.auth.models import AnonymousUser
15+
from django.test import TestCase
16+
17+
TEST_PASSWORD = "secret"
18+
19+
User = get_user_model()
20+
21+
api_package_url = "/api/packages/"
22+
login_redirect_url = settings.LOGIN_REDIRECT_URL
23+
24+
25+
class VulnerableCodeAuthTest(TestCase):
26+
def setUp(self):
27+
self.anonymous_user = AnonymousUser()
28+
self.basic_user = User.objects.create_user(username="basic_user", password=TEST_PASSWORD)
29+
30+
def test_vulnerablecode_auth_api_required_authentication(self):
31+
response = self.client.get(api_package_url)
32+
expected = {"detail": "Authentication credentials were not provided."}
33+
self.assertEqual(expected, response.json())
34+
self.assertEqual(401, response.status_code)

vulnerabilities/tests/test_fix_api.py

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import json
11+
12+
from django.contrib.auth import get_user_model
1013
from django.test import TestCase
14+
from django.test import TransactionTestCase
1115
from django.utils.http import int_to_base36
1216
from packageurl import PackageURL
1317
from rest_framework import status
18+
from rest_framework.test import APIClient
1419

1520
from vulnerabilities.models import Alias
1621
from vulnerabilities.models import Package
@@ -19,9 +24,15 @@
1924
from vulnerabilities.models import VulnerabilityReference
2025
from vulnerabilities.models import VulnerabilityRelatedReference
2126

27+
User = get_user_model()
28+
2229

23-
class APITestCaseVulnerability(TestCase):
30+
class APITestCaseVulnerability(TransactionTestCase):
2431
def setUp(self):
32+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
33+
self.auth = f"Token {self.user.auth_token.key}"
34+
self.csrf_client = APIClient(enforce_csrf_checks=True)
35+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
2536
for i in range(0, 200):
2637
Vulnerability.objects.create(
2738
summary=str(i),
@@ -35,15 +46,15 @@ def setUp(self):
3546
)
3647

3748
def test_api_status(self):
38-
response = self.client.get("/api/vulnerabilities/", format="json")
49+
response = self.csrf_client.get("/api/vulnerabilities/")
3950
self.assertEqual(status.HTTP_200_OK, response.status_code)
4051

4152
def test_api_response(self):
42-
response = self.client.get("/api/vulnerabilities/", format="json").data
53+
response = self.csrf_client.get("/api/vulnerabilities/").data
4354
self.assertEqual(response["count"], 201)
4455

4556
def test_api_with_single_vulnerability(self):
46-
response = self.client.get(
57+
response = self.csrf_client.get(
4758
f"/api/vulnerabilities/{self.vulnerability.id}", format="json"
4859
).data
4960
assert response == {
@@ -66,7 +77,7 @@ def test_api_with_single_vulnerability(self):
6677
}
6778

6879
def test_api_with_single_vulnerability_with_filters(self):
69-
response = self.client.get(
80+
response = self.csrf_client.get(
7081
f"/api/vulnerabilities/{self.vulnerability.id}?type=pypi", format="json"
7182
).data
7283
assert response == {
@@ -87,6 +98,10 @@ def test_api_with_single_vulnerability_with_filters(self):
8798

8899
class APITestCasePackage(TestCase):
89100
def setUp(self):
101+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
102+
self.auth = f"Token {self.user.auth_token.key}"
103+
self.csrf_client = APIClient(enforce_csrf_checks=True)
104+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
90105
vuln = Vulnerability.objects.create(
91106
summary="test-vuln",
92107
)
@@ -123,15 +138,15 @@ def setUp(self):
123138
)
124139

125140
def test_api_status(self):
126-
response = self.client.get("/api/packages/", format="json")
141+
response = self.csrf_client.get("/api/packages/", format="json")
127142
self.assertEqual(status.HTTP_200_OK, response.status_code)
128143

129144
def test_api_response(self):
130-
response = self.client.get("/api/packages/", format="json").data
145+
response = self.csrf_client.get("/api/packages/", format="json").data
131146
self.assertEqual(response["count"], 11)
132147

133148
def test_api_with_single_vulnerability_and_fixed_package(self):
134-
response = self.client.get(f"/api/packages/{self.package.id}", format="json").data
149+
response = self.csrf_client.get(f"/api/packages/{self.package.id}", format="json").data
135150
assert response == {
136151
"url": f"http://testserver/api/packages/{self.package.id}",
137152
"purl": "pkg:generic/nginx/test@11",
@@ -160,7 +175,7 @@ def test_api_with_single_vulnerability_and_fixed_package(self):
160175
}
161176

162177
def test_api_with_single_vulnerability_and_vulnerable_package(self):
163-
response = self.client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
178+
response = self.csrf_client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
164179
assert response == {
165180
"url": f"http://testserver/api/packages/{self.vuln_package.id}",
166181
"purl": "pkg:generic/nginx/test@9",
@@ -204,6 +219,10 @@ def test_api_with_single_vulnerability_and_vulnerable_package(self):
204219

205220
class CPEApi(TestCase):
206221
def setUp(self):
222+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
223+
self.auth = f"Token {self.user.auth_token.key}"
224+
self.csrf_client = APIClient(enforce_csrf_checks=True)
225+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
207226
self.vulnerability = Vulnerability.objects.create(summary="test")
208227
for i in range(0, 10):
209228
ref, _ = VulnerabilityReference.objects.get_or_create(
@@ -214,31 +233,39 @@ def setUp(self):
214233
)
215234

216235
def test_api_status(self):
217-
response = self.client.get("/api/cpes/", format="json")
236+
response = self.csrf_client.get("/api/cpes/", format="json")
218237
self.assertEqual(status.HTTP_200_OK, response.status_code)
219238

220239
def test_api_response(self):
221-
response = self.client.get("/api/cpes/?cpe=cpe:/a:nginx:9", format="json").data
240+
response = self.csrf_client.get("/api/cpes/?cpe=cpe:/a:nginx:9", format="json").data
222241
self.assertEqual(response["count"], 1)
223242

224243

225244
class AliasApi(TestCase):
226245
def setUp(self):
246+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
247+
self.auth = f"Token {self.user.auth_token.key}"
248+
self.csrf_client = APIClient(enforce_csrf_checks=True)
249+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
227250
self.vulnerability = Vulnerability.objects.create(summary="test")
228251
for i in range(0, 10):
229252
Alias.objects.create(alias=f"CVE-{i}", vulnerability=self.vulnerability)
230253

231254
def test_api_status(self):
232-
response = self.client.get("/api/alias/", format="json")
255+
response = self.csrf_client.get("/api/alias/", format="json")
233256
self.assertEqual(status.HTTP_200_OK, response.status_code)
234257

235258
def test_api_response(self):
236-
response = self.client.get("/api/alias?alias=CVE-9", format="json").data
259+
response = self.csrf_client.get("/api/alias?alias=CVE-9", format="json").data
237260
self.assertEqual(response["count"], 1)
238261

239262

240-
class BulkSearchAPI(TestCase):
263+
class BulkSearchAPIPackage(TestCase):
241264
def setUp(self):
265+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
266+
self.auth = f"Token {self.user.auth_token.key}"
267+
self.csrf_client = APIClient(enforce_csrf_checks=True)
268+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
242269
packages = [
243270
"pkg:nginx/nginx@0.6.18",
244271
"pkg:nginx/nginx@1.20.0",
@@ -264,16 +291,20 @@ def test_api_response(self):
264291
request_body = {
265292
"purls": self.packages,
266293
}
267-
response = self.client.post(
294+
response = self.csrf_client.post(
268295
"/api/packages/bulk_search",
269-
data=request_body,
296+
data=json.dumps(request_body),
270297
content_type="application/json",
271298
).json()
272299
assert len(response) == 13
273300

274301

275-
class BulkSearchAPI(TestCase):
302+
class BulkSearchAPICPE(TestCase):
276303
def setUp(self):
304+
self.user = User.objects.create_user("username", "e@mail.com", "secret")
305+
self.auth = f"Token {self.user.auth_token.key}"
306+
self.csrf_client = APIClient(enforce_csrf_checks=True)
307+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
277308
self.exclusive_cpes = [
278309
"cpe:/a:nginx:1.0.7",
279310
"cpe:/a:nginx:1.0.15",
@@ -305,9 +336,9 @@ def test_api_response_with_with_exclusive_cpes_associated_with_two_vulnerabiliti
305336
request_body = {
306337
"cpes": self.exclusive_cpes,
307338
}
308-
response = self.client.post(
339+
response = self.csrf_client.post(
309340
"/api/cpes/bulk_search",
310-
data=request_body,
341+
data=json.dumps(request_body),
311342
content_type="application/json",
312343
).json()
313344
assert len(response) == 1
@@ -320,9 +351,9 @@ def test_api_response_with_no_cpe_associated(self):
320351
request_body = {
321352
"cpes": ["cpe:/a:nginx:1.10.7"],
322353
}
323-
response = self.client.post(
354+
response = self.csrf_client.post(
324355
"/api/cpes/bulk_search",
325-
data=request_body,
356+
data=json.dumps(request_body),
326357
content_type="application/json",
327358
).json()
328359
assert len(response) == 0
@@ -331,9 +362,9 @@ def test_api_response_with_with_non_exclusive_cpes_associated_with_two_vulnerabi
331362
request_body = {
332363
"cpes": self.non_exclusive_cpes,
333364
}
334-
response = self.client.post(
365+
response = self.csrf_client.post(
335366
"/api/cpes/bulk_search",
336-
data=request_body,
367+
data=json.dumps(request_body),
337368
content_type="application/json",
338369
).json()
339370
assert len(response) == 2
@@ -342,20 +373,18 @@ def test_with_empty_list(self):
342373
request_body = {
343374
"cpes": [],
344375
}
345-
response = self.client.post(
376+
response = self.csrf_client.post(
346377
"/api/cpes/bulk_search",
347-
data=request_body,
378+
data=json.dumps(request_body),
348379
content_type="application/json",
349380
).json()
350381
assert response == {"Error": "A non-empty 'cpe' list of package URLs is required."}
351382

352383
def test_with_invalid_cpes(self):
353-
request_body = {
354-
"cpes": ["CVE-2022-2022"],
355-
}
356-
response = self.client.post(
384+
request_body = {"cpes": ["CVE-2022-2022"]}
385+
response = self.csrf_client.post(
357386
"/api/cpes/bulk_search",
358-
data=request_body,
387+
data=json.dumps(request_body),
359388
content_type="application/json",
360389
).json()
361390
assert response == {"Error": "Invalid CPE: CVE-2022-2022"}

vulnerablecode/settings.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import sys
1011
from pathlib import Path
1112

1213
import environ
@@ -129,6 +130,21 @@
129130

130131
USE_I18N = True
131132

133+
IS_TESTS = False
134+
135+
if len(sys.argv) > 0:
136+
IS_TESTS = "pytest" in sys.argv[0]
137+
138+
VULNERABLECODEIO_REQUIRE_AUTHENTICATION = env.bool(
139+
"VULNERABLECODEIO_REQUIRE_AUTHENTICATION", default=False
140+
)
141+
142+
LOGIN_REDIRECT_URL = "/"
143+
LOGOUT_REDIRECT_URL = "/"
144+
145+
if IS_TESTS:
146+
VULNERABLECODEIO_REQUIRE_AUTHENTICATION = True
147+
132148
USE_L10N = True
133149

134150
USE_TZ = True
@@ -148,8 +164,8 @@
148164
# Django restframework
149165

150166
REST_FRAMEWORK = {
151-
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
152-
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
167+
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication",),
168+
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
153169
"DEFAULT_RENDERER_CLASSES": (
154170
"rest_framework.renderers.JSONRenderer",
155171
"rest_framework.renderers.BrowsableAPIRenderer",
@@ -163,3 +179,6 @@
163179
# Limit the load on the Database returning a small number of records by default. https://github.com/nexB/vulnerablecode/issues/819
164180
"PAGE_SIZE": 10,
165181
}
182+
183+
if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION:
184+
REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",)

0 commit comments

Comments
 (0)