~edwargix/git.sr.ht

c2d2de721a756fc2a63a83f9225723c6250234a5 — Drew DeVault 6 years ago 4b23440
Add access grant management web flow
A gitsrht/alembic/versions/ce3a03ec34a5_add_access_table.py => gitsrht/alembic/versions/ce3a03ec34a5_add_access_table.py +28 -0
@@ 0,0 1,28 @@
"""Add access table

Revision ID: ce3a03ec34a5
Revises: f81294e9c2cf
Create Date: 2018-01-27 11:17:49.944381

"""

# revision identifiers, used by Alembic.
revision = 'ce3a03ec34a5'
down_revision = 'f81294e9c2cf'

from alembic import op
import sqlalchemy as sa


def upgrade():
    op.create_table('access',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('created', sa.DateTime, nullable=False),
        sa.Column('updated', sa.DateTime, nullable=False),
        sa.Column('repo_id', sa.Integer, sa.ForeignKey('repository.id'), nullable=False),
        sa.Column('user_id', sa.Integer, sa.ForeignKey('user.id'), nullable=False),
        sa.Column('mode', sa.String(), nullable=False, default='ro'))


def downgrade():
    op.drop_table('access')

M gitsrht/blueprints/manage.py => gitsrht/blueprints/manage.py +52 -0
@@ 5,6 5,7 @@ from srht.config import cfg
from srht.database import db
from srht.validation import Validation
from gitsrht.types import Repository, RepoVisibility, Redirect
from gitsrht.types import Access, AccessMode, User
from gitsrht.decorators import loginrequired
from gitsrht.access import check_access, UserAccess
from gitsrht.repos import create_repo, rename_repo, delete_repo


@@ 86,6 87,57 @@ def settings_access(owner_name, repo_name):
            owner_name=owner_name, repo_name=repo.new_repo.name))
    return render_template("settings_access.html", owner=owner, repo=repo)

@manage.route("/<owner_name>/<repo_name>/settings/access", methods=["POST"])
@loginrequired
def settings_access_POST(owner_name, repo_name):
    owner, repo = check_access(owner_name, repo_name, UserAccess.manage)
    if isinstance(repo, Redirect):
        repo = repo.new_repo
    valid = Validation(request)
    user = valid.require("user", friendly_name="User")
    mode = valid.optional("access", cls=AccessMode, default=AccessMode.ro)
    if not valid.ok:
        return render_template("settings_access.html",
                owner=owner, repo=repo, **valid.kwargs)
    # TODO: Group access
    if user[0] == "~":
        user = user[1:]
    user = User.query.filter(User.username == user).first()
    valid.expect(user,
            "I don't know this user. Have they logged into git.sr.ht before?",
            field="user")
    if not valid.ok:
        return render_template("settings_access.html",
                owner=owner, repo=repo, **valid.kwargs)
    grant = (Access.query
        .filter(Access.repo_id == repo.id, Access.user_id == user.id)
    ).first()
    if not grant:
        grant = Access()
        grant.repo_id = repo.id
        grant.user_id = user.id
        db.session.add(grant)
    grant.mode = mode
    db.session.commit()
    return redirect("/{}/{}/settings/access".format(
        owner.canonical_name, repo.name))

@manage.route("/<owner_name>/<repo_name>/settings/access/revoke/<grant_id>", methods=["POST"])
@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, Redirect):
        repo = repo.new_repo
    grant = (Access.query
        .filter(Access.repo_id == repo.id, Access.id == grant_id)
    ).first()
    if not grant:
        abort(404)
    db.session.delete(grant)
    db.session.commit()
    return redirect("/{}/{}/settings/access".format(
        owner.canonical_name, repo.name))

@manage.route("/<owner_name>/<repo_name>/settings/delete")
@loginrequired
def settings_delete(owner_name, repo_name):

M gitsrht/templates/settings_access.html => gitsrht/templates/settings_access.html +84 -2
@@ 1,8 1,90 @@
{% extends "settings.html" %}
{% block content %}
<div class="row">
  <div class="col-md-6">
    TODO
  <div class="col-md-8">
    {% if len(repo.access_grants) > 0 %}
    <table class="table">
      <thead>
        <tr>
          <th>user</th>
          <th>granted</th>
          <th>last used</th>
          <th>access</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {% for grant in repo.access_grants %}
        <tr>
          <td>
            <a href="/~{{ grant.user.username }}">~{{grant.user.username}}</a>
          </td>
          <td>{{ grant.created | date }}</td>
          <td>{{ grant.created | date }}</td>
          <td>{{ grant.mode.value }}</td>
          <td style="width: 6rem">
            <form
              method="POST"
              action="/~{{owner.username}}/{{
                repo.name
              }}/settings/access/revoke/{{ grant.id }}"
            >
              <button type="submit" class="btn btn-default btn-fill">Revoke</button>
            </form>
          </td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
    <h4>Grant Access</h4>
    {% endif %}
    <form method="POST">
      <div class="form-group {{valid.cls("user")}}">
        <label for="user">User</label>
        <input
          type="text"
          class="form-control"
          id="user"
          name="user"
          placeholder="~{{ current_user.username }}"
          value="{{user or ""}}"
        />
        {{valid.summary("user")}}
      </div>
      <fieldset class="form-group">
        <legend>Access</legend>
        <!-- This is in a weird spot cause it looks better over here -->
        <button type="submit" class="btn btn-default pull-right">Grant access</button>
        <div class="form-check form-check-inline">
          <label
            class="form-check-label"
            title="Can view on the web and clone the repository"
          >
            <input
              class="form-check-input"
              type="radio"
              name="access"
              value="ro"
              {{ "checked" if not access or access.value == "ro" else "" }}
            > Read only
          </label>
        </div>
        <div class="form-check form-check-inline">
          <label
              class="form-check-label"
              title="Can push commits to the repository"
            >
            <input
              class="form-check-input"
              type="radio"
              name="access"
              value="rw"
              {{ "checked" if access and access.value == "rw" else "" }}
            > Read/write
          </label>
        </div>
      </fieldset>
    </form>
  </div>
</div>
{% endblock %}

M gitsrht/types/__init__.py => gitsrht/types/__init__.py +1 -0
@@ 3,3 3,4 @@ from .repository import Repository, RepoVisibility
from .oauthtoken import OAuthToken
from .webhook import Webhook
from .redirect import Redirect
from .access import Access, AccessMode

A gitsrht/types/access.py => gitsrht/types/access.py +24 -0
@@ 0,0 1,24 @@
import sqlalchemy as sa
import sqlalchemy_utils as sau
from srht.database import Base
from enum import Enum

class AccessMode(Enum):
    ro = 'ro'
    rw = 'rw'

class Access(Base):
    __tablename__ = 'access'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    repo_id = sa.Column(sa.Integer, sa.ForeignKey('repository.id'), nullable=False)
    repo = sa.orm.relationship('Repository', backref='access_grants')
    user_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
    user = sa.orm.relationship('User', backref='access_grants')
    mode = sa.Column(sau.ChoiceType(AccessMode, impl=sa.String()),
            nullable=False, default=AccessMode.ro)

    def __repr__(self):
        return '<Access {} {}->{}:{}>'.format(
                self.id, self.user_id, self.repo_id, self.mode)