diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 49407c6..448d570 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -3231,7 +3231,7 @@ def get_api_token(session, token_str): return query.first() -def get_acls(session): +def get_acls(session, restrict=None): """ Returns all the possible ACLs a token can have according to the database. """ @@ -3240,6 +3240,15 @@ def get_acls(session): ).order_by( model.ACL.name ) + if restrict: + if isinstance(restrict, list): + query = query.filter( + model.ACL.name.in_(restrict) + ) + else: + query = query.filter( + model.ACL.name == restrict + ) return query.all() @@ -3259,7 +3268,7 @@ def add_token_to_user(session, project, acls, username): token = pagure.lib.model.Token( id=pagure.lib.login.id_generator(64), user_id=user.id, - project_id=project.id, + project_id=project.id if project else None, expiration=datetime.datetime.utcnow() + datetime.timedelta(days=60) ) session.add(token) diff --git a/pagure/templates/add_token.html b/pagure/templates/add_token.html index 11e9b31..b6aed5c 100644 --- a/pagure/templates/add_token.html +++ b/pagure/templates/add_token.html @@ -1,11 +1,15 @@ +{% if repo %} {% extends "repo_master.html" %} +{% else %} +{% extends "master.html" %} +{% endif %} {% from "_formhelper.html" import render_field_in_row %} {% set tag = "home" %} {% block title %}Create token{% endblock %} - -{% block repo %} +{% macro render_page() %} +
@@ -21,10 +25,14 @@ After that, click 'Create' to generate a token with the selected permissions.

+ {% if repo %}
+ {% else %} + + {% endif %} {% for acl in acls %}
+
+{% endmacro %} + + +{% block content %} + {{ render_page() }} +{% endblock %} + +{% block repo %} + {{ render_page() }} {% endblock %} diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index 2ff690f..2a90324 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -205,7 +205,6 @@
- diff --git a/pagure/templates/user_settings.html b/pagure/templates/user_settings.html index fe22f90..8cdd62c 100644 --- a/pagure/templates/user_settings.html +++ b/pagure/templates/user_settings.html @@ -148,12 +148,104 @@ -
- {% if config.get('PAGURE_AUTH')=='local' %} - Change password + {% if config.get('PAGURE_AUTH')=='local' %} +
+ Change password +
+ {% endif %} +
+ +
+
+
+ API Keys +
+
+

+ API keys are tokens used to authenticate you on pagure. They can also + be used to grant access to 3rd party application to behave on all + projects in your name. +

+

+ These are your personal tokens; they are not visible to others. +

+

+ These keys are valid for 60 days. +

+

+ These keys are private, make sure to store in a safe place and + do not share it. +

+
+ {% if user.tokens %} + + {% endif %} +
+ {% endblock %} diff --git a/pagure/ui/app.py b/pagure/ui/app.py index 693ab36..82759de 100644 --- a/pagure/ui/app.py +++ b/pagure/ui/app.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- """ - (c) 2014-2015 - Copyright Red Hat Inc + (c) 2014-2017 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon """ -import flask +import datetime from math import ceil +import flask from sqlalchemy.exc import SQLAlchemyError import pagure.exceptions @@ -756,3 +757,92 @@ def ssh_hostkey(): return flask.render_template( 'doc_ssh_keys.html', ) + + +@APP.route('/settings/token/new/', methods=('GET', 'POST')) +@APP.route('/settings/token/new', methods=('GET', 'POST')) +@login_required +def add_user_token(): + """ Create an user token (not project specific). + """ + if admin_session_timedout(): + if flask.request.method == 'POST': + flask.flash('Action canceled, try it again', 'error') + return flask.redirect( + flask.url_for('auth_login', next=flask.request.url)) + + # Ensure the user is in the DB at least + user = pagure.lib.search_user( + SESSION, username=flask.g.fas_user.username) + if not user: + flask.abort(404, 'User not found') + + acls = pagure.lib.get_acls( + SESSION, restrict=APP.config.get('CROSS_PROJECT_ACLS')) + form = pagure.forms.NewTokenForm(acls=acls) + + if form.validate_on_submit(): + try: + msg = pagure.lib.add_token_to_user( + SESSION, + project=None, + acls=form.acls.data, + username=flask.g.fas_user.username, + ) + SESSION.commit() + flask.flash(msg) + return flask.redirect(flask.url_for('.user_settings')) + except SQLAlchemyError as err: # pragma: no cover + SESSION.rollback() + APP.logger.exception(err) + flask.flash('API key could not be added', 'error') + + # When form is displayed after an empty submission, show an error. + if form.errors.get('acls'): + flask.flash('You must select at least one permission.', 'error') + + return flask.render_template( + 'add_token.html', + select='settings', + form=form, + acls=acls, + ) + + +@APP.route('/settings/token/revoke//', methods=['POST']) +@APP.route('/settings/token/revoke/', methods=['POST']) +@login_required +def revoke_api_user_token(token_id): + """ Revokie an user token (ie: not project specific). + """ + if admin_session_timedout(): + flask.flash('Action canceled, try it again', 'error') + url = flask.url_for( + 'view_settings', username=username, repo=repo, + namespace=namespace) + return flask.redirect( + flask.url_for('auth_login', next=url)) + + token = pagure.lib.get_api_token(SESSION, token_id) + + if not token \ + or token.user.username != flask.g.fas_user.username: + flask.abort(404, 'Token not found') + + form = pagure.forms.ConfirmationForm() + + if form.validate_on_submit(): + try: + if token.expiration >= datetime.datetime.utcnow(): + token.expiration = datetime.datetime.utcnow() + SESSION.add(token) + SESSION.commit() + flask.flash('Token revoked') + except SQLAlchemyError as err: # pragma: no cover + SESSION.rollback() + APP.logger.exception(err) + flask.flash( + 'Token could not be revoked, please contact an admin', + 'error') + + return flask.redirect(flask.url_for('.user_settings')) diff --git a/tests/test_pagure_flask_ui_app.py b/tests/test_pagure_flask_ui_app.py index 3f2ecf2..a9acd72 100644 --- a/tests/test_pagure_flask_ui_app.py +++ b/tests/test_pagure_flask_ui_app.py @@ -11,6 +11,7 @@ __requires__ = ['SQLAlchemy >= 0.8'] import pkg_resources +import datetime import unittest import shutil import sys @@ -1207,6 +1208,174 @@ class PagureFlaskApptests(tests.Modeltests): self.assertEqual(output.status_code, 404) pagure.APP.config['ENABLE_TICKETS'] = True + @patch('pagure.ui.app.admin_session_timedout') + def test_add_user_token(self, ast): + """ Test the add_user_token endpoint. """ + ast.return_value = False + self.test_new_project() + + user = tests.FakeUser() + with tests.user_set(pagure.APP, user): + output = self.app.get('/settings/token/new/') + self.assertEqual(output.status_code, 404) + self.assertTrue('

Page not found (404)

' in output.data) + + user.username = 'foo' + with tests.user_set(pagure.APP, user): + output = self.app.get('/settings/token/new') + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n ' + 'Create a new token\n', output.data) + self.assertIn( + '', + output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = { + 'acls': ['create_project', 'fork_project'] + } + + # missing CSRF + output = self.app.post('/settings/token/new', data=data) + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Create token - Pagure', output.data) + self.assertIn( + '
\n ' + 'Create a new token\n', output.data) + self.assertIn( + '', + output.data) + + data = { + 'acls': ['new_project'], + 'csrf_token': csrf_token + } + + # Invalid ACLs + output = self.app.post('/settings/token/new', data=data) + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Create token - Pagure', output.data) + self.assertIn( + '
\n ' + 'Create a new token\n', output.data) + self.assertIn( + '', + output.data) + + data = { + 'acls': ['create_project', 'fork_project'], + 'csrf_token': csrf_token + } + + # All good + output = self.app.post( + '/settings/token/new', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + 'foo\'s settings - Pagure', output.data) + self.assertIn( + '\n Token created\n', + output.data) + self.assertEqual( + output.data.count( + 'Valid' + ' until: '), 1) + + ast.return_value = True + output = self.app.get('/settings/token/new') + self.assertEqual(output.status_code, 302) + + @patch('pagure.ui.app.admin_session_timedout') + def test_revoke_api_user_token(self, ast): + """ Test the revoke_api_user_token endpoint. """ + ast.return_value = False + self.test_new_project() + + user = tests.FakeUser() + with tests.user_set(pagure.APP, user): + # Token doesn't exist + output = self.app.post('/settings/token/revoke/foobar') + self.assertEqual(output.status_code, 404) + self.assertTrue('

Page not found (404)

' in output.data) + + # Create the foobar API token but associated w/ the user 'foo' + item = pagure.lib.model.Token( + id='foobar', + user_id=2, # foo + expiration=datetime.datetime.utcnow() \ + + datetime.timedelta(days=30) + ) + self.session.add(item) + self.session.commit() + + # Token not associated w/ this user + output = self.app.post('/settings/token/revoke/foobar') + self.assertEqual(output.status_code, 404) + self.assertTrue('

Page not found (404)

' in output.data) + + user.username = 'foo' + with tests.user_set(pagure.APP, user): + # Missing CSRF token + output = self.app.post( + '/settings/token/revoke/foobar', follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + "foo's settings - Pagure", output.data) + self.assertEqual( + output.data.count( + 'Valid' + ' until: '), 1) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = { + 'csrf_token': csrf_token + } + + # All good - token is deleted + output = self.app.post( + '/settings/token/revoke/foobar', data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + "foo's settings - Pagure", output.data) + self.assertEqual( + output.data.count( + 'Valid' + ' until: '), 0) + + user = pagure.lib.get_user(self.session, key='foo') + self.assertEqual(len(user.tokens), 1) + expiration_dt = user.tokens[0].expiration + + # Token was already deleted - no changes + output = self.app.post( + '/settings/token/revoke/foobar', data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + "foo's settings - Pagure", output.data) + self.assertEqual( + output.data.count( + 'Valid' + ' until: '), 0) + + # Ensure the expiration date did not change + user = pagure.lib.get_user(self.session, key='foo') + self.assertEqual(len(user.tokens), 1) + self.assertEqual( + expiration_dt, user.tokens[0].expiration + ) + + ast.return_value = True + output = self.app.get('/settings/token/new') + self.assertEqual(output.status_code, 302) if __name__ == '__main__': - unittest.main() + unittest.main(verbosity=2)