api: move user related API to /api/user/ (#8283)

This commit is contained in:
Frédéric Péters 2015-09-19 10:11:38 +02:00
parent 21cfacc69a
commit 6de84723ea
5 changed files with 159 additions and 122 deletions

View File

@ -24,18 +24,23 @@ de l'utilisateur, modification, etc.).
<section id="pull">
<title>Mode pull</title>
<p>
Ces accès doivent se faire en passant les informations d'identification
appropriées dans la <em>query string</em>.
</p>
<section>
<title>Profil</title>
<p>
Les informations associées à un utilisateur sont accessibles à l'URL
<code>/user</code>, elles reprennent son nom (<code>user_display_name</code>),
<code>/api/user/</code>, elles reprennent son nom (<code>user_display_name</code>),
son adresse électronique (<code>user_email</code>) ainsi que ses éventuelles
autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
à l'interface d'administration (<code>user_admin_access</code>).
</p>
<screen>
<output style="prompt">$ </output><input>curl -H "Accept: application/json" https://www.example.net/user</input>
<output style="prompt">$ </output><input>curl https://www.example.net/api/user/</input>
<output>{
"user_display_name": "Fred",
"user_email": "fred@example.net",
@ -44,6 +49,12 @@ autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
}
</output></screen>
<note>
<p>Note de compatibilité : cette information est également disponible à
l'adresse <code>/user</code>.
</p>
</note>
</section>
<section id="forms">
@ -51,11 +62,11 @@ autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
<p>
La liste des formulaires transmis par un utilisateur est accessible à l'URL
<code>/myspace/forms</code>, elle reprend un ensemble minimal
<code>/api/user/forms</code>, elle reprend un ensemble minimal
d'informations concernant chacun de ceux-ci.
</p>
<screen>
<output style="prompt">$ </output><input>curl -H "Accept: application/json" https://www.example.net/myspace/forms</input>
<output style="prompt">$ </output><input>curl https://www.example.net/api/user/forms</input>
<output>[
{
"category_id": "1",
@ -116,17 +127,23 @@ autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
}
]</output></screen>
<note>
<p>Note de compatibilité : cette information est également disponible à
l'adresse <code>/myspace/forms</code>.
</p>
</note>
</section>
<section>
<title>Brouillons</title>
<p>
La liste des brouillons de l'utilisateur est accessible à l'adresse
<code>/myspace/drafts</code>.
<code>/api/user/drafts</code>.
</p>
<screen>
<output style="prompt">$ </output><input>curl -H "Accept: application/json" https://www.example.net/myspace/drafts</input>
<output style="prompt">$ </output><input>curl https://www.example.net/myspace/drafts</input>
<output>[
{
"datetime": "2014-07-21 10:15:21",
@ -136,6 +153,12 @@ autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
}
]</output></screen>
<note>
<p>Note de compatibilité : cette information est également disponible à
l'adresse <code>/myspace/drafts</code>.
</p>
</note>
</section>
</section>

View File

@ -68,27 +68,27 @@ def test_user_page_redirect():
assert output.headers.get('location') == 'http://example.net/myspace/'
def test_user_page_error_when_json_and_no_user():
output = get_app(pub).get('/user?format=json', status=403)
output = get_app(pub).get('/api/user/?format=json', status=403)
assert output.json['err_desc'] == 'no user specified'
def test_get_user_from_api_query_string_error_missing_orig():
output = get_app(pub).get('/user?format=json&signature=xxx', status=403)
output = get_app(pub).get('/api/user/?format=json&signature=xxx', status=403)
assert output.json['err_desc'] == 'missing/multiple orig field'
def test_get_user_from_api_query_string_error_invalid_orig():
output = get_app(pub).get('/user?format=json&orig=coin&signature=xxx', status=403)
output = get_app(pub).get('/api/user/?format=json&orig=coin&signature=xxx', status=403)
assert output.json['err_desc'] == 'invalid orig'
def test_get_user_from_api_query_string_error_missing_algo():
output = get_app(pub).get('/user?format=json&orig=coucou&signature=xxx', status=403)
output = get_app(pub).get('/api/user/?format=json&orig=coucou&signature=xxx', status=403)
assert output.json['err_desc'] == 'missing/multiple algo field'
def test_get_user_from_api_query_string_error_invalid_algo():
output = get_app(pub).get('/user?format=json&orig=coucou&signature=xxx&algo=coin', status=403)
output = get_app(pub).get('/api/user/?format=json&orig=coucou&signature=xxx&algo=coin', status=403)
assert output.json['err_desc'] == 'invalid algo'
def test_get_user_from_api_query_string_error_invalid_signature():
output = get_app(pub).get('/user?format=json&orig=coucou&signature=xxx&algo=sha1', status=403)
output = get_app(pub).get('/api/user/?format=json&orig=coucou&signature=xxx&algo=sha1', status=403)
assert output.json['err_desc'] == 'invalid signature'
def test_get_user_from_api_query_string_error_missing_timestamp():
@ -97,7 +97,7 @@ def test_get_user_from_api_query_string_error_missing_timestamp():
hmac.new('1234',
'format=json&orig=coucou&algo=sha1',
hashlib.sha1).digest()))
output = get_app(pub).get('/user?format=json&orig=coucou&algo=sha1&signature=%s' % signature, status=403)
output = get_app(pub).get('/api/user/?format=json&orig=coucou&algo=sha1&signature=%s' % signature, status=403)
assert output.json['err_desc'] == 'missing/multiple timestamp field'
def test_get_user_from_api_query_string_error_missing_email():
@ -108,7 +108,7 @@ def test_get_user_from_api_query_string_error_missing_email():
hmac.new('1234',
query,
hashlib.sha1).digest()))
output = get_app(pub).get('/user?%s&signature=%s' % (query, signature), status=403)
output = get_app(pub).get('/api/user/?%s&signature=%s' % (query, signature), status=403)
assert output.json['err_desc'] == 'no user specified'
def test_get_user_from_api_query_string_error_unknown_nameid():
@ -119,7 +119,7 @@ def test_get_user_from_api_query_string_error_unknown_nameid():
hmac.new('1234',
query,
hashlib.sha1).digest()))
output = get_app(pub).get('/user?%s&signature=%s' % (query, signature), status=403)
output = get_app(pub).get('/api/user/?%s&signature=%s' % (query, signature), status=403)
assert output.json['err_desc'] == 'unknown NameID'
def test_get_user_from_api_query_string_error_missing_email_valid_endpoint():
@ -159,7 +159,7 @@ def test_get_user_from_api_query_string_error_success_sha1(local_user):
hmac.new('1234',
query,
hashlib.sha1).digest()))
output = get_app(pub).get('/user?%s&signature=%s' % (query, signature))
output = get_app(pub).get('/api/user/?%s&signature=%s' % (query, signature))
assert output.json['user_display_name'] == u'Jean Darmette'
def test_get_user_from_api_query_string_error_invalid_signature_algo_mismatch(local_user):
@ -170,7 +170,7 @@ def test_get_user_from_api_query_string_error_invalid_signature_algo_mismatch(lo
hmac.new('1234',
query,
hashlib.sha1).digest()))
output = get_app(pub).get('/user?%s&signature=%s' % (query, signature), status=403)
output = get_app(pub).get('/api/user/?%s&signature=%s' % (query, signature), status=403)
assert output.json['err_desc'] == 'invalid signature'
def test_get_user_from_api_query_string_error_success_sha256(local_user):
@ -181,12 +181,12 @@ def test_get_user_from_api_query_string_error_success_sha256(local_user):
hmac.new('1234',
query,
hashlib.sha256).digest()))
output = get_app(pub).get('/user?%s&signature=%s' % (query, signature))
output = get_app(pub).get('/api/user/?%s&signature=%s' % (query, signature))
assert output.json['user_display_name'] == u'Jean Darmette'
def test_sign_url(local_user):
signed_url = sign_url(
'http://example.net/user?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
'http://example.net/api/user/?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
'1234'
)
url = signed_url[len('http://example.net'):]
@ -194,12 +194,21 @@ def test_sign_url(local_user):
assert output.json['user_display_name'] == u'Jean Darmette'
signed_url = sign_url(
'http://example.net/user?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
'http://example.net/api/user/?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
'12345'
)
url = signed_url[len('http://example.net'):]
output = get_app(pub).get(url, status=403)
def test_get_user_compat_endpoint(local_user):
signed_url = sign_url(
'http://example.net/user?format=json&orig=coucou&email=%s' % urllib.quote(local_user.email),
'1234'
)
url = signed_url[len('http://example.net'):]
output = get_app(pub).get(url)
assert output.json['user_display_name'] == u'Jean Darmette'
def test_formdef_list():
FormDef.wipe()
formdef = FormDef()
@ -311,7 +320,7 @@ def test_formdata(local_user):
assert resp.json['fields']['file']['filename'] == 'test.txt'
assert resp.json['fields']['file']['content_type'] == 'text/plain'
def test_myspace_forms(local_user):
def test_user_forms(local_user):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'test'
@ -320,7 +329,7 @@ def test_myspace_forms(local_user):
fields.StringField(id='1', label='foobar2'),]
formdef.store()
resp = get_app(pub).get(sign_uri('/myspace/forms', user=local_user))
resp = get_app(pub).get(sign_uri('/api/user/forms', user=local_user))
assert len(resp.json) == 0
formdata = formdef.data_class()()
@ -330,11 +339,13 @@ def test_myspace_forms(local_user):
formdata.jump_status('new')
formdata.store()
resp = get_app(pub).get(sign_uri('/myspace/forms', user=local_user))
resp = get_app(pub).get(sign_uri('/api/user/forms', user=local_user))
resp2 = get_app(pub).get(sign_uri('/myspace/forms', user=local_user))
assert len(resp.json) == 1
assert resp.json[0]['form_status'] == 'New'
assert resp.json == resp2.json
def test_myspace_drafts(local_user):
def test_user_drafts(local_user):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'test'
@ -343,7 +354,7 @@ def test_myspace_drafts(local_user):
fields.StringField(id='1', label='foobar2'),]
formdef.store()
resp = get_app(pub).get(sign_uri('/myspace/drafts', user=local_user))
resp = get_app(pub).get(sign_uri('/api/user/drafts', user=local_user))
assert len(resp.json) == 0
formdata = formdef.data_class()()
@ -354,8 +365,10 @@ def test_myspace_drafts(local_user):
formdata.receipt_time = datetime.datetime(2015, 1, 1).timetuple()
formdata.store()
resp = get_app(pub).get(sign_uri('/myspace/drafts', user=local_user))
resp = get_app(pub).get(sign_uri('/api/user/drafts', user=local_user))
resp2 = get_app(pub).get(sign_uri('/myspace/drafts', user=local_user))
assert len(resp.json) == 1
assert resp.json == resp2.json
def test_api_list_formdata(local_user):
Role.wipe()

View File

@ -27,6 +27,7 @@ import sys
from quixote import get_request, get_publisher, get_response
from quixote.directory import Directory
from qommon import misc
from qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
UnknownNameIdAccessForbiddenError)
@ -297,13 +298,101 @@ class ApiCategoriesDirectory(RootDirectory):
return ApiCategoryDirectory(Category.get_by_urlname(component))
class ApiUserDirectory(Directory):
_q_exports = ['', 'forms', 'drafts']
def _q_index(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
raise AccessForbiddenError('no user specified')
user_info = user.get_substitution_variables(prefix='')
del user_info['user']
user_info['user_roles'] = []
for role_id in user.roles or []:
user_info['user_roles'].append(Role.get(role_id).name)
return json.dumps(user_info)
def get_user_forms(self, user):
formdefs = FormDef.select(lambda x: not x.is_disabled())
user_forms = []
for formdef in formdefs:
user_forms.extend(formdef.data_class().get_with_indexed_value(
'user_id', user.id))
try:
user_forms.extend(formdef.data_class().get_with_indexed_value(
'user_hash', user.hash))
except AttributeError:
pass
user_forms.sort(lambda x, y: cmp(x.receipt_time, y.receipt_time))
return user_forms
def drafts(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
raise AccessForbiddenError()
drafts = []
for form in self.get_user_forms(user):
if not form.is_draft():
continue
title = '%(name)s, draft saved on %(datetime)s' % {
'name': form.formdef.name,
'datetime': misc.localstrftime(form.receipt_time)
}
# !!! no trailing slash in the special draft case
url = form.get_url().rstrip('/')
d = {'title': title,
'name': form.formdef.name,
'url': url,
'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', form.receipt_time),
}
drafts.append(d)
return json.dumps(drafts)
def forms(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
raise AccessForbiddenError()
forms = []
for form in self.get_user_forms(user):
if form.is_draft():
continue
visible_status = form.get_visible_status(user=user)
# skip hidden forms
if not visible_status:
continue
name = form.formdef.name
id = form.get_display_id()
status = visible_status.name
title = _('%(name)s #%(id)s (%(status)s)') % {
'name': name,
'id': id,
'status': status
}
url = form.get_url()
d = {'title': title,
'name': form.formdef.name,
'url': url,
'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', form.receipt_time),
'status': status,
}
d.update(form.get_substitution_variables(minimal=True))
forms.append(d)
return json.dumps(forms)
class ApiDirectory(Directory):
_q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
'formdefs', 'categories']
'formdefs', 'categories', 'user']
forms = ApiFormsDirectory()
formdefs = ApiFormdefsDirectory()
categories = ApiCategoriesDirectory()
user = ApiUserDirectory()
def reverse_geocoding(self):
try:

View File

@ -14,97 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import json
from quixote import get_request, get_response, redirect
from quixote import get_request, redirect
import qommon.myspace
from qommon import misc
from qommon import errors
from api import get_user_from_api_query_string
from formdef import FormDef
class MyspaceDirectory(qommon.myspace.MyspaceDirectory):
_q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms']
def get_user_forms(self, user):
formdefs = FormDef.select(lambda x: not x.is_disabled())
user_forms = []
for formdef in formdefs:
user_forms.extend(formdef.data_class().get_with_indexed_value(
'user_id', user.id))
try:
user_forms.extend(formdef.data_class().get_with_indexed_value(
'user_hash', user.hash))
except AttributeError:
pass
user_forms.sort(lambda x, y: cmp(x.receipt_time, y.receipt_time))
return user_forms
def drafts(self):
if get_request().is_json():
return self.drafts_json()
from wcs.api import ApiUserDirectory
return ApiUserDirectory().drafts()
return redirect('.')
def drafts_json(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
return errors.AccessForbiddenError()
drafts = []
for form in self.get_user_forms(user):
if not form.is_draft():
continue
title = '%(name)s, draft saved on %(datetime)s' % {
'name': form.formdef.name,
'datetime': misc.localstrftime(form.receipt_time)
}
# !!! no trailing slash in the special draft case
url = form.get_url().rstrip('/')
d = {'title': title,
'name': form.formdef.name,
'url': url,
'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', form.receipt_time),
}
drafts.append(d)
return json.dumps(drafts)
def forms(self):
if get_request().is_json():
return self.forms_json()
from wcs.api import ApiUserDirectory
return ApiUserDirectory().forms()
return redirect('.')
def forms_json(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
return errors.AccessForbiddenError()
forms = []
for form in self.get_user_forms(user):
if form.is_draft():
continue
visible_status = form.get_visible_status(user=user)
# skip hidden forms
if not visible_status:
continue
name = form.formdef.name
id = form.get_display_id()
status = visible_status.name
title = _('%(name)s #%(id)s (%(status)s)') % {
'name': name,
'id': id,
'status': status
}
url = form.get_url()
d = {'title': title,
'name': form.formdef.name,
'url': url,
'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', form.receipt_time),
'status': status,
}
d.update(form.get_substitution_variables(minimal=True))
forms.append(d)
return json.dumps(forms)

View File

@ -233,22 +233,11 @@ class RootDirectory(Directory):
return self.saml.slo_sp()
def user(self):
# endpoint for backward compatibility, new code should call /api/user/
if get_request().is_json():
return self.user_json()
return self.api.user._q_index()
return redirect('myspace/')
def user_json(self):
get_response().set_content_type('application/json')
user = get_user_from_api_query_string() or get_request().user
if not user:
raise errors.AccessForbiddenError('no user specified')
user_info = user.get_substitution_variables(prefix='')
del user_info['user']
user_info['user_roles'] = []
for role_id in user.roles or []:
user_info['user_roles'].append(Role.get(role_id).name)
return json.dumps(user_info)
def roles(self):
# endpoint for backward compatibility, new code should call /api/roles
if not get_request().is_json():