Started working on granting access to repo users via web interface.
authorSybren A. Stüvel <sybren@stuvel.eu>
Wed, 8 Nov 2017 16:30:18 +0000 (17:30 +0100)
committerSybren A. Stüvel <sybren@stuvel.eu>
Wed, 8 Nov 2017 16:30:18 +0000 (17:30 +0100)
src/templates/svnman/project_settings/settings.pug
src/templates/svnman/project_settings/svnman_layout.pug
svnman/__init__.py
svnman/routes.py

index 2d7349f..026fcbb 100644 (file)
                p
                        button.btn.btn-danger(onclick='deleteRepo()') Delete Subversion repository
 
+               h4 Repository Access
+
+               .row
+                       .col-md-6
+                               .access-users-search
+                                       .form-group
+                                               input#user-search.form-control(
+                                                       name='contacts',
+                                                       type='text',
+                                                       placeholder='Grant user access by name')
+
+                               ul.access-users-list
+                                       | {% for user in svn_users %}
+                                       li.access-users-item(
+                                               data-user-id="{{ user['_id'] }}",
+                                               class="{% if current_user.objectid == user['_id'] %}self{% endif %}")
+                                               .access-users-avatar
+                                                       img(src="{{ user['avatar'] }}")
+                                               .access-users-details
+                                                       span.access-users-name
+                                                               | {{ user['full_name'] }}
+                                                               | {% if current_user.objectid == user['_id'] %}
+                                                               small (You)
+                                                               | {% endif %}
+                                                       span.access-users-extra {{ user['username'] }}
+                                               .access-users-action
+                                                       | {# Only allow deletion if we are: admin, project users, or current_user in the team #}
+                                                       | {% if can_abandon_manager %}
+                                                       | {%     if current_user.objectid == user['_id'] %}
+                                                       button.user-remove(title="Revoke your own access")
+                                                               i.pi-trash
+                                                       | {%     else %}
+                                                       button.user-remove(title="Revoke access of this user")
+                                                               i.pi-trash
+                                                       | {%     endif %}
+                                                       | {% endif %}
+
+                                       | {% endfor %}
+
+                       .col-md-6
+                               .access-users-info
+                                       h4 What can these users do?
+                                       p.
+                                               Users in this list have read/write access to the Subversion repository. It
+                                               is not possible to allow read-only access.
+                                       p.
+                                               Their password is <em>not</em> their Blender ID password, but has to be set
+                                               separately.
+
+
+
+
 | {% endblock svnman_container %}
 
 | {% block footer_scripts %}
 script.
+       var algolia_application_id = '{{config.ALGOLIA_USER}}'
+       var algolia_public_key = '{{config.ALGOLIA_PUBLIC_KEY}}';
+       var algolia_index_users = '{{config.ALGOLIA_INDEX_USERS}}';
+
+       $('#user-search').userSearch(algolia_application_id, algolia_public_key, algolia_index_users,
+               function(event, hit, dataset) {
+                       var $existing = $('li.access-users-item[data-user-id="' + hit.objectID + '"]');
+                       if ($existing.length) {
+                               $existing
+                                       .addClass('active')
+                                       .delay(1000)
+                                       .queue(function() {
+                                               console.log('no');
+                                               $existing.removeClass('active');
+                                               $existing.dequeue();
+                                       });
+                               toastr.info('User already has access');
+                       }
+                       else {
+                               grantUser(hit.objectID);
+                       }
+               }
+       );
+
+       function grantUser(user_id) {
+               console.log('grant to', user_id);
+               $.ajax({
+                       url: '{{ url_for( "svnman.grant_access", project_url=project.url, repo_id=repo_id) }}',
+                       data: {user_id: user_id},
+                       method: 'POST',
+               })
+               .done(function() {
+                       window.location.reload();
+               })
+               .fail(function(err) {
+                       if (err.status == 451) {
+                               toastr.error("This user has no Subversion usage in their subscription package.");
+                               return;
+                       }
+                       var err_elt = xhrErrorResponseElement(err, 'Error granting access: ');
+                       toastr.error(err_elt);
+               });
+       }
+
        function deleteRepo() {
+               if (!confirm('Are you sure you want to delete this repository? This CANNOT be undone! You WILL loose this data.'))
+                       return;
+
                $.ajax({
                        url: '{{ url_for( "svnman.delete_repo", project_url=project.url, repo_id=repo_id) }}',
                        method: 'POST',
index 0843ef7..f184fd5 100644 (file)
@@ -4,6 +4,7 @@
 
 | {% block head %}
 script(src="{{ url_for('static_svnman', filename='assets/js/generated/tutti.min.js') }}")
+script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.autocomplete-0.22.0.min.js') }}")
 | {% endblock %}
 
 | {% block project_context_header %}
index 39d1de8..9c8b3a2 100644 (file)
@@ -1,16 +1,17 @@
 import logging
 import os.path
 import string
-import typing
 from urllib.parse import urljoin
 
 import flask
 from werkzeug.local import LocalProxy
+import werkzeug.exceptions as wz_exceptions
 
 import pillarsdk
 from pillar.extension import PillarExtension
 from pillar.auth import current_user
 from pillar.api.projects import utils as proj_utils
+from pillar.api.utils import str2id
 from pillar import current_app
 
 EXTENSION_NAME = 'svnman'
@@ -170,9 +171,8 @@ class SVNManExtension(PillarExtension):
 
         from . import remote, exceptions
 
+        eprops, proj = self._get_prop_props(project)
         project_id = project['_id']
-        proj = project.to_dict()
-        eprops = proj.setdefault('extension_props', {}).setdefault(EXTENSION_NAME, {})
 
         repo_id = eprops.get('repo_id')
         if repo_id:
@@ -208,19 +208,23 @@ class SVNManExtension(PillarExtension):
 
         return actual_repo_id
 
-    def delete_repo(self, project_url: str, repo_id: str):
+    def _get_prop_props(self, project: pillarsdk.Project) -> (dict, dict):
+        """Gets the project as dictionary and the extension properties."""
+
+        proj = project.to_dict()
+        eprops = proj.setdefault('extension_props', {}).setdefault(EXTENSION_NAME, {})
+        return eprops, proj
+
+    def delete_repo(self, project: pillarsdk.Project, repo_id: str):
         """Deletes an SVN repository and detaches it from the project."""
 
         from . import remote, exceptions
 
-        proj = proj_utils.get_project(project_url)
-        project_id = proj['_id']
-        eprops = proj.setdefault('extension_props', {}).setdefault(EXTENSION_NAME, {})
-
+        eprops, proj = self._get_prop_props(project)
         proj_repo_id = eprops.get('repo_id')
         if proj_repo_id != repo_id:
             self._log.warning('project %s is linked to repo %r, not to %r, refusing to delete',
-                              project_id, proj_repo_id, repo_id)
+                              proj['_id'], proj_repo_id, repo_id)
             raise ValueError()
 
         self.remote.delete_repo(repo_id)
@@ -250,6 +254,47 @@ class SVNManExtension(PillarExtension):
         projects = pillarsdk.Project.all(params, api=api)
         return projects
 
+    def grant_access(self, project: pillarsdk.Project, repo_id: str, user_id: str):
+        """Grants access to the given user."""
+
+        eprops, proj = self._get_prop_props(project)
+        proj_repo_id = eprops.get('repo_id')
+        if proj_repo_id != repo_id:
+            self._log.warning('project %s is linked to repo %r, not to %r, '
+                              'refusing to grant access',
+                              proj['_id'], proj_repo_id, repo_id)
+            raise ValueError()
+
+        db_user = self._get_db_user(proj, repo_id, user_id)
+        username = db_user['username']
+        password = '$2y$10$password-not-yet-set'
+        self._log.info('granting user %s (%r) access to repo %s of project %s',
+                       user_id, username, repo_id, proj['_id'])
+
+        self.remote.modify_access(repo_id, grant=[(username, password)], revoke=[])
+
+    def _get_db_user(self, proj, repo_id, user_id) -> dict:
+        """Returns the user from the database.
+
+        Raises a ValueError if the user is not allowed to use svn.
+        """
+
+        from pillar.auth import UserClass
+
+        user_oid = str2id(user_id)
+        db_user = current_app.db('users').find_one({'_id': user_oid})
+        if not db_user:
+            self._log.warning('user %s not found, not modifying access to repo %s of project %s',
+                              user_id, repo_id, proj['_id'])
+            raise ValueError('User not found')
+
+        thatuser = UserClass.construct('', db_user)
+        if not thatuser.has_cap('svn-use'):
+            self._log.warning('user %s has no svn-use cap, not modifying access to repo %s of'
+                              ' project %s', user_id, repo_id, proj['_id'])
+            raise wz_exceptions.UnavailableForLegalReasons('User is not allowed to use Subversion')
+
+        return db_user
 
 
 def _get_current_svnman() -> SVNManExtension:
index 125f501..f91e93d 100644 (file)
@@ -1,6 +1,6 @@
 import logging
 
-from flask import Blueprint, render_template, jsonify
+from flask import Blueprint, render_template, jsonify, request
 import werkzeug.exceptions as wz_exceptions
 
 from pillar.api.utils.authorization import require_login
@@ -129,3 +129,29 @@ def project_settings(project: pillarsdk.Project, **template_args: dict):
     return render_template('svnman/project_settings/settings.html',
                            project=project,
                            **template_args)
+
+
+@blueprint.route('/<project_url>/grant-access/<repo_id>', methods=['POST'])
+@require_login(require_cap='svn-use')
+@require_project_put()
+def grant_access(project: pillarsdk.Project, repo_id: str):
+    user_id = request.form['user_id']
+    log.info('going to grant access to user %s on repository %s for project url=%r '
+             'on behalf of user %s (%s)',
+             user_id, repo_id, project.url, current_user.user_id, current_user.email)
+
+    from . import exceptions
+
+    try:
+        current_svnman.grant_access(project, repo_id, user_id)
+    except (OSError, IOError):
+        log.exception('unable to reach SVNman API')
+        resp = jsonify(_message='unable to reach SVNman API server')
+        resp.status_code = 500
+        return resp
+    except exceptions.RemoteError as ex:
+        log.error('API sent us an error: %s', ex)
+        resp = jsonify(_message=str(ex))
+        resp.status_code = 500
+        return resp
+    return '', 204