Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitor new packages that might be typosquats #5001

Merged
merged 5 commits into from
Nov 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,6 @@ def test_includeme():
),
pretend.call("admin.flags", "/admin/flags/", domain=warehouse),
pretend.call("admin.flags.edit", "/admin/flags/edit/", domain=warehouse),
pretend.call("admin.squats", "/admin/squats/", domain=warehouse),
pretend.call("admin.squats.review", "/admin/squats/review/", domain=warehouse),
]
24 changes: 21 additions & 3 deletions tests/unit/admin/views/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime

import pretend
import pytest
import uuid
Expand Down Expand Up @@ -88,12 +90,28 @@ def test_gets_project(self, db_request):
[RoleFactory(project=project) for _ in range(5)],
key=lambda x: (x.role_name, x.user.username),
)
delta = datetime.timedelta(days=1)
squatter = ProjectFactory(
name=project.name[:-1], created=project.created + delta
)
squattee = ProjectFactory(
name=project.name[1:], created=project.created - delta
)
db_request.db.add(squatter)
db_request.db.add(squattee)
db_request.matchdict["project_name"] = str(project.normalized_name)
result = views.project_detail(project, db_request)

assert result["project"] == project
assert result["maintainers"] == roles
assert result["journal"] == journals[:30]
assert result == {
"project": project,
"releases": [],
"maintainers": roles,
"journal": journals[:30],
"squatters": [squatter],
"squattees": [squattee],
"ONE_MB": views.ONE_MB,
"MAX_FILESIZE": views.MAX_FILESIZE,
}

def test_non_normalized_name(self, db_request):
project = ProjectFactory.create()
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/admin/views/test_squats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from warehouse.admin.squats import Squat
from warehouse.admin.views import squats as views

from ....common.db.packaging import ProjectFactory


class TestGetSquats:
def test_get_squats(self, db_request):
project_a = ProjectFactory()
project_b = ProjectFactory()
project_c = ProjectFactory()
squat = Squat(squattee=project_a, squatter=project_b)
reviewed_squat = Squat(squattee=project_a, squatter=project_c, reviewed=True)
db_request.db.add(squat)
db_request.db.add(reviewed_squat)

assert views.get_squats(db_request) == {"squats": [squat]}


class TestReviewSquat:
def test_review_squat(self, db_request):
squat = Squat(squattee=ProjectFactory(), squatter=ProjectFactory())
db_request.db.add(squat)

db_request.db.flush()

db_request.POST = {"id": squat.id}
db_request.route_path = lambda *a: "/the/redirect"
db_request.flash = lambda *a: None

views.review_squat(db_request)

db_request.db.flush()

assert squat.reviewed is True
45 changes: 45 additions & 0 deletions tests/unit/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from wtforms.form import Form
from wtforms.validators import ValidationError

from warehouse.admin.squats import Squat
from warehouse.classifiers.models import Classifier
from warehouse.forklift import legacy
from warehouse.packaging.interfaces import IFileStorage
Expand Down Expand Up @@ -2779,6 +2780,50 @@ def test_upload_succeeds_creates_project(self, pyramid_config, db_request):
),
]

def test_upload_succeeds_creates_squats(self, pyramid_config, db_request):
pyramid_config.testing_securitypolicy(userid=1)

squattee = ProjectFactory(name="example")
user = UserFactory.create()
EmailFactory.create(user=user)

filename = "{}-{}.tar.gz".format("exmaple", "1.0")

db_request.user = user
db_request.POST = MultiDict(
{
"metadata_version": "1.2",
"name": "exmaple",
"version": "1.0",
"filetype": "sdist",
"md5_digest": "335c476dc930b959dda9ec82bd65ef19",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"A fake file."),
type="application/tar",
),
}
)

storage_service = pretend.stub(store=lambda path, filepath, meta: None)
db_request.find_service = lambda svc, name=None: storage_service
db_request.remote_addr = "10.10.10.10"
db_request.user_agent = "warehouse-tests/6.6.6"

resp = legacy.file_upload(db_request)

assert resp.status_code == 200

# Ensure that a Project object has been created.
squatter = db_request.db.query(Project).filter(Project.name == "exmaple").one()

# Ensure that a Squat object has been created.
squat = db_request.db.query(Squat).one()

assert squat.squattee == squattee
assert squat.squatter == squatter
assert squat.reviewed is False

@pytest.mark.parametrize(
("emails_verified", "expected_success"),
[
Expand Down
4 changes: 4 additions & 0 deletions warehouse/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ def includeme(config):
# Flags
config.add_route("admin.flags", "/admin/flags/", domain=warehouse)
config.add_route("admin.flags.edit", "/admin/flags/edit/", domain=warehouse)

# Squats
config.add_route("admin.squats", "/admin/squats/", domain=warehouse)
config.add_route("admin.squats.review", "/admin/squats/review/", domain=warehouse)
35 changes: 35 additions & 0 deletions warehouse/admin/squats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Text
from sqlalchemy import orm, sql

from warehouse import db


class Squat(db.ModelBase):

__tablename__ = "warehouse_admin_squat"

id = Column(Integer, primary_key=True, nullable=False)
created = Column(
DateTime(timezone=False), nullable=False, server_default=sql.func.now()
)
squatter_name = Column(
Text, ForeignKey("packages.name", onupdate="CASCADE", ondelete="CASCADE")
)
squattee_name = Column(
Text, ForeignKey("packages.name", onupdate="CASCADE", ondelete="CASCADE")
)
squatter = orm.relationship("Project", foreign_keys=[squatter_name], lazy=False)
squattee = orm.relationship("Project", foreign_keys=[squattee_name], lazy=False)
reviewed = Column(Boolean, nullable=False, server_default=sql.false())
5 changes: 5 additions & 0 deletions warehouse/admin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@
<i class="fa fa-flag"></i> <span>Flags</span>
</a>
</li>
<li>
<a href="{{ request.route_path('admin.squats') }}">
<i class="fa fa-dumbbell"></i> <span>Squats</span>
</a>
</li>
</ul>
</section>
</aside>
Expand Down
47 changes: 47 additions & 0 deletions warehouse/admin/templates/admin/projects/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<div class="box box-primary">
<div class="box-body box-profile">
<h3 class="project-name text-center">{{ project.name }}</h3>
<h4>Attributes:</h4>
<div class="box-body box-attributes">
<table class="table table-hover">
<tr>
Expand All @@ -64,6 +65,7 @@ <h3 class="project-name text-center">{{ project.name }}</h3>
</tr>
</table>
</div>
<h4>Maintainers:</h4>
<div class="box-body box-maintainers">
<table class="table table-hover">
<tr>
Expand Down Expand Up @@ -135,6 +137,7 @@ <h4 class="modal-title" id="exampleModalLabel">Remove role for {{ role.user_name
</tr>
</table>
</div>
<h4>Releases:</h4>
<div class="box-body box-releases">
<table class="table table-hover">
<th>Release version</th>
Expand All @@ -152,6 +155,7 @@ <h4 class="modal-title" id="exampleModalLabel">Remove role for {{ role.user_name
</table>
<a href="{{ request.route_path('admin.project.releases', project_name=project.normalized_name) }}">All releases</a>
</div>
<h4>Journals:</h4>
<div class="box-body box-journals">
<table class="table table-hover">
<tr>
Expand All @@ -175,6 +179,49 @@ <h4 class="modal-title" id="exampleModalLabel">Remove role for {{ role.user_name
</table>
<a href="{{ request.route_path('admin.project.journals', project_name=project.normalized_name) }}">All journals</a>
</div>
{% if squatters %}
<h4>Projects that might be squatting on this project:</h4>
<div class="box-body box-squats">
<table class="table table-hover">
<tr>
<th>name</th>
<th>created</th>
</tr>
{% for project in squatters %}
<tr>
<td>
<a href="{{ request.route_path('admin.project.detail', project_name=project.normalized_name) }}">
{{ project.normalized_name }}
</a>
</td>
<td>{{ project.created }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}

{% if squattees %}
<h4>Projects that this project might be squatting on:</h4>
<div class="box-body box-squats">
<table class="table table-hover">
<tr>
<th>name</th>
<th>created</th>
</tr>
{% for project in squattees %}
<tr>
<td>
<a href="{{ request.route_path('admin.project.detail', project_name=project.normalized_name) }}">
{{ project.normalized_name }}
</a>
</td>
<td>{{ project.created }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
</div>
</div>
Expand Down
80 changes: 80 additions & 0 deletions warehouse/admin/templates/admin/squats/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "admin/base.html" %}

{% import "admin/utils/pagination.html" as pagination %}

{% block title %}Squats{% endblock %}

{% block breadcrumb %}
<li class="active">Squats</li>
{% endblock %}

{% set csrf_token = request.session.get_csrf_token() %}

{% block content %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">List Squats</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>Potential Squatter</th>
<th>Squatting on</th>
<th>Created</th>
<th>Created by</th>
<th></th>
</tr>
{% for squat in squats %}
<tr>
<td>
<a href="{{ request.route_path('admin.project.detail', project_name=squat.squatter.normalized_name) }}">
{{ squat.squatter.normalized_name }}
</a>
</td>
<td>
<a href="{{ request.route_path('admin.project.detail', project_name=squat.squattee.normalized_name) }}">
{{ squat.squattee.normalized_name }}
</a>
</td>
<td>{{ squat.created }}</td>
<td>
{% for user in squat.squatter.users %}
<a href="{{ request.route_path("admin.user.detail", user_id=user.id) }}">
{{ user.username }}
</a>{{ "," if not loop.last }}
{% endfor %}
</td>
<td>
<form method="POST" action="{{ request.route_path('admin.squats.review') }}">
<input type="hidden" name="id" value="{{ squat.id }}">
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
<input type="submit" value="Mark as reviewed">
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5">
<center>
<i>No squats!</i>
</center>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock content %}
Loading