From ca58c70206dc3226d66e028ce6160d331c8eb2b7 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Tue, 2 Feb 2021 22:34:51 +0000 Subject: [PATCH] Add validation to package API --- app/blueprints/packages/packages.py | 6 +- app/flatpages/help/api.md | 2 +- app/logic/packages.py | 107 ++++++++++++++++++++++++---- app/models/packages.py | 1 - requirements.txt | 33 +++++---- 5 files changed, 116 insertions(+), 33 deletions(-) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 77a5d04..53d636a 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -227,13 +227,11 @@ class PackageForm(FlaskForm): media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=makeLabel) content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel) - # harddep_str = StringField("Hard Dependencies", [Optional()]) - # softdep_str = StringField("Soft Dependencies", [Optional()]) repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None]) website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None]) - forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) - submit = SubmitField("Save") + forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) + submit = SubmitField("Save") @bp.route("/packages/new/", methods=["GET", "POST"]) diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index 3398d07..61f21a9 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -24,7 +24,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). * PUT `/api/packages///` (Update) * JSON dictionary with any of these keys (all are optional): * `title`: Human-readable title. - * `short_desc` + * `short_description` * `desc` * `type`: One of `GAME`, `MOD`, `TXP`. * `license`: A license name. diff --git a/app/logic/packages.py b/app/logic/packages.py index a1d69e4..40b55a9 100644 --- a/app/logic/packages.py +++ b/app/logic/packages.py @@ -15,24 +15,96 @@ # along with this program. If not, see . +import re, validators from app.logic.LogicError import LogicError -from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, NotificationType, AuditSeverity +from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, NotificationType, AuditSeverity, License from app.utils import addNotification, addAuditLog +def check(cond: bool, msg: str): + if not cond: + raise LogicError(400, msg) + + +def get_license(name): + if type(name) == License: + return name + + license = License.query.filter(License.name.ilike(name)).first() + if license is None: + raise LogicError(400, "Unknown license: " + name) + return license + + +name_re = re.compile("^[a-z0-9_]+$") + +TYPES = { + "name": str, + "title": str, + "short_description": str, + "short_desc": str, + "desc": str, + "tags": list, + "content_warnings": list, + "repo": str, + "website": str, + "issue_tracker": str, + "issueTracker": str, + "forums": int, +} + +def is_int(val): + try: + int(val) + return True + except ValueError: + return False + + +def validate(data: dict): + for key, typ in TYPES.items(): + if data.get(key) is not None: + check(isinstance(data[key], typ), key + " must be a " + typ.__name__) + + if "name" in data: + name = data["name"] + check(isinstance(name, str), "Name must be a string") + check(bool(name_re.match(name)), + "Name can only contain lower case letters (a-z), digits (0-9), and underscores (_)") + + for key in ["repo", "website", "issue_tracker", "issueTracker"]: + value = data.get(key) + if value is not None: + check(value.startswith("http://") or value.startswith("https://"), + key + " must start with http:// or https://") + + check(validators.url(value, public=True), key + " must be a valid URL") + + def do_edit_package(user: User, package: Package, was_new: bool, data: dict, reason: str = None): + if not package.checkPerm(user, Permission.EDIT_PACKAGE): + raise LogicError(403, "You do not have permission to edit this package") + if "name" in data and package.name != data["name"] and \ not package.checkPerm(user, Permission.CHANGE_NAME): raise LogicError(403, "You do not have permission to change the package name") - if not package.checkPerm(user, Permission.EDIT_PACKAGE): - raise LogicError(403, "You do not have permission to edit this package") - - for alias, to in { "short_description": "short_desc" }.items(): + for alias, to in { "short_description": "short_desc", "issue_tracker": "issueTracker" }.items(): if alias in data: data[to] = data[alias] + validate(data) + + if "type" in data: + data["type"] = PackageType.coerce(data["type"]) + + if "license" in data: + data["license"] = get_license(data["license"]) + + if "media_license" in data: + data["media_license"] = get_license(data["media_license"]) + for key in ["name", "title", "short_desc", "desc", "type", "license", "media_license", "repo", "website", "issueTracker", "forums"]: if key in data: @@ -45,16 +117,27 @@ def do_edit_package(user: User, package: Package, was_new: bool, data: dict, rea m = MetaPackage.GetOrCreate(package.name, {}) package.provides.append(m) - package.tags.clear() - - if "tag" in data: - for tag in data["tag"]: - package.tags.append(Tag.query.get(tag)) + if "tags" in data: + package.tags.clear() + for tag_id in data["tags"]: + if is_int(tag_id): + package.tags.append(Tag.query.get(tag_id)) + else: + tag = Tag.query.filter_by(name=tag_id).first() + if tag is None: + raise LogicError(400, "Unknown tag: " + tag_id) + package.tags.append(tag) if "content_warnings" in data: package.content_warnings.clear() - for warning in data["content_warnings"]: - package.content_warnings.append(ContentWarning.query.get(warning)) + for warning_id in data["content_warnings"]: + if is_int(warning_id): + package.content_warnings.append(ContentWarning.query.get(warning_id)) + else: + warning = ContentWarning.query.filter_by(name=warning_id).first() + if warning is None: + raise LogicError(400, "Unknown warning: " + warning_id) + package.content_warnings.append(warning) if not was_new: if reason is None: diff --git a/app/models/packages.py b/app/models/packages.py index 110f03d..ac1bbfb 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -17,7 +17,6 @@ import datetime import enum -from urllib.parse import urlparse from flask import url_for from flask_sqlalchemy import BaseQuery diff --git a/requirements.txt b/requirements.txt index 41b26d3..a3459fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Flask +Flask~=1.1.2 Flask-FlatPages Flask-Gravatar Flask-Login @@ -12,24 +12,24 @@ GitHub-Flask SQLAlchemy-Searchable bcrypt -markdown -bleach -passlib +markdown~=3.2.2 +bleach~=3.1.5 +passlib~=1.7.2 pygments -beautifulsoup4 -celery -kombu +beautifulsoup4~=4.9.1 +celery~=4.4.6 +kombu~=4.6.11 GitPython git-archive-all lxml -pillow +pillow~=7.2.0 pyScss -redis +redis~=3.5.3 psycopg2 -pytest +pytest~=5.4.3 pytest-cov email_validator @@ -38,8 +38,11 @@ pyyaml ua-parser user-agents -Werkzeug -WTForms -SQLAlchemy -requests -alembic +Werkzeug~=1.0.1 +WTForms~=2.3.1 +SQLAlchemy~=1.3.18 +requests~=2.24.0 +alembic~=1.4.2 + +validators~=0.16.0 +gitdb~=4.0.5