diff --git a/purl_sync/purl_sync/settings.py b/purl_sync/purl_sync/settings.py index 99e82a612..3042c2694 100644 --- a/purl_sync/purl_sync/settings.py +++ b/purl_sync/purl_sync/settings.py @@ -136,3 +136,4 @@ MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") +GIT_PATH = os.path.join(MEDIA_ROOT, "git") diff --git a/purl_sync/purl_sync/urls.py b/purl_sync/purl_sync/urls.py index 70ecfb6db..97b0168fa 100644 --- a/purl_sync/purl_sync/urls.py +++ b/purl_sync/purl_sync/urls.py @@ -18,24 +18,27 @@ from django.contrib import admin from django.urls import path -from review.views import database_admin_profile_view, login_view, security_team_signup_view, create_git_repo_view +from review.views import CreatGitView +from review.views import WebfingerView +from review.views import database_admin_profile_view +from review.views import login_view from review.views import security_team_profile_view -from review.views import review_page_view +from review.views import security_team_signup_view urlpatterns = [ path("admin/", admin.site.urls), + path(".well-known/webfinger", WebfingerView.as_view()), path("security-team/@", security_team_profile_view), path("database-admin/@", database_admin_profile_view), path("login/", login_view), path("signup/", security_team_signup_view), - path("create-repo/", create_git_repo_view), + # path("create-repo/", CreatGitView.as_view()), # path("review//", review_page_view), # path("security-team/@/edit-followers/", database_admin_profile_view), # path("/inbox/", ), # path("/outbox/", ), # path("/followers/", ), # path("/following/", ), - # path(f".well-known/webfinger?resource=acct:@{DOMAIN}", ), ] if settings.DEBUG: diff --git a/purl_sync/pyproject.toml b/purl_sync/pyproject.toml index f3805d680..9580cef5c 100644 --- a/purl_sync/pyproject.toml +++ b/purl_sync/pyproject.toml @@ -8,7 +8,7 @@ DJANGO_SETTINGS_MODULE = "purl_sync.settings" python_files = "*.py" python_classes = "Test" python_functions = "test" - +addopts = "--doctest-modules" [tool.black] line-length = 100 diff --git a/purl_sync/review/activitypub.py b/purl_sync/review/activitypub.py new file mode 100644 index 000000000..e69de29bb diff --git a/purl_sync/review/forms.py b/purl_sync/review/forms.py new file mode 100644 index 000000000..729ff820f --- /dev/null +++ b/purl_sync/review/forms.py @@ -0,0 +1,6 @@ +from django import forms + + +class CreateRepoForm(forms.Form): + repo_name = forms.CharField() + repo_url = forms.URLField(required=True) diff --git a/purl_sync/review/migrations/0001_initial.py b/purl_sync/review/migrations/0001_initial.py index 92a9fb04b..c7ea45276 100644 --- a/purl_sync/review/migrations/0001_initial.py +++ b/purl_sync/review/migrations/0001_initial.py @@ -1,18 +1,19 @@ -# Generated by Django 4.2.2 on 2023-06-26 13:44 +# Generated by Django 4.2.2 on 2023-06-29 08:43 -from django.conf import settings -import django.contrib.auth.models -from django.db import migrations, models -import django.db.models.deletion import uuid +import django.db.models.deletion +from django.conf import settings +from django.db import migrations +from django.db import models + class Migration(migrations.Migration): initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -20,14 +21,9 @@ class Migration(migrations.Migration): name="DatabaseAdmin", fields=[ ( - "user_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to=settings.AUTH_USER_MODEL, + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ( @@ -40,16 +36,24 @@ class Migration(migrations.Migration): ), ("note", models.CharField(help_text="the profile description", max_length=100)), ("public_key", models.TextField()), - ("followers", models.JSONField(default=dict)), + ( + "followers", + models.JSONField( + default=dict, + help_text="e.g. {'secrityteam@vcio': ['pkg:npm/foobar@12.3.1']}", + ), + ), ("followers_count", models.PositiveIntegerField(default=0)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], options={ "abstract": False, }, - bases=("auth.user",), - managers=[ - ("objects", django.contrib.auth.models.UserManager()), - ], ), migrations.CreateModel( name="GitRepo", @@ -108,23 +112,38 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("voter_acct", models.CharField(help_text="security@vcio", max_length=100)), - ("gainer_acct", models.CharField(help_text="security@nexb", max_length=100)), + ("voter_acct", models.CharField(help_text="security@vcio.com", max_length=100)), + ("gainer_acct", models.CharField(help_text="security@nexb.com", max_length=100)), ("positive", models.BooleanField(default=True)), ], ), + migrations.CreateModel( + name="Vulnerability", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("branch_name", models.CharField(max_length=28)), + ("filename", models.CharField(max_length=255)), + ("commit_id", models.CharField(max_length=50)), + ( + "git_repo", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="review.gitrepo" + ), + ), + ], + ), migrations.CreateModel( name="SecurityTeam", fields=[ ( - "user_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to=settings.AUTH_USER_MODEL, + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ( @@ -137,36 +156,23 @@ class Migration(migrations.Migration): ), ("note", models.CharField(help_text="the profile description", max_length=100)), ("public_key", models.TextField()), - ("following", models.JSONField(default=dict)), - ("following_count", models.PositiveIntegerField(default=0)), - ], - options={ - "abstract": False, - }, - bases=("auth.user",), - managers=[ - ("objects", django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name="Vulnerability", - fields=[ ( - "id", - models.UUIDField( - default=uuid.uuid4, editable=False, primary_key=True, serialize=False + "following", + models.JSONField( + default=dict, help_text="e.g. {'datebase1@vcio': ['pkg:npm/foobar@12.3.1']}" ), ), - ("branch_name", models.CharField(max_length=28)), - ("filename", models.CharField(max_length=255)), - ("commit_id", models.CharField(max_length=50)), + ("following_count", models.PositiveIntegerField(default=0)), ( - "git_repo", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="review.gitrepo" + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL ), ), ], + options={ + "abstract": False, + }, ), migrations.CreateModel( name="Review", @@ -177,6 +183,7 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), + ("headline", models.CharField(max_length=300)), ("data", models.TextField()), ( "status", @@ -185,7 +192,7 @@ class Migration(migrations.Migration): ), ), ( - "author", + "creator", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="review.securityteam" ), diff --git a/purl_sync/review/models.py b/purl_sync/review/models.py index 9e0a6f587..223b96a25 100644 --- a/purl_sync/review/models.py +++ b/purl_sync/review/models.py @@ -6,16 +6,17 @@ from purl_sync.settings import DOMAIN -class Actor(User): - avatar = models.ImageField(upload_to="uploads/", help_text="the profile image", default="favicon-16x16.png") +class Actor(models.Model): + avatar = models.ImageField( + upload_to="uploads/", help_text="the profile image", default="favicon-16x16.png" + ) note = models.CharField(help_text="the profile description", max_length=100) public_key = models.TextField(blank=False) - REQUIRED_FIELDS = ["username", "email", "password"] @property def acct(self): """The Webfinger account URI""" - return f"{self.username}@{DOMAIN}" + return f"{self.user.username}@{DOMAIN}" @property def reputation_value(self): @@ -34,7 +35,10 @@ class Reputation(models.Model): class DatabaseAdmin(Actor): - followers = models.JSONField(default=dict, help_text="e.g. {'secrityteam@vcio': ['pkg:npm/foobar@12.3.1']}") + user = models.OneToOneField(User, on_delete=models.CASCADE) + followers = models.JSONField( + default=dict, help_text="e.g. {'secrityteam@vcio': ['pkg:npm/foobar@12.3.1']}" + ) followers_count = models.PositiveIntegerField(default=0) def save(self, *args, **kwargs): @@ -64,7 +68,10 @@ class PackageUrl(models.Model): class SecurityTeam(Actor): - following = models.JSONField(default=dict, help_text="e.g. {'datebase1@vcio': ['pkg:npm/foobar@12.3.1']}") + user = models.OneToOneField(User, on_delete=models.CASCADE) + following = models.JSONField( + default=dict, help_text="e.g. {'datebase1@vcio': ['pkg:npm/foobar@12.3.1']}" + ) following_count = models.PositiveIntegerField(default=0) def save(self, *args, **kwargs): @@ -81,8 +88,11 @@ class Notes(models.Model): class Review(models.Model): - author = models.ForeignKey(SecurityTeam, on_delete=models.CASCADE, help_text="") + headline = models.CharField(max_length=300) + creator = models.ForeignKey(SecurityTeam, on_delete=models.CASCADE, help_text="") + git_repo = models.ForeignKey(GitRepo, on_delete=models.CASCADE, help_text="") + data = models.TextField() notes = models.ManyToManyField(Notes) @@ -107,7 +117,7 @@ class Meta: class RemoteSecurityTeam(RemoteActor): @property - def get_data(self): + def get_profile_data(self): raise NotImplementedError @property @@ -117,7 +127,7 @@ def create_activity(self): class RemoteDatabaseAdmin(RemoteActor): @property - def get_data(self): + def get_profile_data(self): raise NotImplementedError @property diff --git a/purl_sync/review/templates/webfinger.json b/purl_sync/review/templates/webfinger.json new file mode 100644 index 000000000..904817033 --- /dev/null +++ b/purl_sync/review/templates/webfinger.json @@ -0,0 +1,15 @@ +{ + "subject": "{{ resource }}", + "links": [ + { + "rel": "https://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://{{ domain }}/{{ user_type }}/@{{ username }}" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://{{ domain }}/users/{{ user_type }}/{{ username }}" + } + ] +} diff --git a/purl_sync/review/tests/test_models.py b/purl_sync/review/tests/test_models.py index 1a9eac392..417220c3b 100644 --- a/purl_sync/review/tests/test_models.py +++ b/purl_sync/review/tests/test_models.py @@ -1,15 +1,51 @@ import pytest +from django.contrib.auth.models import User -from ..models import DatabaseAdmin, GitRepo +from ..models import DatabaseAdmin +from ..models import GitRepo from ..models import SecurityTeam +@pytest.fixture +def security_team(db): + user1 = User.objects.create( + username="security-team", + email="security@nexb.com", + password="complex_password", + ) + + return SecurityTeam.objects.create( + user=user1, + note="Hello security team", + public_key="public_key", + following={ + "datebase1@vcio": ["pkg:npm/foobar@12.3.1"], + "remote_datebase2@vcio": ["pkg:pypi/django@1.11.1", "pkg:npm/foobar@12.3.1"], + }, + ) + + +def test_security_team(security_team): + assert security_team.user.username == "security-team" + assert security_team.user.email == "security@nexb.com" + assert security_team.public_key == "public_key" + assert security_team.note == "Hello security team" + assert security_team.following == { + "datebase1@vcio": ["pkg:npm/foobar@12.3.1"], + "remote_datebase2@vcio": ["pkg:pypi/django@1.11.1", "pkg:npm/foobar@12.3.1"], + } + assert security_team.following_count == 2 + + @pytest.fixture def database_admin(db): - return DatabaseAdmin.objects.create( + user2 = User.objects.create( username="vulnerablecode", email="vcio@nexb.com", password="complex_password", + ) + return DatabaseAdmin.objects.create( + user=user2, public_key="PUBLIC_KEY", note="Hello vulnerablecode", followers={ @@ -20,24 +56,9 @@ def database_admin(db): ) -@pytest.fixture -def security_team(db): - return SecurityTeam.objects.create( - username="security-team", - email="security@nexb.com", - password="complex_password", - note="Hello security team", - public_key="public_key", - following={ - "datebase1@vcio": ["pkg:npm/foobar@12.3.1"], - "remote_datebase2@vcio": ["pkg:pypi/django@1.11.1", "pkg:npm/foobar@12.3.1"], - }, - ) - - -def test_create_database_admin(database_admin): - assert database_admin.username == "vulnerablecode" - assert database_admin.email == "vcio@nexb.com" +def test_database_admin(database_admin): + assert database_admin.user.username == "vulnerablecode" + assert database_admin.user.email == "vcio@nexb.com" assert database_admin.public_key == "PUBLIC_KEY" assert database_admin.note == "Hello vulnerablecode" assert database_admin.followers == { @@ -48,25 +69,11 @@ def test_create_database_admin(database_admin): assert database_admin.followers_count == 3 -def test_create_security_team(security_team): - assert security_team.username == "security-team" - assert security_team.email == "security@nexb.com" - assert security_team.public_key == "public_key" - assert security_team.note == "Hello security team" - assert security_team.following == { - "datebase1@vcio": ["pkg:npm/foobar@12.3.1"], - "remote_datebase2@vcio": ["pkg:pypi/django@1.11.1", "pkg:npm/foobar@12.3.1"], - } - assert security_team.following_count == 2 - - @pytest.fixture def git_repo(db, database_admin): return GitRepo.objects.create( name="vulnerablecode_data", url="https://github.com/nexB/vulnerablecode-data", path="/home/ziad/vulnerablecode_data", - admin=database_admin + admin=database_admin, ) - - diff --git a/purl_sync/review/tests/test_utils.py b/purl_sync/review/tests/test_utils.py index 621c5ca15..b15b3d336 100644 --- a/purl_sync/review/tests/test_utils.py +++ b/purl_sync/review/tests/test_utils.py @@ -1,14 +1,13 @@ import pytest -from review.utils import create_git_repo +from review.utils import clone_git_repo def test_create_git_repo(tmp_path): - repo = create_git_repo( + repo = clone_git_repo( tmp_path, - "/test_vulnerablecode_data/", + "test_vulnerablecode_data", "https://github.com/nexB/vulnerablecode-data", - "main", ) assert repo.remotes.origin.url == "https://github.com/nexB/vulnerablecode-data" diff --git a/purl_sync/review/utils.py b/purl_sync/review/utils.py index 4a5be450a..41e097be2 100644 --- a/purl_sync/review/utils.py +++ b/purl_sync/review/utils.py @@ -8,18 +8,32 @@ # import os -import git +from git.repo.base import Repo def generate_keys(): raise NotImplementedError -def create_git_repo(repo_path, repo_name, repo_url, branch): +def parse_webfinger(subject): + """ + get the username and host from webfinger acct:user@host + >>> parse_webfinger("acct:ziadhany@example.com") + ('ziadhany', 'example.com') + >>> parse_webfinger("acct:") + ('', '') + """ + acct = subject[5:] + result = acct.split("@") + user_part, host = "", "" + if len(result) == 2: + user_part, host = result + return user_part, host + + +def clone_git_repo(repo_path, repo_name, repo_url): """ create Git repository in ${repo_path}/${repo_name}.git and git pull origin branch """ - repo = git.Repo.init(str(repo_path) + repo_name) - origin = repo.create_remote("origin", repo_url) - origin.pull(branch) + repo = Repo.clone_from(repo_url, os.path.join(repo_path, repo_name)) return repo diff --git a/purl_sync/review/views.py b/purl_sync/review/views.py index 848675f81..8c97f4480 100644 --- a/purl_sync/review/views.py +++ b/purl_sync/review/views.py @@ -1,11 +1,21 @@ -from django.http import Http404, HttpResponse +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.http import Http404 +from django.http import HttpResponse +from django.http import HttpResponseBadRequest from django.http import JsonResponse from django.shortcuts import render +from django.views import View +from purl_sync.settings import DOMAIN +from purl_sync.settings import GIT_PATH + +from .forms import CreateRepoForm from .models import DatabaseAdmin from .models import GitRepo from .models import SecurityTeam -from .utils import create_git_repo +from .utils import clone_git_repo +from .utils import parse_webfinger def database_admin_profile_view(request, username): @@ -16,20 +26,11 @@ def database_admin_profile_view(request, username): raise Http404("No Database Admin matches the given query.") repository_list = GitRepo.objects.filter(admin=database_admin) - if request.content_type == 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"': - return JsonResponse({ - "@context": "https://www.w3.org/ns/activitystreams", - "type": "Person", - "id": "https://social.example/alyssa/", - "name": "Alyssa P. Hacker", - "preferredUsername": "alyssa", - "summary": "Lisp enthusiast hailing from MIT", - "inbox": "https://social.example/alyssa/inbox/", - "outbox": "https://social.example/alyssa/outbox/", - "followers": "https://social.example/alyssa/followers/", - "following": "https://social.example/alyssa/following/", - "liked": "https://social.example/alyssa/liked/" - }) + if ( + request.content_type + == 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + ): + return JsonResponse() else: return render( @@ -56,35 +57,84 @@ def security_team_profile_view(request, username): # require auth and database admin person -def create_git_repo_view(request): - path = "/home/ziad" - - repo = create_git_repo( - path, - "/test_vulnerablecode_data_new/", - "https://github.com/nexB/vulnerablecode", - "main", - ) - - database_admin_actor = DatabaseAdmin.objects.get(username="vulnerablecode") - - GitRepo.objects.create( - name="vulnerablecode_data", - url="https://github.com/nexB/vulnerablecode-data", - path="/home/ziad/test_vulnerablecode_data_new", - admin=database_admin_actor - ) +@login_required +class CreatGitView(View): + form_class = CreateRepoForm + template_name = "form_template.html" + + def post(self, request): + form = self.form_class(request.POST) + + if form.is_valid(): + database_admin_actor = DatabaseAdmin.objects.get(username="vulnerablecode") + clone_git_repo(GIT_PATH, form.repo_name, form.repo_url) + GitRepo.objects.create( + name=form.repo_name, + url=form.repo_url, + path=GIT_PATH, + admin=database_admin_actor, + ) + return HttpResponse("Git Repo created successfully", content_type="text/plain") - return HttpResponse("Git Repo created successfully", content_type="text/plain") + return render(request, self.template_name, {"form": form}) def review_page_view(request, id): - return render(request, 'review.html') + return render(request, "review.html") def login_view(request): - return render(request, 'login.html') + return render(request, "login.html") + + +def security_team_signup_view(request): + return render(request, "security_team_signup.html") def security_team_signup_view(request): - return render(request, 'security_team_signup.html') + return render(request, "security_team_signup.html") + + +class WebfingerView(View): + def get(self, request): + resource = request.GET.get("resource") + + if not resource: + return HttpResponseBadRequest("No resource specified") + + username, domain = parse_webfinger(resource) + + if DOMAIN != domain or not username: + return HttpResponseBadRequest("Invalid domain specified") + + try: + actor = User.objects.get(username=username) + except User.DoesNotExist: + return HttpResponseBadRequest("Not an account resource") + + if hasattr(actor, "databaseadmin"): + return render( + request, + "webfinger.json", + status=200, + content_type="application/json", + context={ + "resource": resource, + "domain": DOMAIN, + "username": username, + "user_type": "database-admin", + }, + ) + else: + return render( + request, + "webfinger.json", + status=200, + content_type="application/json", + context={ + "resource": resource, + "domain": DOMAIN, + "username": username, + "user_type": "security-team", + }, + )