Projet

Général

Profil

0001-admin-manage-api-access-keys-48751.patch

Nicolas Roche, 29 novembre 2020 19:35

Télécharger (20 ko)

Voir les différences:

Subject: [PATCH 1/3] admin: manage api access keys (#48751)

 tests/admin_pages/test_api_access.py          | 166 +++++++++++++++++
 wcs/admin/api_access.py                       | 173 ++++++++++++++++++
 wcs/admin/settings.py                         |   9 +-
 wcs/api_access.py                             |  41 +++++
 wcs/templates/wcs/backoffice/api_access.html  |  22 +++
 .../wcs/backoffice/api_accesses.html          |  22 +++
 6 files changed, 432 insertions(+), 1 deletion(-)
 create mode 100644 tests/admin_pages/test_api_access.py
 create mode 100644 wcs/admin/api_access.py
 create mode 100644 wcs/api_access.py
 create mode 100644 wcs/templates/wcs/backoffice/api_access.html
 create mode 100644 wcs/templates/wcs/backoffice/api_accesses.html
tests/admin_pages/test_api_access.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2020  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import pytest
18

  
19
from wcs.qommon.http_request import HTTPRequest
20
from wcs.api_access import ApiAccess
21

  
22
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
23
from .test_all import create_superuser
24

  
25

  
26
def pytest_generate_tests(metafunc):
27
    if 'pub' in metafunc.fixturenames:
28
        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
29

  
30

  
31
@pytest.fixture
32
def pub(request):
33
    pub = create_temporary_pub(
34
        sql_mode=bool('sql' in request.param),
35
        templates_mode=bool('templates' in request.param)
36
    )
37

  
38
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
39
    pub.set_app_dir(req)
40
    pub.cfg['identification'] = {'methods': ['password']}
41
    pub.cfg['language'] = {'language': 'en'}
42
    pub.write_cfg()
43

  
44
    return pub
45

  
46

  
47
def teardown_module(module):
48
    clean_temporary_pub()
49

  
50

  
51
@pytest.fixture
52
def api_access():
53
    ApiAccess.wipe()
54
    obj = ApiAccess()
55
    obj.name = 'Jhon'
56
    obj.description = 'API key for Jhon'
57
    obj.access_identifier = 'jhon'
58
    obj.access_key = '12345'
59
    obj.store()
60
    return obj
61

  
62

  
63
def test_api_access_new(pub):
64
    create_superuser(pub)
65
    ApiAccess.wipe()
66
    app = login(get_app(pub))
67

  
68
    # go to the page and cancel
69
    resp = app.get('/backoffice/settings/api-access/')
70
    resp = resp.click('New API access')
71
    resp = resp.forms[0].submit('cancel')
72
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
73

  
74
    # go to the page and add an API access
75
    resp = app.get('/backoffice/settings/api-access/')
76
    resp = resp.click('New API access')
77
    resp.form['name'] = 'a new API access'
78
    resp.form['description'] = 'description'
79
    resp.form['access_identifier'] = 'new_access'
80
    resp.form['access_key'] = '1234'
81
    resp = resp.form.submit('submit')
82
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
83
    resp = resp.follow()
84
    assert 'a new API access' in resp.text
85
    resp = resp.click('a new API access')
86
    assert 'API access - a new API access' in resp.text
87

  
88
    # check name unicity
89
    resp = app.get('/backoffice/settings/api-access/new')
90
    resp.form['name'] = 'a new API access'
91
    resp.form['access_identifier'] = 'changed'
92
    resp.form['access_key'] = '1234'
93
    resp = resp.form.submit('submit')
94
    assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
95

  
96
    # check access_identifier unicity
97
    resp.form['name'] = 'new one'
98
    resp.form['access_identifier'] = 'new_access'
99
    resp = resp.form.submit('submit')
100
    assert resp.html.find('div', {'class': 'error'}).text == 'This value is already used.'
101

  
102

  
103
def test_api_access_view(pub, api_access):
104
    create_superuser(pub)
105

  
106
    app = login(get_app(pub))
107
    resp = app.get('/backoffice/settings/api-access/%s/' % api_access.id)
108
    assert '12345' in resp.text
109

  
110
    resp = app.get('/backoffice/settings/api-access/wrong-id/', status=404)
111

  
112

  
113
def test_api_access_edit(pub, api_access):
114
    create_superuser(pub)
115

  
116
    app = login(get_app(pub))
117

  
118
    resp = app.get('/backoffice/settings/api-access/1/')
119
    resp = resp.click(href='edit')
120
    assert resp.form['name'].value == 'Jhon'
121
    resp = resp.form.submit('cancel')
122
    assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
123
    resp = resp.follow()
124
    resp = resp.click(href='edit')
125
    resp.form['name'] = 'Smith Robert'
126
    resp.form['description'] = 'bla bla bla'
127
    resp.form['access_identifier'] = 'smith2'
128
    resp.form['access_key'] = '5678'
129
    resp = resp.form.submit('submit')
130
    assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
131
    resp = resp.follow()
132

  
133
    api_access = ApiAccess.get('1')
134
    assert api_access.name == 'Smith Robert'
135
    assert api_access.description == 'bla bla bla'
136
    assert api_access.access_identifier == 'smith2'
137
    assert api_access.access_key == '5678'
138

  
139
    # check name unicity
140
    resp = app.get('/backoffice/settings/api-access/new')
141
    resp.form['name'] = 'Jhon'
142
    resp.form['access_identifier'] = 'jhon'
143
    resp.form['access_key'] = '1234'
144
    resp = resp.form.submit('submit')
145
    resp = app.get('/backoffice/settings/api-access/1/')
146
    resp = resp.click(href='edit')
147
    resp.form['name'] = 'Jhon'
148
    resp = resp.form.submit('submit')
149
    assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
150

  
151

  
152
def test_api_access_delete(pub, api_access):
153
    create_superuser(pub)
154

  
155
    app = login(get_app(pub))
156

  
157
    resp = app.get('/backoffice/settings/api-access/1/')
158
    resp = resp.click(href='delete')
159
    resp = resp.form.submit('cancel')
160
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
161

  
162
    resp = app.get('/backoffice/settings/api-access/1/')
163
    resp = resp.click(href='delete')
164
    resp = resp.form.submit('submit')
165
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
166
    assert ApiAccess.count() == 0
wcs/admin/api_access.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2020  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from quixote import get_response, redirect
18
from quixote.directory import Directory
19
from quixote.html import TemplateIO, htmltext
20

  
21
from wcs.qommon import _, errors, template
22
from wcs.qommon.form import Form, StringWidget, TextWidget, HtmlWidget
23
from wcs.qommon.backoffice.menu import html_top
24
from wcs.api_access import ApiAccess
25

  
26

  
27
class ApiAccessUI(object):
28
    def __init__(self, api_access):
29
        self.api_access = api_access
30
        if self.api_access is None:
31
            self.api_access = ApiAccess()
32

  
33
    def get_form(self):
34
        form = Form(enctype='multipart/form-data',
35
                    advanced_label=_('Additional options'))
36
        form.add(StringWidget, 'name', title=_('Name'), required=True, size=30,
37
                 value=self.api_access.name)
38
        form.add(TextWidget, 'description', title=_('Description'),
39
                 cols=40, rows=5,
40
                 value=self.api_access.description)
41
        form.add(StringWidget, 'access_identifier', title=_('Access identifier'),
42
                 required=True, size=30,
43
                 value=self.api_access.access_identifier)
44
        form.add(StringWidget, 'access_key', title=_('Access key'), required=True, size=30,
45
                 value=self.api_access.access_key)
46
        if not self.api_access.is_readonly():
47
            form.add_submit('submit', _('Submit'))
48
        form.add_submit('cancel', _('Cancel'))
49
        return form
50

  
51
    def submit_form(self, form):
52
        name = form.get_widget('name').parse()
53
        access_identifier = form.get_widget('access_identifier').parse()
54

  
55
        for api_access in ApiAccess.select():
56
            if api_access.id == self.api_access.id:
57
                continue
58
            if name == api_access.name:
59
                form.get_widget('name').set_error(_('This name is already used.'))
60
            if access_identifier and access_identifier == api_access.access_identifier:
61
                form.get_widget('access_identifier').set_error(_('This value is already used.'))
62
        if form.has_errors():
63
            raise ValueError()
64

  
65
        self.api_access.name = name
66
        self.api_access.description = form.get_widget('description').parse()
67
        self.api_access.access_identifier = access_identifier
68
        self.api_access.access_key = form.get_widget('access_key').parse()
69
        self.api_access.store()
70

  
71

  
72
class ApiAccessPage(Directory):
73
    _q_exports = ['', 'edit', 'delete',]
74

  
75
    def __init__(self, component, instance=None):
76
        try:
77
            self.api_access = instance or ApiAccess.get(component)
78
        except KeyError:
79
            raise errors.TraversalError()
80
        self.api_access_ui = ApiAccessUI(self.api_access)
81
        get_response().breadcrumb.append((component + '/', self.api_access.name))
82

  
83
    def get_sidebar(self):
84
        r = TemplateIO(html=True)
85
        r += htmltext('<ul id="sidebar-actions">')
86
        r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
87
        r += htmltext('</ul>')
88
        return r.getvalue()
89

  
90
    def _q_index(self):
91
        html_top('api-accesss', title=self.api_access.name)
92
        get_response().filter['sidebar'] = self.get_sidebar()
93
        return template.QommonTemplateResponse(
94
            templates=['wcs/backoffice/api_access.html'],
95
            context={'view': self, 'api_access': self.api_access})
96

  
97
    def edit(self):
98
        form = self.api_access_ui.get_form()
99
        if form.get_submit() == 'cancel':
100
            return redirect('.')
101

  
102
        if form.get_submit() == 'submit' and not form.has_errors():
103
            try:
104
                self.api_access_ui.submit_form(form)
105
            except ValueError:
106
                pass
107
            else:
108
                return redirect('../%s/' % self.api_access.id)
109

  
110
        get_response().breadcrumb.append(('edit', _('Edit')))
111
        html_top('api-access', title=_('Edit API access'))
112
        r = TemplateIO(html=True)
113
        r += htmltext('<h2>%s</h2>') % _('Edit API access')
114
        r += form.render()
115
        return r.getvalue()
116

  
117
    def delete(self):
118
        form = Form(enctype='multipart/form-data')
119
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
120
            'You are about to irrevocably delete this API access.')))
121
        form.add_submit('delete', _('Delete'))
122
        form.add_submit('cancel', _('Cancel'))
123
        if form.get_widget('cancel').parse():
124
            return redirect('..')
125
        if not form.is_submitted() or form.has_errors():
126
            get_response().breadcrumb.append(('delete', _('Delete')))
127
            html_top('api_accesss', title=_('Delete API access'))
128
            r = TemplateIO(html=True)
129
            r += htmltext('<h2>%s %s</h2>') % (
130
                _('Deleting API access:'), self.api_access.name)
131
            r += form.render()
132
            return r.getvalue()
133
        else:
134
            self.api_access.remove_self()
135
            return redirect('..')
136

  
137

  
138
class ApiAccessDirectory(Directory):
139
    _q_exports = ['', 'new']
140

  
141
    def _q_traverse(self, path):
142
        get_response().breadcrumb.append(('api-access/', _('API access')))
143
        return super()._q_traverse(path)
144

  
145
    def _q_index(self):
146
        html_top('api-access', title=_('API access'))
147
        return template.QommonTemplateResponse(
148
            templates=['wcs/backoffice/api_accesses.html'],
149
            context={'view': self, 'api_accesses': ApiAccess.select(order_by='name')})
150

  
151
    def new(self):
152
        get_response().breadcrumb.append(('new', _('New')))
153
        api_access_ui = ApiAccessUI(None)
154
        form = api_access_ui.get_form()
155
        if form.get_widget('cancel').parse():
156
            return redirect('.')
157

  
158
        if form.get_submit() == 'submit' and not form.has_errors():
159
            try:
160
                api_access_ui.submit_form(form)
161
            except ValueError:
162
                pass
163
            else:
164
                return redirect('.')
165

  
166
        html_top('api-access', title=_('New API access'))
167
        r = TemplateIO(html=True)
168
        r += htmltext('<h2>%s</h2>') % _('New API access')
169
        r += form.render()
170
        return r.getvalue()
171

  
172
    def _q_lookup(self, component):
173
        return ApiAccessPage(component)
wcs/admin/settings.py
57 57
from wcs.carddef import CardDef
58 58
from wcs.workflows import Workflow, WorkflowImportError
59 59
from wcs.roles import Role
60 60

  
61 61
from wcs.backoffice.studio import StudioDirectory
62 62
from .fields import FieldDefPage, FieldsDirectory
63 63
from .data_sources import NamedDataSourcesDirectory
64 64
from .wscalls import NamedWsCallsDirectory
65
from .api_access import ApiAccessDirectory
65 66

  
66 67

  
67 68
class UserFormDirectory(Directory):
68 69
    _q_exports = ['']
69 70

  
70 71

  
71 72
class IdentificationDirectory(Directory):
72 73
    _q_exports = ['']
......
426 427
    _q_exports = ['', 'themes', 'users',
427 428
            'template', 'emails', 'debug_options', 'language',
428 429
            ('import', 'p_import'), 'export', 'identification', 'sitename',
429 430
            'sms', 'certificates', 'texts', 'install_theme',
430 431
            'session', 'download_theme', 'smstest', 'postgresql',
431 432
            ('admin-permissions', 'admin_permissions'), 'geolocation',
432 433
            'theme_preview', 'filetypes',
433 434
            ('user-template', 'user_template'),
434
            ('data-sources', 'data_sources'), 'wscalls', 'logs']
435
            ('data-sources', 'data_sources'), 'wscalls', 'logs',
436
            ('api-access', 'api_access')]
435 437

  
436 438
    emails = EmailsDirectory()
437 439
    identification = IdentificationDirectory()
438 440
    users = UsersDirectory()
439 441
    texts = TextsDirectory()
440 442
    theme_preview = ThemePreviewDirectory()
441 443
    filetypes = FileTypesDirectory()
442 444
    data_sources = NamedDataSourcesDirectory()
443 445
    wscalls = NamedWsCallsDirectory()
444 446
    logs = LoggerDirectory()
447
    api_access = ApiAccessDirectory()
445 448

  
446 449
    def _q_index(self):
447 450
        html_top('settings', title = _('Settings'))
448 451
        r = TemplateIO(html=True)
449 452

  
450 453
        disabled_screens_option = get_publisher().get_site_option('settings-disabled-screens') or ''
451 454
        disabled_screens = [x.strip() for x in disabled_screens_option.split(',')]
452 455

  
......
486 489
                    _('Session'), _('Configure session management'))
487 490

  
488 491
        if enabled('permissions'):
489 492
            roles = list(Role.select())
490 493
            if roles:
491 494
                r += htmltext('<dt><a href="admin-permissions">%s</a></dt> <dd>%s</dd>') % (
492 495
                    _('Admin Permissions'), _('Configure access to the administration interface'))
493 496

  
497
        if enabled('api-access'):
498
            r += htmltext('<dt><a href="api-access">%s</a></dt> <dd>%s</dd>') % (
499
                _('API access'), _('Configure access to the API endpoints'))
500

  
494 501
        r += htmltext('</dl></div>')
495 502

  
496 503
        if enabled('import-export'):
497 504
            r += htmltext('<div class="section">')
498 505
            r += htmltext('<h2>%s</h2>') % _('Import / Export')
499 506

  
500 507
            r += htmltext('<dl>')
501 508
            r += htmltext('<dt><a href="import">%s</a></dt> <dd>%s</dd>') % (
wcs/api_access.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2020  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from wcs.qommon.xml_storage import XmlStorableObject
18

  
19

  
20
class ApiAccess(XmlStorableObject):
21
    _names = 'apiaccess'
22
    xml_root_node = 'apiaccess'
23
    name = None
24
    access_identifier = None
25
    access_key = None
26
    description = None
27

  
28
    # declarations for serialization
29
    XML_NODES = [
30
        ('name', 'str'),
31
        ('description', 'str'),
32
        ('access_identifier', 'str'),
33
        ('access_key', 'str'),
34
    ]
35

  
36
    @classmethod
37
    def get_access_key(cls, access_identifier):
38
        for api_access in cls.select():
39
            if api_access.access_identifier == access_identifier:
40
                return api_access.access_key
41
        return None
wcs/templates/wcs/backoffice/api_access.html
1
{% load i18n %}
2

  
3
{% block body %}
4
<div id="appbar">
5
  <h2>{% trans "API access" %} - {{ api_access.name }}</h2>
6
<span class="actions">
7
  <a href="edit">{% trans "Edit" %}</a>
8
</span>
9
</div>
10

  
11
{% if api_access.description %}
12
<div class="bo-block">{{ api_access.description }}</div>
13
{% endif %}
14

  
15
<div class="bo-block">
16
  <h3>{% trans "Parameters" %}</h3>
17
  <ul>
18
    <li>{% trans "Access identifier:" %} {{ api_access.access_identifier }}</li>
19
    <li>{% trans "Access key:" %} {{ api_access.access_key }}</li>
20
  </ul>
21
</div>
22
{% endblock %}
wcs/templates/wcs/backoffice/api_accesses.html
1
{% extends "wcs/backoffice/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar-title %}{% trans "API access" %}{% endblock %}
5

  
6
{% block appbar-actions %}
7
<a rel="popup" href="new">{% trans "New API access" %}</a>
8
{% endblock %}
9

  
10
{% block content %}
11
{% if api_accesses %}
12
<ul class="objects-list single-links">
13
  {% for api_access in api_accesses %}
14
  <li><a href="{{ api_access.id }}/">{{ api_access.name }} ({{ api_access.access_identifier }})</a></li>
15
  {% endfor %}
16
</ul>
17
{% else %}
18
<div class="infonotice">
19
{% trans "There are no API access defined." %}
20
</div>
21
{% endif %}
22
{% endblock %}
0
-