diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 693604adb..956445c8e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -59,6 +59,13 @@ Version v30.0.0
- Add bulk search support for CPEs.
+- Add authentication for REST API endpoint.
+ The autentication is disabled by default and can be enabled using the
+ SCANCODEIO_REQUIRE_AUTHENTICATION settings.
+ When enabled, users have to authenticate using
+ their API Key in the REST API.
+ Users can be created using the Django "createsuperuser" management command.
+
Other:
- we dropped calver to use a plain semver.
diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py
index aeace5885..d350e7a47 100644
--- a/vulnerabilities/models.py
+++ b/vulnerabilities/models.py
@@ -12,13 +12,16 @@
import logging
import uuid
+from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator
from django.core.validators import MinValueValidator
from django.db import models
+from django.dispatch import receiver
from django.utils.http import int_to_base36
from packageurl import PackageURL
from packageurl.contrib.django.models import PackageURLMixin
+from rest_framework.authtoken.models import Token
from vulnerabilities.importer import AdvisoryData
from vulnerabilities.importer import AffectedPackage
@@ -418,3 +421,12 @@ def to_advisory_data(self) -> AdvisoryData:
references=[Reference.from_dict(ref) for ref in self.references],
date_published=self.date_published,
)
+
+
+@receiver(models.signals.post_save, sender=settings.AUTH_USER_MODEL)
+def create_auth_token(sender, instance=None, created=False, **kwargs):
+ """
+ Creates an API key token on user creation, using the signal system.
+ """
+ if created:
+ Token.objects.create(user_id=instance.pk)
diff --git a/vulnerabilities/templates/base.html b/vulnerabilities/templates/base.html
index 683b106ed..95ce1c1a0 100644
--- a/vulnerabilities/templates/base.html
+++ b/vulnerabilities/templates/base.html
@@ -1,4 +1,5 @@
-
+
+
{% load static %}
VulnerableCode
diff --git a/vulnerabilities/tests/test_auth.py b/vulnerabilities/tests/test_auth.py
new file mode 100644
index 000000000..41ba6e538
--- /dev/null
+++ b/vulnerabilities/tests/test_auth.py
@@ -0,0 +1,34 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+# This is copied from https://github.com/nexB/scancode.io/commit/eab8eeb13989c26a1600cc64e8b054f171341063
+#
+
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
+from django.test import TestCase
+
+TEST_PASSWORD = "secret"
+
+User = get_user_model()
+
+api_package_url = "/api/packages/"
+login_redirect_url = settings.LOGIN_REDIRECT_URL
+
+
+class VulnerableCodeAuthTest(TestCase):
+ def setUp(self):
+ self.anonymous_user = AnonymousUser()
+ self.basic_user = User.objects.create_user(username="basic_user", password=TEST_PASSWORD)
+
+ def test_vulnerablecode_auth_api_required_authentication(self):
+ response = self.client.get(api_package_url)
+ expected = {"detail": "Authentication credentials were not provided."}
+ self.assertEqual(expected, response.json())
+ self.assertEqual(401, response.status_code)
diff --git a/vulnerabilities/tests/test_fix_api.py b/vulnerabilities/tests/test_fix_api.py
index 0b6aa47d5..2cd53db12 100644
--- a/vulnerabilities/tests/test_fix_api.py
+++ b/vulnerabilities/tests/test_fix_api.py
@@ -7,10 +7,15 @@
# See https://aboutcode.org for more information about nexB OSS projects.
#
+import json
+
+from django.contrib.auth import get_user_model
from django.test import TestCase
+from django.test import TransactionTestCase
from django.utils.http import int_to_base36
from packageurl import PackageURL
from rest_framework import status
+from rest_framework.test import APIClient
from vulnerabilities.models import Alias
from vulnerabilities.models import Package
@@ -19,9 +24,15 @@
from vulnerabilities.models import VulnerabilityReference
from vulnerabilities.models import VulnerabilityRelatedReference
+User = get_user_model()
+
-class APITestCaseVulnerability(TestCase):
+class APITestCaseVulnerability(TransactionTestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
for i in range(0, 200):
Vulnerability.objects.create(
summary=str(i),
@@ -35,15 +46,15 @@ def setUp(self):
)
def test_api_status(self):
- response = self.client.get("/api/vulnerabilities/", format="json")
+ response = self.csrf_client.get("/api/vulnerabilities/")
self.assertEqual(status.HTTP_200_OK, response.status_code)
def test_api_response(self):
- response = self.client.get("/api/vulnerabilities/", format="json").data
+ response = self.csrf_client.get("/api/vulnerabilities/").data
self.assertEqual(response["count"], 201)
def test_api_with_single_vulnerability(self):
- response = self.client.get(
+ response = self.csrf_client.get(
f"/api/vulnerabilities/{self.vulnerability.id}", format="json"
).data
assert response == {
@@ -66,7 +77,7 @@ def test_api_with_single_vulnerability(self):
}
def test_api_with_single_vulnerability_with_filters(self):
- response = self.client.get(
+ response = self.csrf_client.get(
f"/api/vulnerabilities/{self.vulnerability.id}?type=pypi", format="json"
).data
assert response == {
@@ -87,6 +98,10 @@ def test_api_with_single_vulnerability_with_filters(self):
class APITestCasePackage(TestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
vuln = Vulnerability.objects.create(
summary="test-vuln",
)
@@ -123,15 +138,15 @@ def setUp(self):
)
def test_api_status(self):
- response = self.client.get("/api/packages/", format="json")
+ response = self.csrf_client.get("/api/packages/", format="json")
self.assertEqual(status.HTTP_200_OK, response.status_code)
def test_api_response(self):
- response = self.client.get("/api/packages/", format="json").data
+ response = self.csrf_client.get("/api/packages/", format="json").data
self.assertEqual(response["count"], 11)
def test_api_with_single_vulnerability_and_fixed_package(self):
- response = self.client.get(f"/api/packages/{self.package.id}", format="json").data
+ response = self.csrf_client.get(f"/api/packages/{self.package.id}", format="json").data
assert response == {
"url": f"http://testserver/api/packages/{self.package.id}",
"purl": "pkg:generic/nginx/test@11",
@@ -160,7 +175,7 @@ def test_api_with_single_vulnerability_and_fixed_package(self):
}
def test_api_with_single_vulnerability_and_vulnerable_package(self):
- response = self.client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
+ response = self.csrf_client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
assert response == {
"url": f"http://testserver/api/packages/{self.vuln_package.id}",
"purl": "pkg:generic/nginx/test@9",
@@ -204,6 +219,10 @@ def test_api_with_single_vulnerability_and_vulnerable_package(self):
class CPEApi(TestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
self.vulnerability = Vulnerability.objects.create(summary="test")
for i in range(0, 10):
ref, _ = VulnerabilityReference.objects.get_or_create(
@@ -214,31 +233,39 @@ def setUp(self):
)
def test_api_status(self):
- response = self.client.get("/api/cpes/", format="json")
+ response = self.csrf_client.get("/api/cpes/", format="json")
self.assertEqual(status.HTTP_200_OK, response.status_code)
def test_api_response(self):
- response = self.client.get("/api/cpes/?cpe=cpe:/a:nginx:9", format="json").data
+ response = self.csrf_client.get("/api/cpes/?cpe=cpe:/a:nginx:9", format="json").data
self.assertEqual(response["count"], 1)
class AliasApi(TestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
self.vulnerability = Vulnerability.objects.create(summary="test")
for i in range(0, 10):
Alias.objects.create(alias=f"CVE-{i}", vulnerability=self.vulnerability)
def test_api_status(self):
- response = self.client.get("/api/alias/", format="json")
+ response = self.csrf_client.get("/api/alias/", format="json")
self.assertEqual(status.HTTP_200_OK, response.status_code)
def test_api_response(self):
- response = self.client.get("/api/alias?alias=CVE-9", format="json").data
+ response = self.csrf_client.get("/api/alias?alias=CVE-9", format="json").data
self.assertEqual(response["count"], 1)
-class BulkSearchAPI(TestCase):
+class BulkSearchAPIPackage(TestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
packages = [
"pkg:nginx/nginx@0.6.18",
"pkg:nginx/nginx@1.20.0",
@@ -264,16 +291,20 @@ def test_api_response(self):
request_body = {
"purls": self.packages,
}
- response = self.client.post(
+ response = self.csrf_client.post(
"/api/packages/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 13
-class BulkSearchAPI(TestCase):
+class BulkSearchAPICPE(TestCase):
def setUp(self):
+ self.user = User.objects.create_user("username", "e@mail.com", "secret")
+ self.auth = f"Token {self.user.auth_token.key}"
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
self.exclusive_cpes = [
"cpe:/a:nginx:1.0.7",
"cpe:/a:nginx:1.0.15",
@@ -305,9 +336,9 @@ def test_api_response_with_with_exclusive_cpes_associated_with_two_vulnerabiliti
request_body = {
"cpes": self.exclusive_cpes,
}
- response = self.client.post(
+ response = self.csrf_client.post(
"/api/cpes/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 1
@@ -320,9 +351,9 @@ def test_api_response_with_no_cpe_associated(self):
request_body = {
"cpes": ["cpe:/a:nginx:1.10.7"],
}
- response = self.client.post(
+ response = self.csrf_client.post(
"/api/cpes/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 0
@@ -331,9 +362,9 @@ def test_api_response_with_with_non_exclusive_cpes_associated_with_two_vulnerabi
request_body = {
"cpes": self.non_exclusive_cpes,
}
- response = self.client.post(
+ response = self.csrf_client.post(
"/api/cpes/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 2
@@ -342,20 +373,18 @@ def test_with_empty_list(self):
request_body = {
"cpes": [],
}
- response = self.client.post(
+ response = self.csrf_client.post(
"/api/cpes/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert response == {"Error": "A non-empty 'cpe' list of package URLs is required."}
def test_with_invalid_cpes(self):
- request_body = {
- "cpes": ["CVE-2022-2022"],
- }
- response = self.client.post(
+ request_body = {"cpes": ["CVE-2022-2022"]}
+ response = self.csrf_client.post(
"/api/cpes/bulk_search",
- data=request_body,
+ data=json.dumps(request_body),
content_type="application/json",
).json()
assert response == {"Error": "Invalid CPE: CVE-2022-2022"}
diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py
index 0e1abce04..ba3db9042 100644
--- a/vulnerablecode/settings.py
+++ b/vulnerablecode/settings.py
@@ -7,6 +7,7 @@
# See https://aboutcode.org for more information about nexB OSS projects.
#
+import sys
from pathlib import Path
import environ
@@ -129,6 +130,21 @@
USE_I18N = True
+IS_TESTS = False
+
+if len(sys.argv) > 0:
+ IS_TESTS = "pytest" in sys.argv[0]
+
+VULNERABLECODEIO_REQUIRE_AUTHENTICATION = env.bool(
+ "VULNERABLECODEIO_REQUIRE_AUTHENTICATION", default=False
+)
+
+LOGIN_REDIRECT_URL = "/"
+LOGOUT_REDIRECT_URL = "/"
+
+if IS_TESTS:
+ VULNERABLECODEIO_REQUIRE_AUTHENTICATION = True
+
USE_L10N = True
USE_TZ = True
@@ -148,8 +164,8 @@
# Django restframework
REST_FRAMEWORK = {
- "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
- "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
+ "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication",),
+ "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_RENDERER_CLASSES": (
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
@@ -163,3 +179,6 @@
# Limit the load on the Database returning a small number of records by default. https://github.com/nexB/vulnerablecode/issues/819
"PAGE_SIZE": 10,
}
+
+if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION:
+ REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",)