From ad42bf4448a9304e5079fe64f3d21d61db1732c5 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Thu, 24 Feb 2022 19:26:22 -0500 Subject: [PATCH] gitsrht: Store visibility as enum instead of varchar Add a 'visibility' enum type to the database and use it for the repository.visibility column. This required changes to scm.sr.ht code. Instead of updating scm.sr.ht, most of the scm.sr.ht code that git.sr.ht uses was moved to git.sr.ht. --- api/graph/model/repository.go | 22 +-- api/graph/schema.resolvers.go | 29 +--- api/loaders/middleware.go | 6 +- api/webhooks/legacy.go | 5 +- gitsrht-periodic | 3 +- gitsrht-shell/main.go | 10 +- gitsrht-update-hook/submitter.go | 2 +- gitsrht/access.py | 93 ++++++++++++ .../64fcd80183c8_add_visibility_enum.py | 37 +++++ gitsrht/app.py | 30 ++-- gitsrht/blueprints/api/__init__.py | 4 +- gitsrht/blueprints/api/info.py | 14 +- gitsrht/blueprints/api/porcelain.py | 2 +- gitsrht/blueprints/artifacts.py | 2 +- gitsrht/blueprints/auth.py | 22 +++ gitsrht/blueprints/email.py | 2 +- gitsrht/blueprints/manage.py | 38 ++--- gitsrht/blueprints/public.py | 54 +++++++ gitsrht/blueprints/repo.py | 2 +- gitsrht/repos.py | 138 +++++++++--------- gitsrht/templates/base.html | 5 + gitsrht/templates/dashboard.html | 4 +- gitsrht/templates/repo.html | 12 +- gitsrht/templates/settings_info.html | 6 +- gitsrht/templates/summary.html | 4 +- gitsrht/templates/user.html | 4 +- gitsrht/types/__init__.py | 95 ++++++++++-- 27 files changed, 451 insertions(+), 194 deletions(-) create mode 100644 gitsrht/access.py create mode 100644 gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py create mode 100644 gitsrht/blueprints/auth.py create mode 100644 gitsrht/blueprints/public.py create mode 100644 gitsrht/templates/base.html diff --git a/api/graph/model/repository.go b/api/graph/model/repository.go index 7a4d1e6..2af93ec 100644 --- a/api/graph/model/repository.go +++ b/api/graph/model/repository.go @@ -3,7 +3,6 @@ package model import ( "context" "database/sql" - "fmt" "strconv" "time" @@ -23,28 +22,15 @@ type Repository struct { Description *string `json:"description"` Readme *string `json:"readme"` - Path string - OwnerID int - RawVisibility string + Path string + OwnerID int + Visibility Visibility alias string repo *RepoWrapper fields *database.ModelFields } -func (r *Repository) Visibility() Visibility { - visMap := map[string]Visibility{ - "public": VisibilityPublic, - "unlisted": VisibilityUnlisted, - "private": VisibilityPrivate, - } - vis, ok := visMap[r.RawVisibility] - if !ok { - panic(fmt.Errorf("Invalid repo visibility %s", r.RawVisibility)) // Invariant - } - return vis -} - func (r *Repository) Repo() *RepoWrapper { if r.repo != nil { return r.repo @@ -94,7 +80,7 @@ func (r *Repository) Fields() *database.ModelFields { {"updated", "updated", &r.Updated}, {"name", "name", &r.Name}, {"description", "description", &r.Description}, - {"visibility", "visibility", &r.RawVisibility}, + {"visibility", "visibility", &r.Visibility}, {"readme", "readme", &r.Readme}, // Always fetch: diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 3c8f402..4588ca5 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -115,19 +115,6 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi }() if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { - vismap := map[model.Visibility]string{ - model.VisibilityPublic: "public", - model.VisibilityUnlisted: "unlisted", - model.VisibilityPrivate: "private", - } - var ( - dvis string - ok bool - ) - if dvis, ok = vismap[visibility]; !ok { - panic(fmt.Errorf("Unknown visibility %s", visibility)) // Invariant - } - cloneStatus := CloneNone if cloneURL != nil { cloneStatus = CloneInProgress @@ -144,9 +131,9 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi ) RETURNING id, created, updated, name, description, visibility, path, owner_id; - `, name, description, repoPath, dvis, user.UserID, cloneStatus) + `, name, description, repoPath, visibility, user.UserID, cloneStatus) if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, &repo.Name, - &repo.Description, &repo.RawVisibility, + &repo.Description, &repo.Visibility, &repo.Path, &repo.OwnerID); err != nil { if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { return valid.Errorf(ctx, "name", "A repository with this name already exists.") @@ -184,7 +171,7 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi } export := path.Join(repoPath, "git-daemon-export-ok") - if repo.Visibility() != model.VisibilityPrivate { + if repo.Visibility != model.VisibilityPrivate { _, err := os.Create(export) if err != nil { return err @@ -361,7 +348,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m valid.OptionalString("visibility", func(vis string) { valid.Expect(model.Visibility(vis).IsValid(), "Invalid visibility '%s'", vis) - query = query.Set(`visibility`, strings.ToLower(vis)) // TODO: Can we use uppercase here? + query = query.Set(`visibility`, vis) }) valid.NullableString("readme", func(readme *string) { @@ -386,7 +373,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m row := query.RunWith(tx).QueryRowContext(ctx) if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, - &repo.Name, &repo.Description, &repo.RawVisibility, + &repo.Name, &repo.Description, &repo.Visibility, &repo.Path, &repo.OwnerID); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("No repository by ID %d found for this user", id) @@ -418,7 +405,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m } export := path.Join(repo.Path, "git-daemon-export-ok") - if repo.Visibility() == model.VisibilityPrivate { + if repo.Visibility == model.VisibilityPrivate { err := os.Remove(export) if err != nil && !errors.Is(err, os.ErrNotExist) { return err @@ -476,7 +463,7 @@ func (r *mutationResolver) DeleteRepository(ctx context.Context, id int) (*model `, id, auth.ForContext(ctx).UserID) if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, - &repo.Name, &repo.Description, &repo.RawVisibility, + &repo.Name, &repo.Description, &repo.Visibility, &repo.Path, &repo.OwnerID); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("No repository by ID %d found for this user", id) @@ -1302,7 +1289,7 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor sq.Or{ sq.Expr(`? IN (access.user_id, repo.owner_id)`, auth.ForContext(ctx).UserID), - sq.Expr(`repo.visibility = 'public'`), + sq.Expr(`repo.visibility = 'PUBLIC'`), }, sq.Expr(`repo.owner_id = ?`, obj.ID), }) diff --git a/api/loaders/middleware.go b/api/loaders/middleware.go index 79d0fb8..8535bf1 100644 --- a/api/loaders/middleware.go +++ b/api/loaders/middleware.go @@ -138,7 +138,7 @@ func fetchRepositoriesByID(ctx context.Context) func(ids []int) ([]*model.Reposi sq.Expr(`repo.id = ANY(?)`, pq.Array(ids)), sq.Or{ sq.Expr(`? IN (access.user_id, repo.owner_id)`, auser.UserID), - sq.Expr(`repo.visibility != 'private'`), + sq.Expr(`repo.visibility != 'PRIVATE'`), }, }) if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil { @@ -205,7 +205,7 @@ func fetchRepositoriesByOwnerRepoName(ctx context.Context) func([]OwnerRepoName) Where(sq.Or{ sq.Expr(`? IN (access.user_id, repo.owner_id)`, auth.ForContext(ctx).UserID), - sq.Expr(`repo.visibility != 'private'`), + sq.Expr(`repo.visibility != 'PRIVATE'`), }) if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil { panic(err) @@ -272,7 +272,7 @@ func fetchRepositoriesByOwnerIDRepoName(ctx context.Context) func([]OwnerIDRepoN Where(sq.Or{ sq.Expr(`? IN (access.user_id, repo.owner_id)`, auth.ForContext(ctx).UserID), - sq.Expr(`repo.visibility != 'private'`), + sq.Expr(`repo.visibility != 'PRIVATE'`), }) if rows, err = query.RunWith(tx).QueryContext(ctx); err != nil { panic(err) diff --git a/api/webhooks/legacy.go b/api/webhooks/legacy.go index 39dcb9e..97034c9 100644 --- a/api/webhooks/legacy.go +++ b/api/webhooks/legacy.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "strings" "time" "git.sr.ht/~sircmpwn/core-go/auth" @@ -35,7 +36,7 @@ func DeliverLegacyRepoCreate(ctx context.Context, repo *model.Repository) { Updated: repo.Created, Name: repo.Name, Description: repo.Description, - Visibility: repo.RawVisibility, + Visibility: strings.ToLower(repo.Visibility.String()), } // TODO: User groups @@ -69,7 +70,7 @@ func DeliverLegacyRepoUpdate(ctx context.Context, repo *model.Repository) { Updated: repo.Created, Name: repo.Name, Description: repo.Description, - Visibility: repo.RawVisibility, + Visibility: strings.ToLower(repo.Visibility.String()), } // TODO: User groups diff --git a/gitsrht-periodic b/gitsrht-periodic index 8f153f0..ef92ede 100755 --- a/gitsrht-periodic +++ b/gitsrht-periodic @@ -11,13 +11,12 @@ from prometheus_client import CollectorRegistry, Gauge from prometheus_client.context_managers import Timer from srht.config import cfg from srht.database import DbSession -from gitsrht.types import Artifact, User, Repository, RepoVisibility +from gitsrht.types import Artifact, User, Repository from minio import Minio from datetime import datetime, timedelta db = DbSession(cfg("git.sr.ht", "connection-string")) db.init() -repo_api = gr.GitRepoApi() registry = CollectorRegistry() tg = Gauge("gitsrht_periodic_time", diff --git a/gitsrht-shell/main.go b/gitsrht-shell/main.go index 32b2b5a..b79417d 100644 --- a/gitsrht-shell/main.go +++ b/gitsrht-shell/main.go @@ -152,7 +152,7 @@ func main() { // Fetch the necessary info from SQL. This first query fetches: // - // 1. Repository information, such as visibility (public|unlisted|private) + // 1. Repository information, such as visibility (PUBLIC|UNLISTED|PRIVATE) // 2. Information about the repository owner's account // 3. Information about the pusher's account // 4. Any access control policies for that repo that apply to the pusher @@ -266,7 +266,7 @@ func main() { repoOwnerId = pusherId repoOwnerName = pusherName - repoVisibility = "private" + repoVisibility = "PRIVATE" query := client.GraphQLQuery{ Query: ` @@ -339,11 +339,11 @@ func main() { } else { if accessGrant == nil { switch repoVisibility { - case "public": + case "PUBLIC": fallthrough - case "unlisted": + case "UNLISTED": hasAccess = ACCESS_READ - case "private": + case "PRIVATE": fallthrough default: hasAccess = ACCESS_NONE diff --git a/gitsrht-update-hook/submitter.go b/gitsrht-update-hook/submitter.go index 523981f..bbce5f8 100644 --- a/gitsrht-update-hook/submitter.go +++ b/gitsrht-update-hook/submitter.go @@ -232,7 +232,7 @@ func (submitter GitBuildSubmitter) GetCommitNote() string { } func (submitter GitBuildSubmitter) GetCloneUrl() string { - if submitter.Visibility == "private" { + if submitter.Visibility == "PRIVATE" { origin := strings.ReplaceAll(submitter.GitOrigin, "http://", "") origin = strings.ReplaceAll(origin, "https://", "") // Use SSH URL diff --git a/gitsrht/access.py b/gitsrht/access.py new file mode 100644 index 0000000..6731e57 --- /dev/null +++ b/gitsrht/access.py @@ -0,0 +1,93 @@ +from datetime import datetime +from enum import IntFlag +from flask import abort, current_app, request, redirect, url_for +from gitsrht.types import Access, AccessMode, Repository, Redirect, User, RepoVisibility +from srht.database import db +from srht.oauth import current_user +import sqlalchemy as sa +import sqlalchemy_utils as sau +from sqlalchemy.ext.declarative import declared_attr +from enum import Enum + +class UserAccess(IntFlag): + none = 0 + read = 1 + write = 2 + manage = 4 + +def get_repo(owner_name, repo_name): + if owner_name[0] == "~": + user = User.query.filter(User.username == owner_name[1:]).first() + if user: + repo = Repository.query.filter(Repository.owner_id == user.id)\ + .filter(Repository.name == repo_name).first() + else: + repo = None + if user and not repo: + repo = (Redirect.query + .filter(Redirect.owner_id == user.id) + .filter(Redirect.name == repo_name) + ).first() + return user, repo + else: + # TODO: organizations + return None, None + +def get_repo_or_redir(owner, repo): + owner, repo = get_repo(owner, repo) + if not repo: + abort(404) + if not has_access(repo, UserAccess.read): + abort(401) + if isinstance(repo, Redirect): + view_args = request.view_args + if not "repo" in view_args or not "owner" in view_args: + return redirect(url_for(".summary", + owner=repo.new_repo.owner.canonical_name, + repo=repo.new_repo.name)) + view_args["owner"] = repo.new_repo.owner.canonical_name + view_args["repo"] = repo.new_repo.name + abort(redirect(url_for(request.endpoint, **view_args))) + return owner, repo + +def get_access(repo, user=None): + # Note: when updating push access logic, also update git.sr.ht/gitsrht-shell + if not user: + user = current_user + if not repo: + return UserAccess.none + if isinstance(repo, Redirect): + # Just pretend they have full access for long enough to do the redirect + return UserAccess.read | UserAccess.write | UserAccess.manage + if not user: + if repo.visibility == RepoVisibility.PUBLIC or \ + repo.visibility == RepoVisibility.UNLISTED: + return UserAccess.read + return UserAccess.none + if repo.owner_id == user.id: + return UserAccess.read | UserAccess.write | UserAccess.manage + acl = Access.query.filter( + Access.user_id == user.id, + Access.repo_id == repo.id).first() + if acl: + acl.updated = datetime.utcnow() + db.session.commit() + if acl.mode == AccessMode.ro: + return UserAccess.read + else: + return UserAccess.read | UserAccess.write + if repo.visibility == RepoVisibility.PRIVATE: + return UserAccess.none + return UserAccess.read + +def has_access(repo, access, user=None): + return access in get_access(repo, user) + +def check_access(owner_name, repo_name, access): + owner, repo = get_repo(owner_name, repo_name) + if not owner or not repo: + abort(404) + a = get_access(repo) + if not access in a: + abort(403) + return owner, repo diff --git a/gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py b/gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py new file mode 100644 index 0000000..4336ad1 --- /dev/null +++ b/gitsrht/alembic/versions/64fcd80183c8_add_visibility_enum.py @@ -0,0 +1,37 @@ +"""Add visibility enum + +Revision ID: 64fcd80183c8 +Revises: 38952f52f32d +Create Date: 2022-02-24 12:29:23.314019 + +""" + +# revision identifiers, used by Alembic. +revision = '64fcd80183c8' +down_revision = '38952f52f32d' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.execute(""" + UPDATE repository SET visibility = 'private' WHERE visibility = 'autocreated'; + + CREATE TYPE visibility AS ENUM ( + 'PUBLIC', + 'PRIVATE', + 'UNLISTED' + ); + + ALTER TABLE repository + ALTER COLUMN visibility TYPE visibility USING upper(visibility)::visibility; + """) + + +def downgrade(): + op.execute(""" + ALTER TABLE repository + ALTER COLUMN visibility TYPE varchar USING lower(visibility::varchar); + DROP TYPE visibility; + """) diff --git a/gitsrht/app.py b/gitsrht/app.py index dc0d67e..ebfbbc7 100644 --- a/gitsrht/app.py +++ b/gitsrht/app.py @@ -4,25 +4,24 @@ import stat from functools import lru_cache from gitsrht import urls from gitsrht.git import commit_time, commit_links, trim_commit, signature_time -from gitsrht.repos import GitRepoApi from gitsrht.service import oauth_service, webhooks_notify -from gitsrht.types import Access, Redirect, Repository, User -from scmsrht.flask import ScmSrhtFlask +from gitsrht.types import User from srht.config import cfg -from srht.database import DbSession -from srht.flask import session +from srht.database import db, DbSession +from srht.flask import SrhtFlask, session +from jinja2 import FileSystemLoader, ChoiceLoader from werkzeug.urls import url_quote db = DbSession(cfg("git.sr.ht", "connection-string")) db.init() -class GitApp(ScmSrhtFlask): +class GitApp(SrhtFlask): def __init__(self): super().__init__("git.sr.ht", __name__, - access_class=Access, redirect_class=Redirect, - repository_class=Repository, user_class=User, - repo_api=GitRepoApi(), oauth_service=oauth_service) + oauth_service=oauth_service) + from gitsrht.blueprints.auth import auth + from gitsrht.blueprints.public import public from gitsrht.blueprints.api import register_api from gitsrht.blueprints.api.plumbing import plumbing from gitsrht.blueprints.api.porcelain import porcelain @@ -32,6 +31,9 @@ class GitApp(ScmSrhtFlask): from gitsrht.blueprints.repo import repo from srht.graphql import gql_blueprint + self.register_blueprint(auth) + self.register_blueprint(public) + register_api(self) self.register_blueprint(plumbing) self.register_blueprint(porcelain) @@ -68,6 +70,16 @@ class GitApp(ScmSrhtFlask): "path_join": os.path.join, "stat": stat, "trim_commit": trim_commit, + "lookup_user": self.lookup_user } + choices = [self.jinja_loader, FileSystemLoader(os.path.join( + os.path.dirname(__file__), "templates"))] + self.jinja_loader = ChoiceLoader(choices) + + self.url_map.strict_slashes = False + + def lookup_user(self, email): + return User.query.filter(User.email == email).one_or_none() + app = GitApp() diff --git a/gitsrht/blueprints/api/__init__.py b/gitsrht/blueprints/api/__init__.py index 86c0ee8..2217d67 100644 --- a/gitsrht/blueprints/api/__init__.py +++ b/gitsrht/blueprints/api/__init__.py @@ -1,7 +1,7 @@ import pkg_resources from flask import abort -from scmsrht.access import UserAccess, get_access -from scmsrht.types import Repository, User +from gitsrht.access import UserAccess, get_access +from gitsrht.types import Repository, User from srht.flask import csrf_bypass from srht.oauth import current_token, oauth diff --git a/gitsrht/blueprints/api/info.py b/gitsrht/blueprints/api/info.py index cb03bd6..c7c09ff 100644 --- a/gitsrht/blueprints/api/info.py +++ b/gitsrht/blueprints/api/info.py @@ -1,7 +1,7 @@ from flask import Blueprint, Response, current_app, request -from scmsrht.access import UserAccess -from scmsrht.repos import RepoVisibility -from scmsrht.types import Access, Repository, User +import gitsrht.repos as repos +from gitsrht.access import UserAccess +from gitsrht.types import Access, Repository, User, RepoVisibility from gitsrht.webhooks import UserWebhook from gitsrht.blueprints.api import get_user, get_repo from srht.api import paginated_response @@ -22,12 +22,12 @@ def repos_by_user_GET(username): .filter(Repository.owner_id == user.id)) if user.id != current_token.user_id: repos = (repos - .outerjoin(Access._get_current_object(), + .outerjoin(Access, Access.repo_id == Repository.id) .filter(or_( Access.user_id == current_token.user_id, and_( - Repository.visibility == RepoVisibility.public, + Repository.visibility == RepoVisibility.PUBLIC, Access.id.is_(None)) ))) return paginated_response(Repository.id, repos) @@ -37,7 +37,7 @@ def repos_by_user_GET(username): def repos_POST(): valid = Validation(request) user = current_token.user - resp = current_app.repo_api.create_repo(valid, user) + resp = repos.create_repo(valid, user) if not valid.ok: return valid.response # Convert visibility back to lowercase @@ -103,7 +103,7 @@ def repos_by_name_DELETE(reponame): user = current_token.user repo = get_repo(user, reponame, needs=UserAccess.manage) repo_id = repo.id - current_app.repo_api.delete_repo(repo, user) + repos.delete_repo(repo, user) return {}, 204 @info.route("/api/repos//readme", defaults={"username": None}) diff --git a/gitsrht/blueprints/api/porcelain.py b/gitsrht/blueprints/api/porcelain.py index 600c433..be9a987 100644 --- a/gitsrht/blueprints/api/porcelain.py +++ b/gitsrht/blueprints/api/porcelain.py @@ -10,7 +10,7 @@ from gitsrht.repos import upload_artifact from gitsrht.webhooks import RepoWebhook from io import BytesIO from itertools import groupby -from scmsrht.access import UserAccess +from gitsrht.access import UserAccess from gitsrht.blueprints.api import get_user, get_repo from srht.api import paginated_response from srht.database import db diff --git a/gitsrht/blueprints/artifacts.py b/gitsrht/blueprints/artifacts.py index 126472b..1c42cbe 100644 --- a/gitsrht/blueprints/artifacts.py +++ b/gitsrht/blueprints/artifacts.py @@ -7,7 +7,7 @@ from gitsrht.git import Repository as GitRepository, strip_pgp_signature from gitsrht.repos import delete_artifact, upload_artifact from gitsrht.types import Artifact from minio import Minio -from scmsrht.access import check_access, get_repo_or_redir, UserAccess +from gitsrht.access import check_access, UserAccess from srht.config import cfg from srht.database import db from srht.oauth import loginrequired diff --git a/gitsrht/blueprints/auth.py b/gitsrht/blueprints/auth.py new file mode 100644 index 0000000..afdf5c9 --- /dev/null +++ b/gitsrht/blueprints/auth.py @@ -0,0 +1,22 @@ +import os +from flask import Blueprint, request +from gitsrht.access import get_repo, has_access, UserAccess +from urllib.parse import urlparse, unquote + +auth = Blueprint("auth", __name__) + +@auth.route("/authorize") +def authorize_http_access(): + original_uri = request.headers.get("X-Original-URI") + original_uri = urlparse(original_uri) + path = unquote(original_uri.path) + original_path = os.path.normpath(path).split('/') + if len(original_path) < 3: + return "authorized", 200 + owner, repo = original_path[1], original_path[2] + owner, repo = get_repo(owner, repo) + if not repo: + return "unauthorized", 403 + if not has_access(repo, UserAccess.read): + return "unauthorized", 403 + return "authorized", 200 diff --git a/gitsrht/blueprints/email.py b/gitsrht/blueprints/email.py index 00d036b..894f304 100644 --- a/gitsrht/blueprints/email.py +++ b/gitsrht/blueprints/email.py @@ -13,7 +13,7 @@ from flask import Blueprint, render_template, abort, request, url_for, session from flask import redirect from gitsrht.git import Repository as GitRepository, commit_time, diffstat from gitsrht.git import get_log -from scmsrht.access import get_repo_or_redir +from gitsrht.access import get_repo_or_redir from srht.config import cfg, cfgi, cfgb from srht.oauth import loginrequired, current_user from srht.validation import Validation diff --git a/gitsrht/blueprints/manage.py b/gitsrht/blueprints/manage.py index c6e2d4c..628f8fc 100644 --- a/gitsrht/blueprints/manage.py +++ b/gitsrht/blueprints/manage.py @@ -1,6 +1,7 @@ import pygit2 from flask import Blueprint, current_app, request, render_template, abort from flask import redirect, url_for +import gitsrht.repos as repos from gitsrht.git import Repository as GitRepository from srht.config import cfg from srht.database import db @@ -8,11 +9,8 @@ from srht.flask import session from srht.graphql import exec_gql, GraphQLError from srht.oauth import current_user, loginrequired, UserType from srht.validation import Validation -from scmsrht.access import check_access, UserAccess -from scmsrht.repos.access import AccessMode -from scmsrht.repos.redirect import BaseRedirectMixin -from scmsrht.repos.repository import RepoVisibility -from scmsrht.types import Access, User +from gitsrht.access import check_access, UserAccess, AccessMode +from gitsrht.types import Access, User, Redirect import shutil import os @@ -28,10 +26,8 @@ def create_GET(): @manage.route("/create", methods=["POST"]) @loginrequired def create_POST(): - if not current_app.repo_api: - abort(501) valid = Validation(request) - resp = current_app.repo_api.create_repo(valid) + resp = repos.create_repo(valid) if not valid.ok: return render_template("create.html", **valid.kwargs) @@ -51,10 +47,8 @@ def clone(): @manage.route("/clone", methods=["POST"]) @loginrequired def clone_POST(): - if not current_app.repo_api: - abort(501) valid = Validation(request) - resp = current_app.repo_api.clone_repo(valid) + resp = repos.clone_repo(valid) if not valid.ok: return render_template("clone.html", **valid.kwargs) return redirect(url_for("repo.summary", @@ -64,7 +58,7 @@ def clone_POST(): @loginrequired def settings_info(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): return redirect(url_for(".settings_info", owner_name=owner_name, repo_name=repo.new_repo.name)) return render_template("settings_info.html", owner=owner, repo=repo) @@ -73,7 +67,7 @@ def settings_info(owner_name, repo_name): @loginrequired def settings_info_POST(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): repo = repo.new_repo valid = Validation(request) @@ -101,7 +95,7 @@ def settings_info_POST(owner_name, repo_name): @loginrequired def settings_rename(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): return redirect(url_for(".settings_rename", owner_name=owner_name, repo_name=repo.new_repo.name)) return render_template("settings_rename.html", owner=owner, repo=repo) @@ -110,7 +104,7 @@ def settings_rename(owner_name, repo_name): @loginrequired def settings_rename_POST(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): repo = repo.new_repo valid = Validation(request) @@ -138,7 +132,7 @@ def settings_rename_POST(owner_name, repo_name): @loginrequired def settings_access(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): return redirect(url_for(".settings_manage", owner_name=owner_name, repo_name=repo.new_repo.name)) return render_template("settings_access.html", owner=owner, repo=repo) @@ -147,7 +141,7 @@ def settings_access(owner_name, repo_name): @loginrequired def settings_access_POST(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): repo = repo.new_repo valid = Validation(request) username = valid.require("user", friendly_name="User") @@ -190,7 +184,7 @@ def settings_access_POST(owner_name, repo_name): @loginrequired def settings_access_revoke_POST(owner_name, repo_name, grant_id): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): repo = repo.new_repo grant = (Access.query .filter(Access.repo_id == repo.id, Access.id == grant_id) @@ -206,7 +200,7 @@ def settings_access_revoke_POST(owner_name, repo_name, grant_id): @loginrequired def settings_delete(owner_name, repo_name): owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): return redirect(url_for(".settings_delete", owner_name=owner_name, repo_name=repo.new_repo.name)) return render_template("settings_delete.html", owner=owner, repo=repo) @@ -214,14 +208,12 @@ def settings_delete(owner_name, repo_name): @manage.route("///settings/delete", methods=["POST"]) @loginrequired def settings_delete_POST(owner_name, repo_name): - if not current_app.repo_api: - abort(501) owner, repo = check_access(owner_name, repo_name, UserAccess.manage) - if isinstance(repo, BaseRedirectMixin): + if isinstance(repo, Redirect): # Normally we'd redirect but we don't want to fuck up some other repo abort(404) repo_id = repo.id - current_app.repo_api.delete_repo(repo) + repos.delete_repo(repo) session["notice"] = "{}/{} was deleted.".format( owner.canonical_name, repo.name) return redirect("/" + owner.canonical_name) diff --git a/gitsrht/blueprints/public.py b/gitsrht/blueprints/public.py new file mode 100644 index 0000000..3fd3215 --- /dev/null +++ b/gitsrht/blueprints/public.py @@ -0,0 +1,54 @@ +from flask import Blueprint, current_app, request +from flask import render_template, abort +from srht.config import cfg +from srht.flask import paginate_query +from srht.oauth import current_user +from srht.search import search_by +from gitsrht.types import Access, Repository, User, RepoVisibility +from sqlalchemy import and_, or_ + +public = Blueprint('public', __name__) + +@public.route("/") +def index(): + if current_user: + repos = (Repository.query + .filter(Repository.owner_id == current_user.id) + .order_by(Repository.updated.desc()) + .limit(10)).all() + return render_template("dashboard.html", repos=repos) + return render_template("index.html") + +@public.route("/~") +@public.route("/~/") +def user_index(username): + user = User.query.filter(User.username == username).first() + if not user: + abort(404) + terms = request.args.get("search") + repos = (Repository.query + .filter(Repository.owner_id == user.id)) + if current_user and current_user.id != user.id: + repos = (repos + .outerjoin(Access, + Access.repo_id == Repository.id) + .filter(or_( + Access.user_id == current_user.id, + Repository.visibility == RepoVisibility.PUBLIC, + ))) + elif not current_user: + repos = repos.filter(Repository.visibility == RepoVisibility.PUBLIC) + + search_error = None + try: + repos = search_by(repos, terms, + [Repository.name, Repository.description]) + except ValueError as ex: + search_error = str(ex) + + repos = repos.order_by(Repository.updated.desc()) + repos, pagination = paginate_query(repos) + + return render_template("user.html", + user=user, repos=repos, + search=terms, search_error=search_error, **pagination) diff --git a/gitsrht/blueprints/repo.py b/gitsrht/blueprints/repo.py index f19531e..07c42c2 100644 --- a/gitsrht/blueprints/repo.py +++ b/gitsrht/blueprints/repo.py @@ -19,7 +19,7 @@ from jinja2.utils import url_quote, escape from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer -from scmsrht.access import get_repo, get_repo_or_redir +from gitsrht.access import get_repo, get_repo_or_redir from scmsrht.formatting import get_formatted_readme, get_highlighted_file from scmsrht.urls import get_clone_urls from srht.config import cfg, get_origin diff --git a/gitsrht/repos.py b/gitsrht/repos.py index 56628bc..bb52b7c 100644 --- a/gitsrht/repos.py +++ b/gitsrht/repos.py @@ -6,7 +6,6 @@ import shutil import subprocess from gitsrht.types import Artifact, Repository, Redirect from minio import Minio -from scmsrht.repos.repository import RepoVisibility from srht.config import cfg from srht.database import db from srht.graphql import exec_gql, GraphQLError @@ -78,85 +77,82 @@ def upload_artifact(valid, repo, commit, f, filename): db.session.add(artifact) return artifact -# TODO: Remove repo API wrapper class +def get_repo_path(owner, repo_name): + return os.path.join(repos_path, "~" + owner.username, repo_name) -class GitRepoApi(): - def get_repo_path(self, owner, repo_name): - return os.path.join(repos_path, "~" + owner.username, repo_name) - - def create_repo(self, valid, user=None): - repo_name = valid.require("name", friendly_name="Name") - description = valid.optional("description") - visibility = valid.optional("visibility") - if not valid.ok: - return None +def create_repo(valid, user=None): + repo_name = valid.require("name", friendly_name="Name") + description = valid.optional("description") + visibility = valid.optional("visibility") + if not valid.ok: + return None - # Convert the visibility to uppercase. This is needed for the REST API - # TODO: Remove this when the REST API is phased out - if visibility is not None: - visibility = visibility.upper() + # Convert the visibility to uppercase. This is needed for the REST API + # TODO: Remove this when the REST API is phased out + if visibility is not None: + visibility = visibility.upper() - resp = exec_gql("git.sr.ht", """ - mutation CreateRepository( - $name: String!, - $visibility: Visibility = PUBLIC, - $description: String) { - createRepository( - name: $name, - visibility: $visibility, - description: $description) { - id - created - updated - name - owner { - canonicalName - ... on User { - name: username - } + resp = exec_gql("git.sr.ht", """ + mutation CreateRepository( + $name: String!, + $visibility: Visibility = PUBLIC, + $description: String) { + createRepository( + name: $name, + visibility: $visibility, + description: $description) { + id + created + updated + name + owner { + canonicalName + ... on User { + name: username } - description - visibility } + description + visibility } - """, valid=valid, user=user, name=repo_name, - description=description, visibility=visibility) + } + """, valid=valid, user=user, name=repo_name, + description=description, visibility=visibility) - if not valid.ok: - return None - return resp["createRepository"] + if not valid.ok: + return None + return resp["createRepository"] - def clone_repo(self, valid): - cloneUrl = valid.require("cloneUrl", friendly_name="Clone URL") - name = valid.require("name", friendly_name="Name") - description = valid.optional("description") - visibility = valid.optional("visibility") - if not valid.ok: - return None +def clone_repo(valid): + cloneUrl = valid.require("cloneUrl", friendly_name="Clone URL") + name = valid.require("name", friendly_name="Name") + description = valid.optional("description") + visibility = valid.optional("visibility") + if not valid.ok: + return None - resp = exec_gql("git.sr.ht", """ - mutation CreateRepository( - $name: String!, - $visibility: Visibility = UNLISTED, - $description: String, - $cloneUrl: String) { - createRepository(name: $name, - visibility: $visibility, - description: $description, - cloneUrl: $cloneUrl) { - name - } + resp = exec_gql("git.sr.ht", """ + mutation CreateRepository( + $name: String!, + $visibility: Visibility = UNLISTED, + $description: String, + $cloneUrl: String) { + createRepository(name: $name, + visibility: $visibility, + description: $description, + cloneUrl: $cloneUrl) { + name } - """, valid=valid, name=name, visibility=visibility, - description=description, cloneUrl=cloneUrl) + } + """, valid=valid, name=name, visibility=visibility, + description=description, cloneUrl=cloneUrl) - if not valid.ok: - return None - return resp["createRepository"] + if not valid.ok: + return None + return resp["createRepository"] - def delete_repo(self, repo, user=None): - exec_gql("git.sr.ht", """ - mutation DeleteRepository($id: Int!) { - deleteRepository(id: $id) { id } - } - """, user=user, id=repo.id) +def delete_repo(repo, user=None): + exec_gql("git.sr.ht", """ + mutation DeleteRepository($id: Int!) { + deleteRepository(id: $id) { id } + } + """, user=user, id=repo.id) diff --git a/gitsrht/templates/base.html b/gitsrht/templates/base.html new file mode 100644 index 0000000..3516172 --- /dev/null +++ b/gitsrht/templates/base.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} + +{% block body %} +{% block content %}{% endblock %} +{% endblock %} diff --git a/gitsrht/templates/dashboard.html b/gitsrht/templates/dashboard.html index f25b119..6cfc3d1 100644 --- a/gitsrht/templates/dashboard.html +++ b/gitsrht/templates/dashboard.html @@ -30,9 +30,9 @@ owner=current_user.canonical_name, repo=repo.name)}}" >~{{current_user.username}}/{{repo.name}} - {% if repo.visibility.value != 'public' %} + {% if repo.visibility.value != 'PUBLIC' %} - {{ repo.visibility.value }} + {{ repo.visibility.value.lower() }} {% endif %} diff --git a/gitsrht/templates/repo.html b/gitsrht/templates/repo.html index 2bf5a6b..41ac808 100644 --- a/gitsrht/templates/repo.html +++ b/gitsrht/templates/repo.html @@ -2,7 +2,7 @@ {% block head %} {% block repohead %} {% endblock %} -{% if repo.visibility.value =='unlisted' %} +{% if repo.visibility.value == 'UNLISTED' %} {% endif %} {# VCS meta tags #} @@ -44,18 +44,18 @@ >{{owner.canonical_name}}/{{repo.name}}