From 3d19193d3face940a3c92ae6dffa209663e2e6b8 Mon Sep 17 00:00:00 2001 From: Matthias Nilsson Date: Tue, 24 Oct 2017 16:40:18 +0200 Subject: [PATCH] Add OAuth2 Client model This includes create+edit forms, deletion, etc. Closes #65. --- messages.pot | 150 +++++++++---- .../bc2c31758e2a_add_oauth2_client_table.py | 38 ++++ tests/conftest.py | 12 +- tests/end2end/test_deleting_client.py | 60 ++++++ tests/end2end/test_editing_client.py | 69 ++++++ tests/end2end/test_registering_client.py | 70 +++++++ tests/factories.py | 17 ++ tests/forms/test_client.py | 198 ++++++++++++++++++ tests/models/test_client.py | 58 +++++ xl_auth/app.py | 3 +- xl_auth/client/__init__.py | 6 + xl_auth/client/forms.py | 78 +++++++ xl_auth/client/models.py | 62 ++++++ xl_auth/client/views.py | 92 ++++++++ xl_auth/templates/clients/edit.html | 42 ++++ xl_auth/templates/clients/home.html | 51 +++++ xl_auth/templates/clients/register.html | 34 +++ .../translations/sv/LC_MESSAGES/messages.po | 152 ++++++++++---- 18 files changed, 1101 insertions(+), 91 deletions(-) create mode 100644 migrations/versions/bc2c31758e2a_add_oauth2_client_table.py create mode 100644 tests/end2end/test_deleting_client.py create mode 100644 tests/end2end/test_editing_client.py create mode 100644 tests/end2end/test_registering_client.py create mode 100644 tests/forms/test_client.py create mode 100644 tests/models/test_client.py create mode 100644 xl_auth/client/__init__.py create mode 100644 xl_auth/client/forms.py create mode 100644 xl_auth/client/models.py create mode 100644 xl_auth/client/views.py create mode 100644 xl_auth/templates/clients/edit.html create mode 100644 xl_auth/templates/clients/home.html create mode 100644 xl_auth/templates/clients/register.html diff --git a/messages.pot b/messages.pot index 51caee44..23a7436d 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: xl_auth 0.4.4\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2017-10-25 13:18+0200\n" +"POT-Creation-Date: 2017-10-25 14:02+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -29,7 +29,8 @@ msgstr "" #: tests/end2end/test_deleting_permission.py:26 tests/end2end/test_editing_collection.py:158 #: tests/end2end/test_editing_permission.py:31 tests/end2end/test_editing_permission.py:66 #: tests/end2end/test_editing_user.py:189 tests/end2end/test_editing_user.py:215 -#: xl_auth/templates/collections/home.html:42 xl_auth/templates/permissions/home.html:37 +#: xl_auth/templates/clients/home.html:35 xl_auth/templates/collections/home.html:42 +#: xl_auth/templates/permissions/home.html:37 msgid "Edit" msgstr "" @@ -42,6 +43,49 @@ msgstr "" msgid "Successfully deleted permissions for \"%(username)s\" on collection \"%(code)s\"." msgstr "" +#: tests/end2end/test_editing_client.py:82 tests/end2end/test_editing_client.py:115 +#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_registering_collection.py:102 +#: xl_auth/client/forms.py:14 xl_auth/collection/forms.py:18 xl_auth/templates/clients/home.html:17 +#: xl_auth/templates/users/home.html:18 xl_auth/templates/users/home.html:61 +msgid "Name" +msgstr "" + +#: tests/end2end/test_editing_client.py:82 tests/end2end/test_editing_client.py:148 +#: tests/end2end/test_editing_client.py:215 tests/end2end/test_editing_client.py:248 +#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_editing_user.py:146 +#: tests/end2end/test_registering_collection.py:79 tests/end2end/test_registering_collection.py:102 +#: tests/forms/test_client.py:48 tests/forms/test_client.py:71 tests/forms/test_client.py:94 +#: tests/forms/test_client.py:105 tests/forms/test_client.py:141 tests/forms/test_client.py:164 +#: tests/forms/test_client.py:187 tests/forms/test_client.py:198 tests/forms/test_collection.py:21 +#: tests/forms/test_collection.py:38 tests/forms/test_permission.py:31 +#: tests/forms/test_permission.py:39 tests/forms/test_permission.py:47 +#: tests/forms/test_permission.py:55 +msgid "This field is required." +msgstr "" + +#: tests/end2end/test_editing_client.py:115 tests/forms/test_client.py:60 +#: tests/forms/test_client.py:153 +msgid "Field must be between 3 and 64 characters long." +msgstr "" + +#: tests/end2end/test_editing_client.py:148 tests/end2end/test_editing_client.py:181 +#: xl_auth/client/forms.py:15 xl_auth/templates/clients/home.html:18 +msgid "Description" +msgstr "" + +#: tests/end2end/test_editing_client.py:182 tests/forms/test_client.py:83 +#: tests/forms/test_client.py:176 +msgid "Field must be between 3 and 350 characters long." +msgstr "" + +#: tests/end2end/test_editing_client.py:215 xl_auth/client/forms.py:11 +msgid "Redirect URIs" +msgstr "" + +#: tests/end2end/test_editing_client.py:248 xl_auth/client/forms.py:12 +msgid "Default scopes" +msgstr "" + #: tests/end2end/test_editing_collection.py:53 tests/end2end/test_registering_collection.py:52 msgid "category" msgstr "" @@ -63,20 +107,6 @@ msgstr "" msgid "Code cannot be modified" msgstr "" -#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_registering_collection.py:102 -#: xl_auth/collection/forms.py:18 xl_auth/templates/users/home.html:18 -#: xl_auth/templates/users/home.html:61 -msgid "Name" -msgstr "" - -#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_editing_user.py:146 -#: tests/end2end/test_registering_collection.py:79 tests/end2end/test_registering_collection.py:102 -#: tests/forms/test_collection.py:21 tests/forms/test_collection.py:38 -#: tests/forms/test_permission.py:31 tests/forms/test_permission.py:39 -#: tests/forms/test_permission.py:47 tests/forms/test_permission.py:55 -msgid "This field is required." -msgstr "" - #: tests/end2end/test_editing_collection.py:124 tests/end2end/test_registering_collection.py:125 #: xl_auth/collection/forms.py:19 xl_auth/templates/collections/home.html:23 #: xl_auth/templates/collections/home.html:68 @@ -112,19 +142,19 @@ msgstr "" msgid "Users" msgstr "" -#: tests/end2end/test_editing_user.py:45 xl_auth/templates/permissions/home.html:30 -#: xl_auth/templates/permissions/home.html:31 xl_auth/templates/permissions/home.html:32 -#: xl_auth/templates/users/home.html:33 xl_auth/templates/users/home.html:76 -#: xl_auth/templates/users/profile.html:58 xl_auth/templates/users/profile.html:61 -#: xl_auth/templates/users/profile.html:65 +#: tests/end2end/test_editing_user.py:45 xl_auth/templates/clients/home.html:29 +#: xl_auth/templates/permissions/home.html:30 xl_auth/templates/permissions/home.html:31 +#: xl_auth/templates/permissions/home.html:32 xl_auth/templates/users/home.html:33 +#: xl_auth/templates/users/home.html:76 xl_auth/templates/users/profile.html:58 +#: xl_auth/templates/users/profile.html:61 xl_auth/templates/users/profile.html:65 msgid "Yes" msgstr "" -#: tests/end2end/test_editing_user.py:45 xl_auth/templates/permissions/home.html:30 -#: xl_auth/templates/permissions/home.html:31 xl_auth/templates/permissions/home.html:32 -#: xl_auth/templates/users/home.html:33 xl_auth/templates/users/home.html:76 -#: xl_auth/templates/users/profile.html:58 xl_auth/templates/users/profile.html:61 -#: xl_auth/templates/users/profile.html:65 +#: tests/end2end/test_editing_user.py:45 xl_auth/templates/clients/home.html:31 +#: xl_auth/templates/permissions/home.html:30 xl_auth/templates/permissions/home.html:31 +#: xl_auth/templates/permissions/home.html:32 xl_auth/templates/users/home.html:33 +#: xl_auth/templates/users/home.html:76 xl_auth/templates/users/profile.html:58 +#: xl_auth/templates/users/profile.html:61 xl_auth/templates/users/profile.html:65 msgid "No" msgstr "" @@ -171,6 +201,10 @@ msgstr "" msgid "Unknown username/email" msgstr "" +#: tests/end2end/test_registering_client.py:31 xl_auth/templates/clients/home.html:11 +msgid "New Client" +msgstr "" + #: tests/end2end/test_registering_collection.py:31 tests/end2end/test_registering_collection.py:171 #: xl_auth/templates/collections/home.html:15 msgid "New Collection" @@ -205,6 +239,16 @@ msgstr "" msgid "Email already registered" msgstr "" +#: tests/forms/test_client.py:25 tests/forms/test_client.py:118 tests/forms/test_collection.py:124 +#: tests/forms/test_collection.py:135 tests/forms/test_permission.py:144 +#: tests/forms/test_permission.py:174 tests/forms/test_user.py:63 tests/forms/test_user.py:106 +#: tests/forms/test_user.py:132 tests/forms/test_user.py:169 xl_auth/client/forms.py:41 +#: xl_auth/client/forms.py:68 xl_auth/collection/forms.py:41 xl_auth/collection/forms.py:73 +#: xl_auth/permission/forms.py:59 xl_auth/permission/forms.py:100 xl_auth/user/forms.py:44 +#: xl_auth/user/forms.py:97 xl_auth/user/forms.py:123 xl_auth/user/forms.py:150 +msgid "You do not have sufficient privileges for this operation." +msgstr "" + #: tests/forms/test_collection.py:30 msgid "Field must be between 1 and 5 characters long." msgstr "" @@ -217,15 +261,6 @@ msgstr "" msgid "Field must be between 2 and 255 characters long." msgstr "" -#: tests/forms/test_collection.py:124 tests/forms/test_collection.py:135 -#: tests/forms/test_permission.py:144 tests/forms/test_permission.py:174 tests/forms/test_user.py:63 -#: tests/forms/test_user.py:106 tests/forms/test_user.py:132 tests/forms/test_user.py:169 -#: xl_auth/collection/forms.py:41 xl_auth/collection/forms.py:73 xl_auth/permission/forms.py:59 -#: xl_auth/permission/forms.py:100 xl_auth/user/forms.py:44 xl_auth/user/forms.py:97 -#: xl_auth/user/forms.py:123 xl_auth/user/forms.py:150 -msgid "You do not have sufficient privileges for this operation." -msgstr "" - #: tests/forms/test_permission.py:22 xl_auth/permission/forms.py:104 xl_auth/permission/views.py:61 #: xl_auth/permission/views.py:87 #, python-format @@ -269,6 +304,20 @@ msgstr "" msgid "Replaced by %(replaced_by_code)s" msgstr "" +#: xl_auth/client/forms.py:13 xl_auth/templates/clients/home.html:19 +msgid "Confidential" +msgstr "" + +#: xl_auth/client/views.py:44 +#, python-format +msgid "Client \"%(name)s\" created." +msgstr "" + +#: xl_auth/client/views.py:68 +#, python-format +msgid "Thank you for updating client details for \"%(id)s\"." +msgstr "" + #: xl_auth/collection/forms.py:19 msgid "Bibliography" msgstr "" @@ -366,16 +415,33 @@ msgstr "" msgid "Log out" msgstr "" -#: xl_auth/templates/collections/edit.html:5 -msgid "Edit Existing Collection" +#: xl_auth/templates/clients/edit.html:5 +msgid "Edit OAuth2 Client" msgstr "" -#: xl_auth/templates/collections/edit.html:29 xl_auth/templates/permissions/edit.html:35 -#: xl_auth/templates/users/administer.html:25 xl_auth/templates/users/change_password.html:21 -#: xl_auth/templates/users/edit_details.html:17 +#: xl_auth/templates/clients/edit.html:31 xl_auth/templates/collections/edit.html:29 +#: xl_auth/templates/permissions/edit.html:35 xl_auth/templates/users/administer.html:25 +#: xl_auth/templates/users/change_password.html:21 xl_auth/templates/users/edit_details.html:17 msgid "Save" msgstr "" +#: xl_auth/templates/clients/home.html:4 xl_auth/templates/clients/home.html:8 +msgid "OAuth2 Clients" +msgstr "" + +#: xl_auth/templates/clients/home.html:20 xl_auth/templates/users/home.html:20 +#: xl_auth/templates/users/home.html:63 +msgid "Admin" +msgstr "" + +#: xl_auth/templates/clients/register.html:5 +msgid "Register New OAuth2 Client" +msgstr "" + +#: xl_auth/templates/collections/edit.html:5 +msgid "Edit Existing Collection" +msgstr "" + #: xl_auth/templates/collections/home.html:6 xl_auth/templates/collections/home.html:54 #: xl_auth/templates/collections/home.html:88 msgid "Go to" @@ -498,10 +564,6 @@ msgstr "" msgid "Active Users" msgstr "" -#: xl_auth/templates/users/home.html:20 xl_auth/templates/users/home.html:63 -msgid "Admin" -msgstr "" - #: xl_auth/templates/users/home.html:37 xl_auth/templates/users/home.html:80 msgid "Edit Details" msgstr "" diff --git a/migrations/versions/bc2c31758e2a_add_oauth2_client_table.py b/migrations/versions/bc2c31758e2a_add_oauth2_client_table.py new file mode 100644 index 00000000..0a158b42 --- /dev/null +++ b/migrations/versions/bc2c31758e2a_add_oauth2_client_table.py @@ -0,0 +1,38 @@ +"""Add OAuth2 Client table. + +Revision ID: bc2c31758e2a +Revises: b09534921ab0 +Create Date: 2017-10-26 14:57:37.549116 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'bc2c31758e2a' +down_revision = 'b09534921ab0' +branch_labels = None +depends_on = None + + +def upgrade(): + """Add OAuth2 Client table.""" + op.create_table('clients', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('client_id', sa.String(length=64), nullable=False), + sa.Column('client_secret', sa.String(length=256), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.Column('is_confidential', sa.Boolean(), nullable=False), + sa.Column('redirect_uris', sa.Text(), nullable=False), + sa.Column('default_scopes', sa.Text(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=True), + sa.Column('description', sa.String(length=400), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('client_id'), + sa.UniqueConstraint('client_secret')) + + +def downgrade(): + """Drop OAuth2 Client table.""" + op.drop_table('clients') diff --git a/tests/conftest.py b/tests/conftest.py index 447d12ff..d52969a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,8 @@ from xl_auth.database import db as _db from xl_auth.settings import TestConfig -from .factories import CollectionFactory, PermissionFactory, SuperUserFactory, UserFactory +from .factories import (ClientFactory, CollectionFactory, PermissionFactory, SuperUserFactory, + UserFactory) @pytest.yield_fixture(scope='function') @@ -81,3 +82,12 @@ def permission(db): permission = PermissionFactory() db.session.commit() return permission + + +# noinspection PyShadowingNames +@pytest.fixture +def client(db): + """A client for the tests.""" + client = ClientFactory() + db.session.commit() + return client diff --git a/tests/end2end/test_deleting_client.py b/tests/end2end/test_deleting_client.py new file mode 100644 index 00000000..ed97b0b4 --- /dev/null +++ b/tests/end2end/test_deleting_client.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""Test deleting clients.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from flask import url_for +from flask_babel import gettext as _ + +from xl_auth.client.models import Client + + +def test_superuser_can_delete_existing_client(superuser, client, testapp): + """Delete existing client.""" + old_count = len(Client.query.all()) + name = client.name + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = superuser.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + # Clicks Clients button + # res = res.click(href=url_for('client.home')) + # FIXME: No nav link yet + assert res.lxml.xpath("//a[contains(@href,'{0}')]".format(url_for('client.home'))) == [] + + res = testapp.get('/clients/') + # Clicks Delete button on a client + res = res.click(href=url_for('client.delete', id=client.id)).follow() + assert res.status_code == 200 + # Client was deleted, so number of clients are 1 less than initial state + assert _('Successfully deleted OAuth2 Client "%(name)s".', name=name) in res + assert len(Client.query.all()) == old_count - 1 + + +def test_user_cannot_delete_client(user, client, testapp): + """Attempt to delete a client.""" + old_count = len(Client.query.all()) + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = user.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + + # We see no Clients button + assert res.lxml.xpath("//a[contains(@text,'{0}')]".format(_('Clients'))) == [] + + # Try to go there directly + testapp.get('/clients/', status=403) + + # Try to delete + testapp.delete(url_for('client.delete', id=client.id), status=403) + + # Nothing was deleted + assert len(Client.query.all()) == old_count diff --git a/tests/end2end/test_editing_client.py b/tests/end2end/test_editing_client.py new file mode 100644 index 00000000..44174d4d --- /dev/null +++ b/tests/end2end/test_editing_client.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""Test editing clients.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from flask import url_for + +from xl_auth.client.models import Client + + +def test_superuser_can_edit_existing_client(superuser, client, testapp): + """Edit an existing client.""" + old_count = len(Client.query.all()) + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = superuser.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + + # Clicks Clients button + # res = res.click(href=url_for('client.home')) + # FIXME: No nav link yet + assert res.lxml.xpath("//a[contains(@href,'{0}')]".format(url_for('client.home'))) == [] + + res = testapp.get('/clients/') + # Clicks Edit Client button + res = res.click(href=url_for('client.edit', id=client.id)) + + # Fills out the form + form = res.forms['editForm'] + form['name'] = 'Test Client' + form['description'] = 'Some description' + form['redirect_uris'] = 'http://localhost/' + form['default_scopes'] = 'read write' + + # Submits + res = form.submit().follow() + assert res.status_code == 200 + + # No new client was created + assert len(Client.query.all()) == old_count + + # The new client is listed under existing clients + assert len(res.lxml.xpath("//td[contains(., '{0}')]".format(form['name'].value))) == 1 + assert len(res.lxml.xpath("//td[contains(., '{0}')]".format(form['description'].value))) == 1 + + +def test_user_cannot_edit_existing_client(user, client, testapp): + """Attempt to edit an existing client.""" + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = user.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + + # No Client home button for regular users + assert res.lxml.xpath("//a[contains(@href,'{0}')]".format(url_for('client.home'))) == [] + + # Try to go there directly + testapp.get('/clients/', status=403) + + # Try to go directly to edit + testapp.get(url_for('client.edit', id=client.id), status=403) diff --git a/tests/end2end/test_registering_client.py b/tests/end2end/test_registering_client.py new file mode 100644 index 00000000..a95d8dba --- /dev/null +++ b/tests/end2end/test_registering_client.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +"""Test registering clients.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from flask import url_for +from flask_babel import gettext as _ + +from xl_auth.client.models import Client + + +def test_superuser_can_register_new_client(superuser, testapp): + """Register a new client.""" + old_count = len(Client.query.all()) + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = superuser.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + + # Clicks Clients button + # res = res.click(href=url_for('client.home')) + # FIXME: No nav link yet + assert res.lxml.xpath("//a[contains(@href,'{0}')]".format(url_for('client.home'))) == [] + + res = testapp.get('/clients/') + # Clicks Register New Client button + res = res.click(_('New Client')) + + # Fills out the form + form = res.forms['registerForm'] + form['name'] = 'Test Client' + form['description'] = 'Some description' + form['redirect_uris'] = 'http://localhost/' + form['default_scopes'] = 'read write' + + # Submits + res = form.submit().follow() + assert res.status_code == 200 + + # A new client was created + assert len(Client.query.all()) == old_count + 1 + + # The new client is listed under existing clients + assert len(res.lxml.xpath("//td[contains(., '{0}')]".format(form['name'].value))) == 1 + assert len(res.lxml.xpath("//td[contains(., '{0}')]".format(form['description'].value))) == 1 + + +def test_user_cannot_register_client(user, collection, testapp): + """Attempt to register a client.""" + # Goes to homepage + res = testapp.get('/') + # Fills out login form + form = res.forms['loginForm'] + form['username'] = user.email + form['password'] = 'myPrecious' + # Submits + res = form.submit().follow() + + # No Client home button for regular users + assert res.lxml.xpath("//a[contains(@href,'{0}')]".format(url_for('client.home'))) == [] + + # Try to go there directly + testapp.get('/clients/', status=403) + + # Try to go directly to register + testapp.get('/clients/register/', status=403) diff --git a/tests/factories.py b/tests/factories.py index a837e28d..00b101e0 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -8,6 +8,7 @@ from factory import LazyFunction, PostGenerationMethodCall, Sequence from factory.alchemy import SQLAlchemyModelFactory +from xl_auth.client.models import Client from xl_auth.collection.models import Collection from xl_auth.database import db from xl_auth.permission.models import Permission @@ -77,3 +78,19 @@ class Meta: """Factory configuration.""" model = Permission + + +class ClientFactory(BaseFactory): + """Client factory.""" + + name = Sequence(lambda _: 'name{0}'.format(_)) + description = Sequence(lambda _: 'description{0}'.format(_)) + is_confidential = True + redirect_uris = 'http://example.com' + default_scopes = 'read write' + created_by = '1' + + class Meta: + """Factory configuration.""" + + model = Client diff --git a/tests/forms/test_client.py b/tests/forms/test_client.py new file mode 100644 index 00000000..ff9c9318 --- /dev/null +++ b/tests/forms/test_client.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +"""Test client forms.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from random import choice + +import pytest +from flask_babel import gettext as _ +from wtforms.validators import ValidationError + +from xl_auth.client.forms import EditForm, RegisterForm + + +def test_user_cannot_register_client(user): + """Attempt to register a client as regular user.""" + form = RegisterForm(user, name='Client', + description='OAuth2 Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + with pytest.raises(ValidationError) as e_info: + form.validate() + assert e_info.value.args[0] == _('You do not have sufficient privileges for this operation.') + + +def test_register_form_validate_success(superuser): + """Register client.""" + form = RegisterForm(superuser, name='Client', + description='OAuth2 Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is True + + +def test_register_form_missing_name(superuser): + """Attempt to register client with missing name.""" + form = RegisterForm(superuser, + description='OAuth2 Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.name.errors + + +def test_register_form_too_short_name(superuser): + """Attempt to register client with too short name.""" + form = RegisterForm(superuser, name='C', + description='OAuth2 Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('Field must be between 3 and 64 characters long.') in form.name.errors + + +def test_register_form_missing_description(superuser): + """Attempt to register client with missing description.""" + form = RegisterForm(superuser, name='Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.description.errors + + +def test_register_form_too_short_description(superuser): + """Attempt to register client with too short description.""" + form = RegisterForm(superuser, name='Client', + description='C', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('Field must be between 3 and 350 characters long.') in form.description.errors + + +def test_register_form_missing_redirect_uris(superuser): + """Attempt to register client with missing redirect URIs.""" + form = RegisterForm(superuser, name='Client', + description='OAuth2 Client', + is_confidential=choice([True, False]), + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.redirect_uris.errors + + +def test_register_form_missing_default_scopes(superuser): + """Attempt to register client with missing default scopes.""" + form = RegisterForm(superuser, name='Client', + description='OAuth2 Client', + is_confidential=choice([True, False]), + redirect_uris='http://localhost/') + + assert form.validate() is False + assert _('This field is required.') in form.default_scopes.errors + + +def test_user_cannot_edit_collection(user, client): + """Attempt to edit a client as regular user.""" + form = EditForm(user, name=client.name, + description=client.description, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + with pytest.raises(ValidationError) as e_info: + form.validate() + assert e_info.value.args[0] == _('You do not have sufficient privileges for this operation.') + + +def test_edit_form_validate_success(superuser, client): + """Edit entry with success.""" + form = EditForm(superuser, name=client.name, + description=client.description, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is True + + +def test_edit_form_missing_name(superuser, client): + """Attempt to register client with missing name.""" + form = EditForm(superuser, + description=client.description, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.name.errors + + +def test_edit_form_too_short_name(superuser, client): + """Attempt to register client with too short name.""" + form = EditForm(superuser, name='C', + description=client.description, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('Field must be between 3 and 64 characters long.') in form.name.errors + + +def test_edit_form_missing_description(superuser, client): + """Attempt to register client with missing description.""" + form = EditForm(superuser, name=client.name, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.description.errors + + +def test_edit_form_too_short_description(superuser, client): + """Attempt to register client with too short description.""" + form = EditForm(superuser, name=client.name, + description='C', + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/', + default_scopes='read write') + + assert form.validate() is False + assert _('Field must be between 3 and 350 characters long.') in form.description.errors + + +def test_edit_form_missing_redirect_uris(superuser, client): + """Attempt to register client with missing redirect URIs.""" + form = EditForm(superuser, name=client.name, + description=client.description, + is_confidential=not client.is_confidential, + default_scopes='read write') + + assert form.validate() is False + assert _('This field is required.') in form.redirect_uris.errors + + +def test_edit_form_missing_default_scopes(superuser, client): + """Attempt to register client with missing default scopes.""" + form = EditForm(superuser, name=client.name, + description=client.description, + is_confidential=not client.is_confidential, + redirect_uris='http://localhost/') + + assert form.validate() is False + assert _('This field is required.') in form.default_scopes.errors diff --git a/tests/models/test_client.py b/tests/models/test_client.py new file mode 100644 index 00000000..a5817a7c --- /dev/null +++ b/tests/models/test_client.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""Unit tests for Client model.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +import pytest + +from xl_auth.client.models import Client + +from ..factories import ClientFactory + + +@pytest.mark.usefixtures('db', 'user') +def test_get_by_id(user): + """Get client by ID.""" + client = ClientFactory(created_by=user.id) + client.save() + + retrieved = Client.get_by_id(client.id) + assert retrieved == client + + +@pytest.mark.usefixtures('db', 'user') +def test_factory(db, user): + """Test user factory.""" + client = ClientFactory(created_by=user.id) + db.session.commit() + assert bool(client.client_id) + assert bool(client.client_secret) + assert bool(client.created_by) + assert bool(client.is_confidential) + assert bool(client.name) + assert bool(client.description) + + +@pytest.mark.usefixtures('db') +def test_client_type(): + """Test client_type.""" + confidential_client = ClientFactory() + assert confidential_client.client_type == 'confidential' + + public_client = ClientFactory(is_confidential=False) + assert public_client.client_type == 'public' + + +@pytest.mark.usefixtures('db') +def test_default_redirect_uri(): + """Test default_redirect_uri.""" + client = ClientFactory(redirect_uris='http://example.com/foo http://example.com/bar') + expected = 'http://example.com/foo' + assert client.default_redirect_uri == expected + + +@pytest.mark.usefixtures('db') +def test_repr(): + """Check repr output.""" + client = ClientFactory(name='OAuth2 Client') + assert repr(client) == ''.format('OAuth2 Client') diff --git a/xl_auth/app.py b/xl_auth/app.py index 79988367..ecf8cdc9 100644 --- a/xl_auth/app.py +++ b/xl_auth/app.py @@ -5,7 +5,7 @@ from flask import Flask, render_template -from . import collection, commands, permission, public, user +from . import client, collection, commands, permission, public, user from .extensions import (babel, bcrypt, cache, csrf_protect, db, debug_toolbar, login_manager, migrate, webpack) from .settings import ProdConfig @@ -46,6 +46,7 @@ def register_blueprints(app): app.register_blueprint(user.views.blueprint) app.register_blueprint(collection.views.blueprint) app.register_blueprint(permission.views.blueprint) + app.register_blueprint(client.views.blueprint) return None diff --git a/xl_auth/client/__init__.py b/xl_auth/client/__init__.py new file mode 100644 index 00000000..50ffa620 --- /dev/null +++ b/xl_auth/client/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""The client module.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from . import models, views # noqa diff --git a/xl_auth/client/forms.py b/xl_auth/client/forms.py new file mode 100644 index 00000000..1ccb3971 --- /dev/null +++ b/xl_auth/client/forms.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Client forms.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from flask_babel import lazy_gettext as _ +from flask_wtf import FlaskForm +from wtforms import BooleanField, StringField +from wtforms.validators import DataRequired, Length, ValidationError + +redirect_uris = StringField(_('Redirect URIs'), validators=[DataRequired()]) +default_scopes = StringField(_('Default scopes'), validators=[DataRequired()]) +is_confidential = BooleanField(_('Confidential'), default=True) +name = StringField(_('Name'), validators=[DataRequired(), Length(min=3, max=64)]) +description = StringField(_('Description'), validators=[DataRequired(), Length(min=3, max=350)]) + + +class RegisterForm(FlaskForm): + """Client register form.""" + + redirect_uris = redirect_uris + default_scopes = default_scopes + is_confidential = is_confidential + name = name + description = description + + def __init__(self, current_user, *args, **kwargs): + """Create instance.""" + super(RegisterForm, self).__init__(*args, **kwargs) + self.current_user = current_user + + def validate(self): + """Validate the form.""" + initial_validation = super(RegisterForm, self).validate() + + if not initial_validation: + return False + + if not self.current_user.is_admin: + raise ValidationError(_('You do not have sufficient privileges for this operation.')) + + return True + + +class EditForm(FlaskForm): + """Client edit form.""" + + redirect_uris = redirect_uris + default_scopes = default_scopes + is_confidential = is_confidential + name = name + description = description + + def __init__(self, current_user, *args, **kwargs): + """Create instance.""" + super(EditForm, self).__init__(*args, **kwargs) + self.current_user = current_user + + def validate(self): + """Validate the form.""" + initial_validation = super(EditForm, self).validate() + + if not initial_validation: + return False + + if not self.current_user.is_admin: + raise ValidationError(_('You do not have sufficient privileges for this operation.')) + + return True + + def set_defaults(self, client): + """Apply 'client' attributes as field defaults.""" + self.redirect_uris.default = client.redirect_uris + self.default_scopes.default = client.default_scopes + self.is_confidential.default = client.is_confidential + self.name.default = client.name + self.description.default = client.description + self.process() diff --git a/xl_auth/client/models.py b/xl_auth/client/models.py new file mode 100644 index 00000000..3d3e8633 --- /dev/null +++ b/xl_auth/client/models.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""Client models.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from codecs import getencoder +from os import urandom + +from ..database import Column, Model, SurrogatePK, db + + +class Client(SurrogatePK, Model): + """An OAuth2 Client.""" + + __tablename__ = 'clients' + client_id = Column(db.String(64), unique=True, nullable=False) + client_secret = Column(db.String(256), unique=True, nullable=False) + + created_by = Column(db.ForeignKey('users.id'), nullable=False) + + is_confidential = Column(db.Boolean(), default=True, nullable=False) + + redirect_uris = Column(db.Text(), nullable=False) + default_scopes = Column(db.Text(), nullable=False) + + # Human readable info fields + name = Column(db.String(64)) + description = Column(db.String(400)) + + def __init__(self, **kwargs): + """Create instance.""" + client_id = Client._generate_client_id() + client_secret = Client._generate_client_secret() + Model.__init__(self, client_id=client_id, client_secret=client_secret, **kwargs) + + @staticmethod + def _generate_client_id(): + return getencoder('hex')(urandom(64))[0].decode('utf-8') + + @staticmethod + def _generate_client_secret(): + return getencoder('hex')(urandom(256))[0].decode('utf-8') + + @property + def client_type(self): + """Return client type.""" + if self.is_confidential: + return 'confidential' + else: + return 'public' + + @property + def default_redirect_uri(self): + """Return default redirect URI.""" + if self.redirect_uris: + return self.redirect_uris.split()[0] + else: + return None + + def __repr__(self): + """Represent instance as a unique string.""" + return ''.format(name=self.name) diff --git a/xl_auth/client/views.py b/xl_auth/client/views.py new file mode 100644 index 00000000..f04daf27 --- /dev/null +++ b/xl_auth/client/views.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +"""Client views.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from flask import Blueprint, abort, flash, redirect, render_template, request, url_for +from flask_babel import lazy_gettext as _ +from flask_login import current_user, login_required + +from ..utils import flash_errors +from .forms import EditForm, RegisterForm +from .models import Client + +blueprint = Blueprint('client', __name__, url_prefix='/clients', static_folder='../static') + + +@blueprint.route('/') +@login_required +def home(): + """Client landing page.""" + if not current_user.is_admin: + abort(403) + + clients = Client.query.all() + + return render_template('clients/home.html', clients=clients) + + +@blueprint.route('/register/', methods=['GET', 'POST']) +@login_required +def register(): + """Create new client.""" + if not current_user.is_admin: + abort(403) + + register_form = RegisterForm(current_user, request.form) + if register_form.validate_on_submit(): + Client.create(name=register_form.name.data, + description=register_form.description.data, + is_confidential=register_form.is_confidential.data, + redirect_uris=register_form.redirect_uris.data, + default_scopes=register_form.default_scopes.data, + created_by=current_user.id).save() + flash(_('Client "%(name)s" created.', name=register_form.name.data), 'success') + return redirect(url_for('client.home')) + else: + flash_errors(register_form) + return render_template('clients/register.html', register_form=register_form) + + +@blueprint.route('/delete/', methods=['GET', 'DELETE']) +@login_required +def delete(id): + """Delete client.""" + if not current_user.is_admin: + abort(403) + + client = Client.query.get(id) + if not client: + abort(404) + else: + name = client.name + client.delete() + flash(_('Successfully deleted OAuth2 Client "%(name)s".', name=name), 'success') + return redirect(url_for('client.home')) + + +@blueprint.route('/edit/', methods=['GET', 'POST']) +@login_required +def edit(id): + """Edit client details.""" + if not current_user.is_admin: + abort(403) + + client = Client.query.get(id) + if not client: + abort(404) + + edit_form = EditForm(current_user, request.form) + if edit_form.validate_on_submit(): + client.update(name=edit_form.name.data, description=edit_form.description.data, + is_confidential=edit_form.is_confidential.data, + redirect_uris=edit_form.redirect_uris.data, + default_scopes=edit_form.default_scopes.data).save() + flash(_('Thank you for updating client details for "%(id)s".', id=id), + 'success') + return redirect(url_for('client.home')) + else: + edit_form.set_defaults(client) + flash_errors(edit_form) + return render_template( + 'clients/edit.html', edit_form=edit_form, client=client) diff --git a/xl_auth/templates/clients/edit.html b/xl_auth/templates/clients/edit.html new file mode 100644 index 00000000..bc30f79e --- /dev/null +++ b/xl_auth/templates/clients/edit.html @@ -0,0 +1,42 @@ + +{% extends "layout.html" %} +{% block content %} +
+

{{ _('Edit OAuth2 Client') }}

+
+
+
+

{{ _('OAuth2 Client Credentials') }}

+
+ +

{{ _('Client ID') }}:

+

{{ _('Client Secret') }}:

+
+
+ +
+ {{ edit_form.name.label }} + {{ edit_form.name(placeholder="Client name", class_="form-control") }} +
+
+ {{ edit_form.description.label }} + {{ edit_form.description(placeholder="Client description", class_="form-control") }} +
+
+ +
+
+ {{ edit_form.redirect_uris.label }} + {{ edit_form.redirect_uris(placeholder="Redirect URIs", class_="form-control") }} +
+
+ {{ edit_form.default_scopes.label }} + {{ edit_form.default_scopes(placeholder="read write", class_="form-control") }} +
+

+
+
+{% endblock %} diff --git a/xl_auth/templates/clients/home.html b/xl_auth/templates/clients/home.html new file mode 100644 index 00000000..685031ee --- /dev/null +++ b/xl_auth/templates/clients/home.html @@ -0,0 +1,51 @@ + +{% extends "layout.html" %} +{% block content %} +

{{ _('OAuth2 Clients') }}

+
+
+
+ {{ _('OAuth2 Clients') }} + {% if current_user.is_admin %} + {{ _('New Client') }} + {% endif %} +
+ + + + + + + + + + + {% for client in clients %} + + + + {% if client.is_confidential %} + + {% else %} + + {% endif %} + {% if current_user.is_admin %} + + {% endif %} + + {% endfor %} + +
{{ _('Name') }}{{ _('Description') }}{{ _('Confidential') }}{{ _('Admin') }}
{{ client.name }}{{ client.description }}{{ _('Yes') }}{{ _('No') }} + + + + + + {{ _('Delete client') }} + +
+
+{% endblock %} diff --git a/xl_auth/templates/clients/register.html b/xl_auth/templates/clients/register.html new file mode 100644 index 00000000..44a27c69 --- /dev/null +++ b/xl_auth/templates/clients/register.html @@ -0,0 +1,34 @@ + +{% extends "layout.html" %} +{% block content %} +
+

{{ _('Register New OAuth2 Client') }}

+
+
+ +
+ {{ register_form.name.label }} + {{ register_form.name(placeholder="Client name", class_="form-control") }} +
+
+ {{ register_form.description.label }} + {{ register_form.description(placeholder="Client description", class_="form-control") }} +
+
+ +
+
+ {{ register_form.redirect_uris.label }} + {{ register_form.redirect_uris(placeholder="Redirect URIs", class_="form-control") }} +
+
+ {{ register_form.default_scopes.label }} + {{ register_form.default_scopes(placeholder="read write", class_="form-control") }} +
+

+
+
+{% endblock %} diff --git a/xl_auth/translations/sv/LC_MESSAGES/messages.po b/xl_auth/translations/sv/LC_MESSAGES/messages.po index 6c624e1d..9c29e7ae 100644 --- a/xl_auth/translations/sv/LC_MESSAGES/messages.po +++ b/xl_auth/translations/sv/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.2.1\n" "Report-Msgid-Bugs-To: mats.blomdahl@gmail.com\n" -"POT-Creation-Date: 2017-10-25 13:18+0200\n" +"POT-Creation-Date: 2017-10-25 14:02+0200\n" "PO-Revision-Date: 2017-09-19 12:23+0200\n" "Last-Translator: Mats Blomdahl \n" "Language: sv\n" @@ -30,7 +30,8 @@ msgstr "Behörigheter" #: tests/end2end/test_deleting_permission.py:26 tests/end2end/test_editing_collection.py:158 #: tests/end2end/test_editing_permission.py:31 tests/end2end/test_editing_permission.py:66 #: tests/end2end/test_editing_user.py:189 tests/end2end/test_editing_user.py:215 -#: xl_auth/templates/collections/home.html:42 xl_auth/templates/permissions/home.html:37 +#: xl_auth/templates/clients/home.html:35 xl_auth/templates/collections/home.html:42 +#: xl_auth/templates/permissions/home.html:37 msgid "Edit" msgstr "Ändra" @@ -43,6 +44,49 @@ msgstr "🙀 Ta bort behörighet" msgid "Successfully deleted permissions for \"%(username)s\" on collection \"%(code)s\"." msgstr "Behörighet borttagen för \"%(username)s\" på samling \"%(code)s\"." +#: tests/end2end/test_editing_client.py:82 tests/end2end/test_editing_client.py:115 +#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_registering_collection.py:102 +#: xl_auth/client/forms.py:14 xl_auth/collection/forms.py:18 xl_auth/templates/clients/home.html:17 +#: xl_auth/templates/users/home.html:18 xl_auth/templates/users/home.html:61 +msgid "Name" +msgstr "Namn" + +#: tests/end2end/test_editing_client.py:82 tests/end2end/test_editing_client.py:148 +#: tests/end2end/test_editing_client.py:215 tests/end2end/test_editing_client.py:248 +#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_editing_user.py:146 +#: tests/end2end/test_registering_collection.py:79 tests/end2end/test_registering_collection.py:102 +#: tests/forms/test_client.py:48 tests/forms/test_client.py:71 tests/forms/test_client.py:94 +#: tests/forms/test_client.py:105 tests/forms/test_client.py:141 tests/forms/test_client.py:164 +#: tests/forms/test_client.py:187 tests/forms/test_client.py:198 tests/forms/test_collection.py:21 +#: tests/forms/test_collection.py:38 tests/forms/test_permission.py:31 +#: tests/forms/test_permission.py:39 tests/forms/test_permission.py:47 +#: tests/forms/test_permission.py:55 +msgid "This field is required." +msgstr "Det här fältet är obligatoriskt." + +#: tests/end2end/test_editing_client.py:115 tests/forms/test_client.py:60 +#: tests/forms/test_client.py:153 +msgid "Field must be between 3 and 64 characters long." +msgstr "Fältet måste vara mellan 3 och 64 tecken långt." + +#: tests/end2end/test_editing_client.py:148 tests/end2end/test_editing_client.py:181 +#: xl_auth/client/forms.py:15 xl_auth/templates/clients/home.html:18 +msgid "Description" +msgstr "Beskrivning" + +#: tests/end2end/test_editing_client.py:182 tests/forms/test_client.py:83 +#: tests/forms/test_client.py:176 +msgid "Field must be between 3 and 350 characters long." +msgstr "Fältet måste vara mellan 3 och 350 tecken långt." + +#: tests/end2end/test_editing_client.py:215 xl_auth/client/forms.py:11 +msgid "Redirect URIs" +msgstr "Redirect URIs" + +#: tests/end2end/test_editing_client.py:248 xl_auth/client/forms.py:12 +msgid "Default scopes" +msgstr "Default scopes" + #: tests/end2end/test_editing_collection.py:53 tests/end2end/test_registering_collection.py:52 msgid "category" msgstr "kategori" @@ -64,20 +108,6 @@ msgstr "Sigel" msgid "Code cannot be modified" msgstr "Koden kan inte ändras" -#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_registering_collection.py:102 -#: xl_auth/collection/forms.py:18 xl_auth/templates/users/home.html:18 -#: xl_auth/templates/users/home.html:61 -msgid "Name" -msgstr "Namn" - -#: tests/end2end/test_editing_collection.py:102 tests/end2end/test_editing_user.py:146 -#: tests/end2end/test_registering_collection.py:79 tests/end2end/test_registering_collection.py:102 -#: tests/forms/test_collection.py:21 tests/forms/test_collection.py:38 -#: tests/forms/test_permission.py:31 tests/forms/test_permission.py:39 -#: tests/forms/test_permission.py:47 tests/forms/test_permission.py:55 -msgid "This field is required." -msgstr "Det här fältet är obligatoriskt." - #: tests/end2end/test_editing_collection.py:124 tests/end2end/test_registering_collection.py:125 #: xl_auth/collection/forms.py:19 xl_auth/templates/collections/home.html:23 #: xl_auth/templates/collections/home.html:68 @@ -113,19 +143,19 @@ msgstr "En behörighet för användare \"%(username)s\" på samling \"%(code)s\" msgid "Users" msgstr "Användare" -#: tests/end2end/test_editing_user.py:45 xl_auth/templates/permissions/home.html:30 -#: xl_auth/templates/permissions/home.html:31 xl_auth/templates/permissions/home.html:32 -#: xl_auth/templates/users/home.html:33 xl_auth/templates/users/home.html:76 -#: xl_auth/templates/users/profile.html:58 xl_auth/templates/users/profile.html:61 -#: xl_auth/templates/users/profile.html:65 +#: tests/end2end/test_editing_user.py:45 xl_auth/templates/clients/home.html:29 +#: xl_auth/templates/permissions/home.html:30 xl_auth/templates/permissions/home.html:31 +#: xl_auth/templates/permissions/home.html:32 xl_auth/templates/users/home.html:33 +#: xl_auth/templates/users/home.html:76 xl_auth/templates/users/profile.html:58 +#: xl_auth/templates/users/profile.html:61 xl_auth/templates/users/profile.html:65 msgid "Yes" msgstr "Ja" -#: tests/end2end/test_editing_user.py:45 xl_auth/templates/permissions/home.html:30 -#: xl_auth/templates/permissions/home.html:31 xl_auth/templates/permissions/home.html:32 -#: xl_auth/templates/users/home.html:33 xl_auth/templates/users/home.html:76 -#: xl_auth/templates/users/profile.html:58 xl_auth/templates/users/profile.html:61 -#: xl_auth/templates/users/profile.html:65 +#: tests/end2end/test_editing_user.py:45 xl_auth/templates/clients/home.html:31 +#: xl_auth/templates/permissions/home.html:30 xl_auth/templates/permissions/home.html:31 +#: xl_auth/templates/permissions/home.html:32 xl_auth/templates/users/home.html:33 +#: xl_auth/templates/users/home.html:76 xl_auth/templates/users/profile.html:58 +#: xl_auth/templates/users/profile.html:61 xl_auth/templates/users/profile.html:65 msgid "No" msgstr "Nej" @@ -172,6 +202,10 @@ msgstr "Felaktigt lösenord" msgid "Unknown username/email" msgstr "Okänd epost/användarnamn" +#: tests/end2end/test_registering_client.py:31 xl_auth/templates/clients/home.html:11 +msgid "New Client" +msgstr "Ny klient" + #: tests/end2end/test_registering_collection.py:31 tests/end2end/test_registering_collection.py:171 #: xl_auth/templates/collections/home.html:15 msgid "New Collection" @@ -206,6 +240,16 @@ msgstr "Lösenorden måste stämma överens" msgid "Email already registered" msgstr "Epost-adressen är redan registrerad" +#: tests/forms/test_client.py:25 tests/forms/test_client.py:118 tests/forms/test_collection.py:124 +#: tests/forms/test_collection.py:135 tests/forms/test_permission.py:144 +#: tests/forms/test_permission.py:174 tests/forms/test_user.py:63 tests/forms/test_user.py:106 +#: tests/forms/test_user.py:132 tests/forms/test_user.py:169 xl_auth/client/forms.py:41 +#: xl_auth/client/forms.py:68 xl_auth/collection/forms.py:41 xl_auth/collection/forms.py:73 +#: xl_auth/permission/forms.py:59 xl_auth/permission/forms.py:100 xl_auth/user/forms.py:44 +#: xl_auth/user/forms.py:97 xl_auth/user/forms.py:123 xl_auth/user/forms.py:150 +msgid "You do not have sufficient privileges for this operation." +msgstr "Du har inte tillräcklig behörighet för att utföra denna operation." + #: tests/forms/test_collection.py:30 msgid "Field must be between 1 and 5 characters long." msgstr "Fältet måste vara mellan 1 och 5 tecken långt." @@ -218,15 +262,6 @@ msgstr "Koden existerar ej" msgid "Field must be between 2 and 255 characters long." msgstr "Fältet måste vara mellan 2 och 255 tecken långt." -#: tests/forms/test_collection.py:124 tests/forms/test_collection.py:135 -#: tests/forms/test_permission.py:144 tests/forms/test_permission.py:174 tests/forms/test_user.py:63 -#: tests/forms/test_user.py:106 tests/forms/test_user.py:132 tests/forms/test_user.py:169 -#: xl_auth/collection/forms.py:41 xl_auth/collection/forms.py:73 xl_auth/permission/forms.py:59 -#: xl_auth/permission/forms.py:100 xl_auth/user/forms.py:44 xl_auth/user/forms.py:97 -#: xl_auth/user/forms.py:123 xl_auth/user/forms.py:150 -msgid "You do not have sufficient privileges for this operation." -msgstr "Du har inte tillräcklig behörighet för att utföra denna operation." - #: tests/forms/test_permission.py:22 xl_auth/permission/forms.py:104 xl_auth/permission/views.py:61 #: xl_auth/permission/views.py:87 #, python-format @@ -270,6 +305,20 @@ msgstr "Ersätter samling %(replaces_code)s" msgid "Replaced by %(replaced_by_code)s" msgstr "Ersatt av samling %(replaced_by_code)s" +#: xl_auth/client/forms.py:13 xl_auth/templates/clients/home.html:19 +msgid "Confidential" +msgstr "Konfidentiell" + +#: xl_auth/client/views.py:44 +#, python-format +msgid "Client \"%(name)s\" created." +msgstr "Klient \"%(name)s\" skapad." + +#: xl_auth/client/views.py:68 +#, python-format +msgid "Thank you for updating client details for \"%(id)s\"." +msgstr "Tack för att du uppdaterade detaljer för klient \"%(id)s\"." + #: xl_auth/collection/forms.py:19 msgid "Bibliography" msgstr "Bibliografi" @@ -367,16 +416,33 @@ msgstr "Samlingar/sigler" msgid "Log out" msgstr "Logga ut" -#: xl_auth/templates/collections/edit.html:5 -msgid "Edit Existing Collection" -msgstr "Ändra befintlig samling" +#: xl_auth/templates/clients/edit.html:5 +msgid "Edit OAuth2 Client" +msgstr "Redigera OAuth2-klient" -#: xl_auth/templates/collections/edit.html:29 xl_auth/templates/permissions/edit.html:35 -#: xl_auth/templates/users/administer.html:25 xl_auth/templates/users/change_password.html:21 -#: xl_auth/templates/users/edit_details.html:17 +#: xl_auth/templates/clients/edit.html:31 xl_auth/templates/collections/edit.html:29 +#: xl_auth/templates/permissions/edit.html:35 xl_auth/templates/users/administer.html:25 +#: xl_auth/templates/users/change_password.html:21 xl_auth/templates/users/edit_details.html:17 msgid "Save" msgstr "Spara" +#: xl_auth/templates/clients/home.html:4 xl_auth/templates/clients/home.html:8 +msgid "OAuth2 Clients" +msgstr "OAuth2-klienter" + +#: xl_auth/templates/clients/home.html:20 xl_auth/templates/users/home.html:20 +#: xl_auth/templates/users/home.html:63 +msgid "Admin" +msgstr "Admin" + +#: xl_auth/templates/clients/register.html:5 +msgid "Register New OAuth2 Client" +msgstr "Registrera ny OAuth2-klient" + +#: xl_auth/templates/collections/edit.html:5 +msgid "Edit Existing Collection" +msgstr "Ändra befintlig samling" + #: xl_auth/templates/collections/home.html:6 xl_auth/templates/collections/home.html:54 #: xl_auth/templates/collections/home.html:88 msgid "Go to" @@ -504,10 +570,6 @@ msgstr "Byt användarlösenord" msgid "Active Users" msgstr "Aktiva användare" -#: xl_auth/templates/users/home.html:20 xl_auth/templates/users/home.html:63 -msgid "Admin" -msgstr "Admin" - #: xl_auth/templates/users/home.html:37 xl_auth/templates/users/home.html:80 msgid "Edit Details" msgstr "Ändra profil"