Projet

Général

Profil

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

Nicolas Roche, 27 novembre 2020 17:19

Télécharger (33,2 ko)

Voir les différences:

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

 tests/admin_pages/test_api_access.py          | 237 +++++++++++++++++
 tests/test_snapshots.py                       |  58 ++++
 wcs/admin/api_access.py                       | 247 ++++++++++++++++++
 wcs/admin/settings.py                         |   9 +-
 wcs/api_access.py                             |  70 +++++
 wcs/backoffice/snapshots.py                   |   5 +
 wcs/snapshots.py                              |   3 +-
 wcs/templates/wcs/backoffice/api_access.html  |  24 ++
 .../wcs/backoffice/api_accesses.html          |  23 ++
 9 files changed, 674 insertions(+), 2 deletions(-)
 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 xml.etree.ElementTree as ET
18

  
19
from django.utils.six import StringIO
20

  
21
import pytest
22
from webtest import Upload
23

  
24
from wcs.qommon.http_request import HTTPRequest
25
from wcs.api_access import ApiAccess
26

  
27
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
28
from .test_all import create_superuser
29

  
30

  
31
def pytest_generate_tests(metafunc):
32
    if 'pub' in metafunc.fixturenames:
33
        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
34

  
35

  
36
@pytest.fixture
37
def pub(request):
38
    pub = create_temporary_pub(
39
        sql_mode=bool('sql' in request.param),
40
        templates_mode=bool('templates' in request.param)
41
    )
42

  
43
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
44
    pub.set_app_dir(req)
45
    pub.cfg['identification'] = {'methods': ['password']}
46
    pub.cfg['language'] = {'language': 'en'}
47
    pub.write_cfg()
48

  
49
    return pub
50

  
51

  
52
def teardown_module(module):
53
    clean_temporary_pub()
54

  
55

  
56
@pytest.fixture
57
def api_access():
58
    ApiAccess.wipe()
59
    api_access = ApiAccess(name='Jhon')
60
    api_access.description = 'API key for Jhon'
61
    api_access.access_key = '12345'
62
    api_access.store()
63
    return api_access
64

  
65

  
66
def test_api_access_new(pub):
67
    create_superuser(pub)
68
    ApiAccess.wipe()
69
    app = login(get_app(pub))
70

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

  
77
    # go to the page and add an API access
78
    resp = app.get('/backoffice/settings/api-access/')
79
    resp = resp.click('New API access')
80
    resp.form['name'] = 'a new API access'
81
    resp.form['description'] = 'description'
82
    resp.form['access_id'] = 'jhon'
83
    resp.form['access_key'] = '1234'
84
    resp = resp.form.submit('submit')
85
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
86
    resp = resp.follow()
87
    assert 'a new API access' in resp.text
88
    resp = resp.click('a new API access')
89
    assert 'API access - a new API access' in resp.text
90
    resp = resp.click('Edit')
91
    assert 'Edit API access' in resp.text
92
    assert ApiAccess.get('1').name == 'a new API access'
93

  
94
    # a key must be defined
95
    resp = app.get('/backoffice/settings/api-access/new')
96
    resp.form['name'] = 'a new API access'
97
    resp = resp.form.submit('submit')
98
    assert resp.html.find('div', {'class': 'error'}).text == 'required field'
99

  
100
    # can't have 2 api access with same name
101
    resp.form['access_key'] = '1234'
102
    resp = resp.form.submit('submit')
103
    assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
104

  
105
    # can't have 2 api access with same access_id
106
    resp.form['name'] = 'new name'
107
    resp.form['access_id'] = 'jhon'
108
    resp = resp.form.submit('submit')
109
    assert resp.html.find('div', {'class': 'error'}).text == 'This value is already used.'
110

  
111

  
112
def test_api_access_view(pub, api_access):
113
    create_superuser(pub)
114

  
115
    app = login(get_app(pub))
116
    resp = app.get('/backoffice/settings/api-access/%s/' % api_access.id)
117
    assert '12345' in resp.text
118

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

  
121

  
122
def test_api_access_edit(pub, api_access):
123
    create_superuser(pub)
124

  
125
    app = login(get_app(pub))
126

  
127
    resp = app.get('/backoffice/settings/api-access/1/')
128
    resp = resp.click(href='edit')
129
    assert resp.form['name'].value == 'Jhon'
130
    resp = resp.form.submit('cancel')
131
    assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
132
    resp = resp.follow()
133
    resp = resp.click(href='edit')
134
    assert 'access_id' in resp.form.fields
135
    resp.form['name'] = 'Smith Robert'
136
    resp.form['description'] = 'bla bla bla'
137
    resp.form['access_id'] = 'smith2'
138
    resp.form['access_key'] = '5678'
139
    resp = resp.form.submit('submit')
140
    assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
141
    resp = resp.follow()
142

  
143
    assert ApiAccess.get('1').name == 'Smith Robert'
144
    assert ApiAccess.get('1').description == 'bla bla bla'
145
    assert ApiAccess.get('1').access_id == 'smith2'
146
    assert ApiAccess.get('1').access_key == '5678'
147

  
148
    # can't have 2 api access with same name
149
    resp = app.get('/backoffice/settings/api-access/new')
150
    resp.form['name'] = 'Jhon'
151
    resp.form['access_key'] = '1234'
152
    resp = resp.form.submit('submit')
153
    resp = app.get('/backoffice/settings/api-access/1/edit')
154
    resp.form['name'] = 'Jhon'
155
    resp = resp.form.submit('submit')
156
    assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
157

  
158

  
159
def test_api_access_delete(pub, api_access):
160
    create_superuser(pub)
161

  
162
    app = login(get_app(pub))
163

  
164
    resp = app.get('/backoffice/settings/api-access/1/')
165
    resp = resp.click(href='delete')
166
    resp = resp.form.submit('cancel')
167
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
168

  
169
    resp = app.get('/backoffice/settings/api-access/1/')
170
    resp = resp.click(href='delete')
171
    resp = resp.form.submit('submit')
172
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
173
    assert ApiAccess.count() == 0
174

  
175

  
176
def test_api_access_export(pub, api_access):
177
    create_superuser(pub)
178

  
179
    app = login(get_app(pub))
180
    resp = app.get('/backoffice/settings/api-access/1/')
181

  
182
    resp = resp.click(href='export')
183
    xml_export = resp.text
184

  
185
    ds = StringIO(xml_export)
186
    api_access2 = ApiAccess.import_from_xml(ds)
187
    assert api_access2.name == 'Jhon'
188

  
189

  
190
def test_api_access_import(pub, api_access):
191
    create_superuser(pub)
192

  
193
    api_access.access_id = 'foobar'
194
    api_access.store()
195
    api_access_xml = ET.tostring(api_access.export_to_xml(include_id=True))
196
    ApiAccess.wipe()
197
    assert ApiAccess.count() == 0
198

  
199
    app = login(get_app(pub))
200
    resp = app.get('/backoffice/settings/api-access/')
201
    resp = resp.click(href='import')
202
    resp.forms[0]['file'] = Upload('api_access.wcs', api_access_xml)
203
    resp = resp.forms[0].submit()
204
    assert ApiAccess.count() == 1
205
    assert set([aa.access_id for aa in ApiAccess.select()]) == set(['foobar'])
206

  
207
    # check access_id
208
    resp = app.get('/backoffice/settings/api-access/')
209
    resp = resp.click(href='import')
210
    resp.forms[0]['file'] = Upload('api_access.wcs', api_access_xml)
211
    resp = resp.forms[0].submit()
212
    assert ApiAccess.count() == 2
213
    assert set([aa.access_id for aa in ApiAccess.select()]) == set(['foobar', 'jhon'])
214
    resp = app.get('/backoffice/settings/api-access/')
215
    resp = resp.click(href='import')
216
    resp.forms[0]['file'] = Upload('api_access.wcs', api_access_xml)
217
    resp = resp.forms[0].submit()
218
    assert ApiAccess.count() == 3
219
    assert set([aa.access_id for aa in ApiAccess.select()]) == set(
220
        ['foobar', 'jhon', 'jhon_1'])
221

  
222
    # import an invalid file
223
    resp = app.get('/backoffice/settings/api-access/')
224
    resp = resp.click(href='import')
225
    resp = resp.form.submit('cancel')
226
    assert resp.location == 'http://example.net/backoffice/settings/api-access/'
227
    resp = resp.follow()
228
    resp = resp.click(href='import')
229
    resp.form['file'] = Upload('api_access.wcs', b'garbage')
230
    resp = resp.form.submit()
231
    assert 'Invalid File' in resp.text
232

  
233

  
234
def test_api_access_new_access_id(pub, api_access):
235
    api_access.access_id = None
236
    api_access.store()
237
    assert api_access.access_id
tests/test_snapshots.py
10 10
from wcs.blocks import BlockDef
11 11
from wcs.carddef import CardDef
12 12
from wcs.data_sources import NamedDataSource
13 13
from wcs.formdef import FormDef
14 14
from wcs.qommon.form import UploadedFile
15 15
from wcs.workflows import Workflow
16 16
from wcs.workflows import ExportToModel
17 17
from wcs.wscalls import NamedWsCall
18
from wcs.api_access import ApiAccess
18 19

  
19 20
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
20 21
from admin_pages.test_all import create_superuser, create_role
21 22

  
22 23

  
23 24
@pytest.fixture
24 25
def pub(request, emails):
25 26
    pub = create_temporary_pub(sql_mode=True, templates_mode=True, lazy_mode=True,)
......
212 213
    resp.form['action'].value = 'overwrite'
213 214
    resp = resp.form.submit('submit')
214 215
    assert FormDef.count() == 2
215 216
    formdef = FormDef.get(resp.location.split('/')[-2])
216 217
    assert formdef.id == formdef_with_history.id
217 218
    assert formdef.url_name == formdef_with_history.url_name
218 219

  
219 220

  
221
def test_api_access_snapshot_restore(pub):
222
    create_superuser(pub)
223
    create_role()
224
    app = login(get_app(pub))
225

  
226
    api_access = ApiAccess(name='test')
227
    api_access.store()
228
    for i in range(2):
229
        api_access.name = 'test %s' % i
230
        api_access.store()
231

  
232
    assert pub.snapshot_class.count() == 3
233

  
234
    # restore as new
235
    resp = app.get('/backoffice/settings/api-access/%s/history/' % api_access.id)
236
    snapshot = pub.snapshot_class.select_object_history(api_access)[2]
237
    resp = resp.click(href='%s/restore' % snapshot.id)
238
    assert resp.form['action'].value == 'as-new'
239
    resp = resp.form.submit('submit')
240
    assert ApiAccess.count() == 2
241
    api_access2 = ApiAccess.get(resp.location.split('/')[-2])
242
    assert api_access2.name == 'test'
243
    assert api_access2.id != api_access.id
244

  
245
    # restore over
246
    resp = app.get('/backoffice/settings/api-access/%s/history/' % api_access.id)
247
    snapshot = pub.snapshot_class.select_object_history(api_access)[2]
248
    resp = resp.click(href='%s/restore' % snapshot.id)
249
    resp.form['action'].value = 'overwrite'
250
    resp = resp.form.submit('submit')
251
    assert ApiAccess.count() == 2
252
    api_access2 = ApiAccess.get(resp.location.split('/')[-2])
253
    assert api_access2.id == api_access.id
254

  
255

  
220 256
def test_block_snapshot_browse(pub, blocks_feature):
221 257
    create_superuser(pub)
222 258
    create_role()
223 259

  
224 260
    BlockDef.wipe()
225 261
    blockdef = BlockDef()
226 262
    blockdef.name = 'testblock'
227 263
    blockdef.fields = []
......
418 454
    resp = app.get('/backoffice/settings/wscalls/%s/history/' % wscall.id)
419 455
    snapshot = pub.snapshot_class.select_object_history(wscall)[0]
420 456
    resp = resp.click(href='%s/view/' % snapshot.id)
421 457
    assert 'This webservice call is readonly' in resp
422 458
    with pytest.raises(IndexError):
423 459
        resp = resp.click('Edit')
424 460

  
425 461

  
462
def test_api_access_snapshot_browse(pub):
463
    create_superuser(pub)
464
    create_role()
465

  
466
    ApiAccess.wipe()
467
    api_access = ApiAccess(name='test')
468
    api_access.store()
469
    assert pub.snapshot_class.count() == 1
470
    # check calling .store() without changes doesn't create snapshots
471
    api_access.store()
472
    assert pub.snapshot_class.count() == 1
473

  
474
    app = login(get_app(pub))
475

  
476
    resp = app.get('/backoffice/settings/api-access/%s/history/' % api_access.id)
477
    snapshot = pub.snapshot_class.select_object_history(api_access)[0]
478
    resp = resp.click(href='%s/view/' % snapshot.id)
479
    assert 'This API access is readonly' in resp
480
    with pytest.raises(IndexError):
481
        resp = resp.click('Edit')
482

  
483

  
426 484
def test_form_snapshot_save(pub, formdef_with_history):
427 485
    create_superuser(pub)
428 486
    create_role()
429 487
    app = login(get_app(pub))
430 488

  
431 489
    resp = app.get('/backoffice/forms/%s/' % formdef_with_history.id)
432 490
    resp = resp.click('Save snapshot')
433 491
    resp.form['label'] = 'test snapshot'
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
import xml.etree.ElementTree as ET
18

  
19
from quixote import get_publisher, get_response, redirect
20
from quixote.directory import Directory
21
from quixote.html import TemplateIO, htmltext
22

  
23
from wcs.qommon import _, errors, template
24
from wcs.qommon import misc
25
from wcs.qommon.form import (
26
    Form, StringWidget, TextWidget, HtmlWidget, FileWidget, get_session, force_str)
27
from wcs.qommon.backoffice.menu import html_top
28
from wcs.api_access import ApiAccess
29
from wcs.backoffice.snapshots import SnapshotsDirectory
30

  
31

  
32
class ApiAccessUI(object):
33
    def __init__(self, api_access):
34
        self.api_access = api_access
35
        if self.api_access is None:
36
            self.api_access = ApiAccess()
37

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

  
55
    def submit_form(self, form):
56
        name = form.get_widget('name').parse()
57
        access_id = form.get_widget('access_id').parse()
58

  
59
        for api_access in ApiAccess.select():
60
            if api_access.id == self.api_access.id:
61
                continue
62
            if name == api_access.name:
63
                form.get_widget('name').set_error(_('This name is already used.'))
64
            if access_id == api_access.access_id:
65
                form.get_widget('access_id').set_error(_('This value is already used.'))
66
        if form.has_errors():
67
            raise ValueError()
68

  
69
        self.api_access.name = name
70
        self.api_access.description = form.get_widget('description').parse()
71
        self.api_access.access_id = access_id
72
        self.api_access.access_key = form.get_widget('access_key').parse()
73
        self.api_access.store()
74

  
75

  
76
class ApiAccessPage(Directory):
77
    _q_exports = ['', 'edit', 'delete', 'export',
78
                  ('history', 'snapshots_dir'),]
79

  
80
    def __init__(self, component, instance=None):
81
        try:
82
            self.api_access = instance or ApiAccess.get(component)
83
        except KeyError:
84
            raise errors.TraversalError()
85
        self.api_access_ui = ApiAccessUI(self.api_access)
86
        get_response().breadcrumb.append((component + '/', self.api_access.name))
87
        self.snapshots_dir = SnapshotsDirectory(self.api_access)
88

  
89
    def get_sidebar(self):
90
        r = TemplateIO(html=True)
91
        if self.api_access.is_readonly():
92
            r += htmltext('<div class="infonotice"><p>%s</p></div>') % _(
93
                'This API access is readonly.')
94
        r += htmltext('<ul id="sidebar-actions">')
95
        if not self.api_access.is_readonly():
96
            r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
97
            r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
98
            if get_publisher().snapshot_class:
99
                r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _(
100
                    'Save snapshot')
101
                r += htmltext('<li><a href="history/">%s</a></li>') % _('History')
102
        r += htmltext('</ul>')
103
        return r.getvalue()
104

  
105
    def _q_index(self):
106
        html_top('api-accesss', title=self.api_access.name)
107
        get_response().filter['sidebar'] = self.get_sidebar()
108
        return template.QommonTemplateResponse(
109
            templates=['wcs/backoffice/api_access.html'],
110
            context={'view': self, 'api_access': self.api_access})
111

  
112
    def edit(self):
113
        form = self.api_access_ui.get_form()
114
        if form.get_submit() == 'cancel':
115
            return redirect('.')
116

  
117
        if form.get_submit() == 'submit' and not form.has_errors():
118
            try:
119
                self.api_access_ui.submit_form(form)
120
            except ValueError:
121
                pass
122
            else:
123
                return redirect('../%s/' % self.api_access.id)
124

  
125
        get_response().breadcrumb.append(('edit', _('Edit')))
126
        html_top('api-access', title=_('Edit API access'))
127
        r = TemplateIO(html=True)
128
        r += htmltext('<h2>%s</h2>') % _('Edit API access')
129
        r += form.render()
130
        return r.getvalue()
131

  
132
    def delete(self):
133
        form = Form(enctype='multipart/form-data')
134
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
135
            'You are about to irrevocably delete this API access.')))
136
        form.add_submit('delete', _('Delete'))
137
        form.add_submit('cancel', _('Cancel'))
138
        if form.get_widget('cancel').parse():
139
            return redirect('..')
140
        if not form.is_submitted() or form.has_errors():
141
            get_response().breadcrumb.append(('delete', _('Delete')))
142
            html_top('api_accesss', title=_('Delete API access'))
143
            r = TemplateIO(html=True)
144
            r += htmltext('<h2>%s %s</h2>') % (
145
                _('Deleting API access:'), self.api_access.name)
146
            r += form.render()
147
            return r.getvalue()
148
        else:
149
            self.api_access.remove_self()
150
            return redirect('..')
151

  
152
    def export(self):
153
        x = self.api_access.export_to_xml(include_id=True)
154
        misc.indent_xml(x)
155
        response = get_response()
156
        response.set_content_type('application/x-wcs-api-access')
157
        response.set_header(
158
            'content-disposition',
159
            'attachment; filename=api-access-%s.wcs' % self.api_access.access_id)
160
        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
161

  
162

  
163
class ApiAccessDirectory(Directory):
164
    _q_exports = ['', 'new', ('import', 'p_import')]
165

  
166
    def _q_traverse(self, path):
167
        get_response().breadcrumb.append(('api-access/', _('API access')))
168
        return super(ApiAccessDirectory, self)._q_traverse(path)
169

  
170
    def _q_index(self):
171
        html_top('api-access', title=_('API access'))
172
        return template.QommonTemplateResponse(
173
            templates=['wcs/backoffice/api_accesses.html'],
174
            context={'view': self, 'api_accesses': ApiAccess.select(order_by='name')})
175

  
176
    def new(self):
177
        get_response().breadcrumb.append(('new', _('New')))
178
        api_access_ui = ApiAccessUI(None)
179
        form = api_access_ui.get_form()
180
        if form.get_widget('cancel').parse():
181
            return redirect('.')
182

  
183
        if form.get_submit() == 'submit' and not form.has_errors():
184
            try:
185
                api_access_ui.submit_form(form)
186
            except ValueError:
187
                pass
188
            else:
189
                return redirect('.')
190

  
191
        html_top('api-access', title=_('New API access'))
192
        r = TemplateIO(html=True)
193
        r += htmltext('<h2>%s</h2>') % _('New API access')
194
        r += form.render()
195
        return r.getvalue()
196

  
197
    def _q_lookup(self, component):
198
        return ApiAccessPage(component)
199

  
200
    def p_import(self):
201
        form = Form(enctype='multipart/form-data')
202
        import_title = _('Import API access')
203

  
204
        form.add(FileWidget, 'file', title=_('File'), required=True)
205
        form.add_submit('submit', import_title)
206
        form.add_submit('cancel', _('Cancel'))
207

  
208
        if form.get_submit() == 'cancel':
209
            return redirect('.')
210

  
211
        if form.is_submitted() and not form.has_errors():
212
            try:
213
                return self.import_submit(form)
214
            except ValueError:
215
                pass
216

  
217
        get_response().breadcrumb.append(('import', _('Import')))
218
        html_top('api-access', title=import_title)
219
        r = TemplateIO(html=True)
220
        r += htmltext('<h2>%s</h2>') % import_title
221
        r += htmltext('<p>%s</p>') % _(
222
            'You can install a new API access by uploading a file.')
223
        r += form.render()
224
        return r.getvalue()
225

  
226
    def import_submit(self, form):
227
        fp = form.get_widget('file').parse().fp
228

  
229
        error = False
230
        try:
231
            api_access = ApiAccess.import_from_xml(fp)
232
            get_session().message = (
233
                'info', _('This API access has been successfully imported.'))
234
        except ValueError:
235
            error = True
236

  
237
        if error:
238
            form.set_error('file', _('Invalid File'))
239
            raise ValueError()
240

  
241
        # check access_id unicity
242
        for obj in ApiAccess.select():
243
            if obj.access_id == api_access.access_id:
244
                api_access.access_id = None  # a new one will be set in .store()
245
                break
246
        api_access.store()
247
        return redirect('%s/' % api_access.id)
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 quixote import get_publisher
18

  
19
from .qommon.misc import simplify
20
from .qommon.xml_storage import XmlStorableObject
21

  
22

  
23
class ApiAccess(XmlStorableObject):
24
    _names = 'apiaccess'
25
    xml_root_node = 'apiaccess'
26
    name = None
27
    access_id = None
28
    access_key = None
29
    description = None
30

  
31
    # declarations for serialization
32
    XML_NODES = [
33
        ('name', 'str'),
34
        ('description', 'str'),
35
        ('access_id', 'str'),
36
        ('access_key', 'str'),
37
    ]
38

  
39
    def __init__(self, name=None):
40
        XmlStorableObject.__init__(self)
41
        self.name = name
42

  
43
    def get_admin_url(self):
44
        base_url = get_publisher().get_backoffice_url()
45
        return '%s/settings/api-access/%s/' % (base_url, self.id)
46

  
47
    def store(self, comment=None):
48
        assert not self.is_readonly()
49
        if self.access_id is None:
50
            # set access_id if it's not yet there
51
            self.access_id = self.get_new_access_id()
52
        super(ApiAccess, self).store()
53
        if get_publisher().snapshot_class:
54
            get_publisher().snapshot_class.snap(instance=self, comment=comment)
55

  
56
    def get_new_access_id(self):
57
        new_access_id = simplify(self.name, space='_')
58
        base_new_access_id = new_access_id
59
        suffix_no = 0
60
        while True:
61
            for obj in self.select():
62
                if obj.access_id == new_access_id:
63
                    break
64
            else:
65
                break
66
            if obj.id == self.id:
67
                break
68
            suffix_no += 1
69
            new_access_id = '%s_%s' % (base_new_access_id, suffix_no)
70
        return new_access_id
wcs/backoffice/snapshots.py
136 136
    @property
137 137
    def view(self):
138 138
        from wcs.blocks import BlockDef
139 139
        from wcs.carddef import CardDef
140 140
        from wcs.data_sources import NamedDataSource
141 141
        from wcs.formdef import FormDef
142 142
        from wcs.workflows import Workflow
143 143
        from wcs.wscalls import NamedWsCall
144
        from wcs.api_access import ApiAccess
144 145
        klass = self.snapshot.get_object_class()
145 146
        if klass is BlockDef:
146 147
            from wcs.admin.blocks import BlockDirectory
147 148
            return BlockDirectory(section='forms', objectdef=self.snapshot.instance)
148 149
        if klass is FormDef:
149 150
            from wcs.admin.forms import FormDefPage
150 151
            return FormDefPage(component='view', instance=self.snapshot.instance)
151 152
        if klass is CardDef:
......
158 159
        if klass is NamedDataSource:
159 160
            from wcs.admin.data_sources import NamedDataSourcePage
160 161
            return NamedDataSourcePage(component='view',
161 162
                    instance=self.snapshot.instance)
162 163
        if klass is NamedWsCall:
163 164
            from wcs.admin.wscalls import NamedWsCallPage
164 165
            return NamedWsCallPage(component='view',
165 166
                    instance=self.snapshot.instance)
167
        if klass is ApiAccess:
168
            from wcs.admin.api_access import ApiAccessPage
169
            return ApiAccessPage(component='view',
170
                    instance=self.snapshot.instance)
wcs/snapshots.py
69 69

  
70 70
    def get_object_class(self):
71 71
        from wcs.blocks import BlockDef
72 72
        from wcs.carddef import CardDef
73 73
        from wcs.data_sources import NamedDataSource
74 74
        from wcs.formdef import FormDef
75 75
        from wcs.workflows import Workflow
76 76
        from wcs.wscalls import NamedWsCall
77
        for klass in (BlockDef, CardDef, NamedDataSource, FormDef, Workflow, NamedWsCall):
77
        from wcs.api_access import ApiAccess
78
        for klass in (BlockDef, CardDef, NamedDataSource, FormDef, Workflow, NamedWsCall, ApiAccess):
78 79
            if klass.xml_root_node == self.object_type:
79 80
                return klass
80 81
        raise KeyError('no class for object type: %s' % self.object_type)
81 82

  
82 83
    @property
83 84
    def instance(self):
84 85
        if self._instance is None:
85 86
            tree = ET.fromstring(self.serialization)
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
{% if not api_access.is_readonly %}
7
<span class="actions">
8
  <a href="edit">{% trans "Edit" %}</a>
9
</span>
10
{% endif %}
11
</div>
12

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

  
17
<div class="bo-block">
18
  <h3>{% trans "Parameters" %}</h3>
19
  <ul>
20
    <li>{% trans "Access identifier:" %} {{ api_access.access_id }}</li>
21
    <li>{% trans "Access key:" %} {{ api_access.access_key }}</li>
22
  </ul>
23
</div>
24
{% 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="import">{% trans "Import" %}</a>
8
<a rel="popup" href="new">{% trans "New API access" %}</a>
9
{% endblock %}
10

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