Revision 8cb96389
b/docs/astakos-api-guide.rst | ||
---|---|---|
310 | 310 |
Authenticate |
311 | 311 |
^^^^^^^^^^^^ |
312 | 312 |
|
313 |
Fallback call which receives the user token or the user uuid/token and returns
|
|
314 |
back the token as well as information about the token holder and the services
|
|
315 |
he/she can access. |
|
313 |
Fallback call which receives the user token or the user uuid/token pair and
|
|
314 |
returns back the token as well as information about the token holder and the
|
|
315 |
services he/she can access.
|
|
316 | 316 |
|
317 | 317 |
========================================= ========= ================== |
318 | 318 |
Uri Method Description |
319 | 319 |
========================================= ========= ================== |
320 |
``/astakos/api/tokens/`` POST Checks whether the provided token is valid and conforms with the provided uuid (if present) and returns back information about the user
|
|
320 |
``/identity/v2.0/tokens/`` POST Checks whether the provided token is valid and conforms with the provided uuid (if present) and returns back information about the user
|
|
321 | 321 |
========================================= ========= ================== |
322 | 322 |
|
323 | 323 |
The input should be json formatted. |
... | ... | |
328 | 328 |
|
329 | 329 |
{ |
330 | 330 |
"auth":{ |
331 |
"passwordCredentials":{ |
|
332 |
"username":"c18088be-16b1-4263-8180-043c54e22903", |
|
333 |
"password":"CDEe2k0T/HdiJWBMMbHyOA==" |
|
331 |
"token":{ |
|
332 |
"id":"CDEe2k0T/HdiJWBMMbHyOA==" |
|
334 | 333 |
}, |
335 | 334 |
"tenantName":"c18088be-16b1-4263-8180-043c54e22903" |
336 | 335 |
} |
... | ... | |
342 | 341 |
|
343 | 342 |
{ |
344 | 343 |
"auth":{ |
345 |
"token":{ |
|
346 |
"id":"CDEe2k0T/HdiJWBMMbHyOA==" |
|
344 |
"passwordCredentials":{ |
|
345 |
"username":"c18088be-16b1-4263-8180-043c54e22903", |
|
346 |
"password":"CDEe2k0T/HdiJWBMMbHyOA==" |
|
347 | 347 |
}, |
348 | 348 |
"tenantName":"c18088be-16b1-4263-8180-043c54e22903" |
349 | 349 |
} |
... | ... | |
359 | 359 |
|
360 | 360 |
:: |
361 | 361 |
|
362 |
{'serviceCatalog': [ |
|
363 |
{'endpoints': [{'SNF:uiURL': 'https://node1.example.com/ui/', |
|
364 |
'adminURL': 'https://node1.example.com/v1', |
|
365 |
'internalUrl': 'https://node1.example.com/v1', |
|
366 |
'publicURL': 'https://node1.example.com/v1', |
|
367 |
'region': 'cyclades'}], |
|
368 |
'name': 'cyclades', |
|
369 |
'type': 'compute'}, |
|
370 |
{'endpoints': [{'SNF:uiURL': 'https://node2.example.com/ui/', |
|
371 |
'adminURL': 'https://node2.example.com/v1', |
|
372 |
'internalUrl': 'https://node2.example.com/v1', |
|
373 |
'publicURL': 'https://node2.example.com/v1', |
|
374 |
'region': 'pithos'}], |
|
375 |
'name': 'pithos', |
|
376 |
'type': 'storage'}], |
|
377 |
'token': {'expires': '2013-06-19T15:23:59.975572+00:00', |
|
378 |
'id': 'CDEe2k0T/HdiJWBMMbHyOA==', |
|
379 |
'tenant': {'id': 'c18088be-16b1-4263-8180-043c54e22903', |
|
380 |
'name': 'Firstname Lastname'}}, |
|
381 |
'user': {'id': 'c18088be-16b1-4263-8180-043c54e22903', |
|
382 |
'name': 'Firstname Lastname', |
|
383 |
'roles': [{'id': 1, 'name': 'default'}], |
|
384 |
'roles_links': []}} |
|
385 |
|
|
362 |
{"access": { |
|
363 |
"serviceCatalog": [ |
|
364 |
{"SNF:uiURL": "https://node2.example.com/ui/", |
|
365 |
"endpoints": [{ |
|
366 |
"publicURL": "https://object-store.example.synnefo.org/pithos/public/v2.0", |
|
367 |
"versionId": "v2.0"}], |
|
368 |
"endpoints_links": [], |
|
369 |
"name": "pithos_public", |
|
370 |
"type": "public"}, |
|
371 |
{"SNF:uiURL": "https://node2.example.com/ui/", |
|
372 |
"endpoints": [{ |
|
373 |
"publicURL": "https://object-store.example.synnefo.org/pithos/object-store/v1", |
|
374 |
"versionId": "v1"}], |
|
375 |
"endpoints_links": [], |
|
376 |
"name": "pithos_object-store", |
|
377 |
"type": "object-store"}, |
|
378 |
{"SNF:uiURL": "https://node2.example.com/ui/", |
|
379 |
"endpoints": [{ |
|
380 |
"publicURL": "https://object-store.example.synnefo.org/pithos/ui", |
|
381 |
"versionId": ""}], |
|
382 |
"endpoints_links": [], |
|
383 |
"name": "pithos_ui", |
|
384 |
"type": "pithos_ui"}, |
|
385 |
{"SNF:uiURL": "http://localhost:8080", |
|
386 |
"endpoints": [{ |
|
387 |
"publicURL": "https://accounts.example.synnefo.org/ui/v1.0", |
|
388 |
"versionId": "v1.0"}], |
|
389 |
"endpoints_links": [], |
|
390 |
"name": "astakos_ui", |
|
391 |
"type": "astakos_ui"}, |
|
392 |
{"SNF:uiURL": "http://localhost:8080", |
|
393 |
"endpoints": [{ |
|
394 |
"publicURL": "https://accounts.example.synnefo.org/account/v1.0", |
|
395 |
"versionId": "v1.0"}], |
|
396 |
"endpoints_links": [], |
|
397 |
"name": "astakos_account", |
|
398 |
"type": "account"}, |
|
399 |
{"SNF:uiURL": "http://localhost:8080", |
|
400 |
"endpoints": [{ |
|
401 |
"publicURL": "https://accounts.example.synnefo.org/identity/v2.0", |
|
402 |
"versionId": "v2.0"}], |
|
403 |
"endpoints_links": [], |
|
404 |
"name": "astakos_keystone", |
|
405 |
"type": "identity"}], |
|
406 |
"token": { |
|
407 |
"expires": "2013-06-19T15:23:59.975572+00:00", |
|
408 |
"id": "CDEe2k0T/HdiJWBMMbHyOA==", |
|
409 |
"tenant": {"id": "c18088be-16b1-4263-8180-043c54e22903", |
|
410 |
"name": "Firstname Lastname"}}, |
|
411 |
"user": { |
|
412 |
"id": "c18088be-16b1-4263-8180-043c54e22903", |
|
413 |
"name": "Firstname Lastname", |
|
414 |
"roles": [{"id": 1, "name": "default"}, |
|
415 |
"roles_links": []}}} |
|
386 | 416 |
|
387 | 417 |
Example xml response: |
388 | 418 |
|
389 | 419 |
:: |
390 | 420 |
|
391 | 421 |
<?xml version="1.0" encoding="UTF-8"?> |
422 |
|
|
392 | 423 |
<access xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
393 | 424 |
xmlns="http://docs.openstack.org/identity/api/v2.0"> |
394 | 425 |
<token id="CDEe2k0T/HdiJWBMMbHyOA==" expires="2013-06-19T15:23:59.975572+00:00"> |
... | ... | |
400 | 431 |
</roles> |
401 | 432 |
</user> |
402 | 433 |
<serviceCatalog> |
403 |
<service type="None" name="cyclades"> |
|
404 |
<endpoint region="cyclades" |
|
405 |
publicURL="https://node1.example.com/v1" |
|
406 |
adminURL="https://node1.example.com/v1" |
|
407 |
internalURL="https://node1.example.com/v1" |
|
408 |
SNF:uiURL="https://node1.example.com/ui/"> |
|
409 |
</endpoint> |
|
434 |
<service type="public" name="pithos_public" SNF:uiURL=""> |
|
435 |
<endpoint |
|
436 |
versionId="v2.0" |
|
437 |
publicURL="https://object-store.example.synnefo.org/pithos/public/v2.0" |
|
438 |
</service> |
|
439 |
<service type="object-store" name="pithos_object-store" SNF:uiURL=""> |
|
440 |
<endpoint |
|
441 |
versionId="v1" |
|
442 |
publicURL="https://object-store.example.synnefo.org/pithos/object-store/v1" |
|
443 |
</service> |
|
444 |
<service type="pithos_ui" name="pithos_ui" SNF:uiURL=""> |
|
445 |
<endpoint |
|
446 |
versionId="" |
|
447 |
publicURL="https://object-store.example.synnefo.org/pithos/ui" |
|
448 |
</service> |
|
449 |
<service type="astakos_ui" name="astakos_ui" SNF:uiURL=""> |
|
450 |
<endpoint |
|
451 |
versionId="v1.0" |
|
452 |
publicURL="https://accounts.example.synnefo.org/ui/v1.0" |
|
453 |
</service> |
|
454 |
<service type="account" name="astakos_account" SNF:uiURL=""> |
|
455 |
<endpoint |
|
456 |
versionId="v1.0" |
|
457 |
publicURL="https://accounts.example.synnefo.org/account/v1.0" |
|
410 | 458 |
</service> |
411 |
<service type="None" name="pithos"> |
|
412 |
<endpoint |
|
413 |
region="pithos" |
|
414 |
publicURL="https://node2.example.com/v1" |
|
415 |
adminURL="https://node2.example.com/v1" |
|
416 |
internalURL="https://node2.example.com/v1" |
|
417 |
SNF:uiURL="https://node2.example.com/ui/"> |
|
418 |
</endpoint> |
|
459 |
<service type="identity" name="astakos_keystone" SNF:uiURL=""> |
|
460 |
<endpoint |
|
461 |
versionId="v2.0" |
|
462 |
publicURL="https://accounts.example.synnefo.org/identity/v2.0" |
|
419 | 463 |
</service> |
420 | 464 |
</serviceCatalog> |
421 | 465 |
</access> |
b/snf-astakos-app/astakos/api/tokens.py | ||
---|---|---|
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 | 34 |
from urlparse import urlunsplit, urlsplit |
35 |
from collections import defaultdict |
|
35 | 36 |
|
36 | 37 |
from django.http import urlencode |
37 | 38 |
from django.views.decorators.csrf import csrf_exempt |
... | ... | |
114 | 115 |
if user.uuid != uuid: |
115 | 116 |
raise faults.Unauthorized('Invalid credentials') |
116 | 117 |
|
117 |
access = {} |
|
118 |
access['token'] = {'id': user.auth_token, |
|
119 |
'expires': utils.isoformat(user.auth_token_expires), |
|
120 |
'tenant': {'id': user.uuid, 'name': user.realname}} |
|
121 |
access['user'] = {'id': user.uuid, 'name': user.realname, |
|
122 |
'roles': list(user.groups.values('id', 'name')), |
|
123 |
'roles_links': []} |
|
124 |
access['serviceCatalog'] = [] |
|
125 |
append = access['serviceCatalog'].append |
|
126 |
for s in Service.objects.all().order_by('id'): |
|
127 |
append({'name': s.name, 'type': s.type, |
|
128 |
'endpoints': [{'adminURL': s.api_url, |
|
129 |
'publicURL': s.api_url, |
|
130 |
'internalURL': s.api_url, |
|
131 |
'SNF:uiURL': s.url, |
|
132 |
'region': s.name}]}) |
|
118 |
d = defaultdict(dict) |
|
119 |
d["access"]["token"] = { |
|
120 |
"id": user.auth_token, |
|
121 |
"expires": utils.isoformat(user.auth_token_expires), |
|
122 |
"tenant": {"id": user.uuid, "name": user.realname}} |
|
123 |
d["access"]["user"] = { |
|
124 |
"id": user.uuid, 'name': user.realname, |
|
125 |
"roles": list(user.groups.values("id", "name")), |
|
126 |
"roles_links": []} |
|
127 |
d["access"]["serviceCatalog"] = [] |
|
128 |
append = d["access"]["serviceCatalog"].append |
|
129 |
for s in Service.objects.all().order_by("id"): |
|
130 |
endpoints = [] |
|
131 |
for l in [e.data.values('key', 'value') for e in s.endpoints.all()]: |
|
132 |
endpoint = dict((d['key'], d['value']) for d in l) |
|
133 |
endpoints.append(endpoint) |
|
134 |
append({"name": s.name, |
|
135 |
"type": s.type, |
|
136 |
"SNF:uiURL": s.component.url, |
|
137 |
"endpoints": endpoints, |
|
138 |
"endpoints_links": []}) |
|
133 | 139 |
|
134 | 140 |
if request.serialization == 'xml': |
135 |
return xml_response({'access': access}, 'api/access.xml')
|
|
141 |
return xml_response({'d': d}, 'api/access.xml')
|
|
136 | 142 |
else: |
137 |
return json_response(access) |
|
143 |
return json_response(d) |
b/snf-astakos-app/astakos/im/templates/api/access.xml | ||
---|---|---|
2 | 2 |
{% load filters %} |
3 | 3 |
<access xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
4 | 4 |
xmlns="http://docs.openstack.org/identity/api/v2.0"> |
5 |
<token id="{{access.token.id}}" expires="{{access.token.expires}}">
|
|
6 |
<tenant id="{{access.token.tenant.id}}" name="{{access.token.tenant.name}}" />
|
|
5 |
<token id="{{d.access.token.id}}" expires="{{d.access.token.expires}}">
|
|
6 |
<tenant id="{{d.access.token.tenant.id}}" name="{{d.access.token.tenant.name}}" />
|
|
7 | 7 |
</token> |
8 |
<user id="{{access.user.id}}" name="{{access.user.name}}">
|
|
8 |
<user id="{{d.access.user.id}}" name="{{d.access.user.name}}">
|
|
9 | 9 |
<roles> |
10 |
{% for r in access.user.roles %} |
|
10 |
{% for r in d.access.user.roles %}
|
|
11 | 11 |
<role id="{{r.id}}" name="{{r.name}}"/> |
12 | 12 |
{% endfor %} |
13 | 13 |
</roles> |
14 | 14 |
</user> |
15 | 15 |
<serviceCatalog> |
16 |
{% for s in access.serviceCatalog %} |
|
17 |
<service type="{{s.type}}" name="{{s.name}}"> |
|
16 |
{% for s in d.access.serviceCatalog %}
|
|
17 |
<service type="{{s.type}}" name="{{s.name}}" SNF:uiURL="{{s.component.url}}">
|
|
18 | 18 |
{% for e in s.endpoints %} |
19 | 19 |
<endpoint |
20 |
region="{{e.region}}" |
|
21 |
publicURL="{{e.publicURL}}" |
|
22 |
adminURL="{{e.adminURL}}" |
|
23 |
internalURL="{{e.internalURL}}" |
|
24 |
SNF:uiURL="{{e|lookup:'SNF:uiURL'}}"/> |
|
20 |
{% for k, v in e.items %} |
|
21 |
{{k}}="{{v}}" |
|
22 |
{% endfor %} |
|
25 | 23 |
{% endfor %} |
26 | 24 |
</service> |
27 | 25 |
{% endfor %} |
b/snf-astakos-app/astakos/im/tests/api.py | ||
---|---|---|
34 | 34 |
from astakos.im.tests.common import * |
35 | 35 |
|
36 | 36 |
from django.test import TestCase |
37 |
from django.core.urlresolvers import reverse |
|
38 |
|
|
37 | 39 |
|
38 | 40 |
from urllib import quote |
39 | 41 |
from urlparse import urlparse, parse_qs |
40 |
from xml.dom import minidom |
|
42 |
#from xml.dom import minidom
|
|
41 | 43 |
|
42 | 44 |
import json |
43 | 45 |
|
... | ... | |
391 | 393 |
backend.activate_user(self.user2) |
392 | 394 |
assert self.user2.is_active is True |
393 | 395 |
|
394 |
Service(name='service1', url='http://localhost/service1', |
|
395 |
api_url='http://localhost/api/service1', |
|
396 |
type='service1').save() |
|
397 |
Service(name='service2', url='http://localhost/service2', |
|
398 |
api_url='http://localhost/api/service2', |
|
399 |
type='service2').save() |
|
400 |
Service(name='service3', url='http://localhost/service3', |
|
401 |
api_url='http://localhost/api/service3', |
|
402 |
type='service3').save() |
|
396 |
c1 = Component(name='component1', url='http://localhost/component1') |
|
397 |
c1.save() |
|
398 |
s1 = Service(component=c1, type='type1', name='service1') |
|
399 |
s1.save() |
|
400 |
e1 = Endpoint(service=s1) |
|
401 |
e1.save() |
|
402 |
e1.data.create(key='versionId', value='v1.0') |
|
403 |
e1.data.create(key='publicURL', value='http://localhost:8000/s1/v1.0') |
|
404 |
|
|
405 |
s2 = Service(component=c1, type='type2', name='service2') |
|
406 |
s2.save() |
|
407 |
e2 = Endpoint(service=s2) |
|
408 |
e2.save() |
|
409 |
e2.data.create(key='versionId', value='v1.0') |
|
410 |
e2.data.create(key='publicURL', value='http://localhost:8000/s2/v1.0') |
|
411 |
|
|
412 |
c2 = Component(name='component2', url='http://localhost/component2') |
|
413 |
c2.save() |
|
414 |
s3 = Service(component=c2, type='type3', name='service3') |
|
415 |
s3.save() |
|
416 |
e3 = Endpoint(service=s3) |
|
417 |
e3.save() |
|
418 |
e3.data.create(key='versionId', value='v2.0') |
|
419 |
e3.data.create(key='publicURL', value='http://localhost:8000/s3/v2.0') |
|
403 | 420 |
|
404 | 421 |
def test_authenticate(self): |
405 | 422 |
client = Client() |
406 | 423 |
|
407 | 424 |
# Check not allowed method |
408 |
url = '/astakos/api/tokens'
|
|
425 |
url = reverse('astakos.api.tokens.authenticate')
|
|
409 | 426 |
r = client.get(url, post_data={}) |
410 | 427 |
self.assertEqual(r.status_code, 400) |
411 | 428 |
|
412 | 429 |
# Malformed request |
413 |
url = '/astakos/api/tokens'
|
|
430 |
url = reverse('astakos.api.tokens.authenticate')
|
|
414 | 431 |
r = client.post(url, post_data={}) |
415 | 432 |
self.assertEqual(r.status_code, 400) |
416 | 433 |
|
417 | 434 |
# Check unsupported xml input |
418 |
url = '/astakos/api/tokens'
|
|
435 |
url = reverse('astakos.api.tokens.authenticate')
|
|
419 | 436 |
post_data = """ |
420 | 437 |
<?xml version="1.0" encoding="UTF-8"?> |
421 | 438 |
<auth xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
... | ... | |
431 | 448 |
"Unsupported Content-type: 'application/xml'") |
432 | 449 |
|
433 | 450 |
# Check malformed request: missing password |
434 |
url = '/astakos/api/tokens'
|
|
451 |
url = reverse('astakos.api.tokens.authenticate')
|
|
435 | 452 |
post_data = """{"auth":{"passwordCredentials":{"username":"%s"}, |
436 | 453 |
"tenantName":"%s"}}""" % ( |
437 | 454 |
self.user1.uuid, self.user1.uuid) |
... | ... | |
442 | 459 |
'Malformed request') |
443 | 460 |
|
444 | 461 |
# Check malformed request: missing username |
445 |
url = '/astakos/api/tokens'
|
|
462 |
url = reverse('astakos.api.tokens.authenticate')
|
|
446 | 463 |
post_data = """{"auth":{"passwordCredentials":{"password":"%s"}, |
447 | 464 |
"tenantName":"%s"}}""" % ( |
448 | 465 |
self.user1.auth_token, self.user1.uuid) |
... | ... | |
453 | 470 |
'Malformed request') |
454 | 471 |
|
455 | 472 |
# Check invalid pass |
456 |
url = '/astakos/api/tokens'
|
|
473 |
url = reverse('astakos.api.tokens.authenticate')
|
|
457 | 474 |
post_data = """{"auth":{"passwordCredentials":{"username":"%s", |
458 | 475 |
"password":"%s"}, |
459 | 476 |
"tenantName":"%s"}}""" % ( |
... | ... | |
465 | 482 |
'Invalid token') |
466 | 483 |
|
467 | 484 |
# Check inconsistent pass |
468 |
url = '/astakos/api/tokens'
|
|
485 |
url = reverse('astakos.api.tokens.authenticate')
|
|
469 | 486 |
post_data = """{"auth":{"passwordCredentials":{"username":"%s", |
470 | 487 |
"password":"%s"}, |
471 | 488 |
"tenantName":"%s"}}""" % ( |
... | ... | |
477 | 494 |
'Invalid credentials') |
478 | 495 |
|
479 | 496 |
# Check invalid json data |
480 |
url = '/astakos/api/tokens'
|
|
497 |
url = reverse('astakos.api.tokens.authenticate')
|
|
481 | 498 |
r = client.post(url, "not json", content_type='application/json') |
482 | 499 |
self.assertEqual(r.status_code, 400) |
483 | 500 |
body = json.loads(r.content) |
484 | 501 |
self.assertEqual(body['badRequest']['message'], 'Invalid JSON data') |
485 | 502 |
|
486 | 503 |
# Check auth with token |
487 |
url = '/astakos/api/tokens'
|
|
504 |
url = reverse('astakos.api.tokens.authenticate')
|
|
488 | 505 |
post_data = """{"auth":{"token": {"id":"%s"}, |
489 | 506 |
"tenantName":"%s"}}""" % ( |
490 | 507 |
self.user1.auth_token, self.user1.uuid) |
... | ... | |
497 | 514 |
self.fail(e) |
498 | 515 |
|
499 | 516 |
# Check successful json response |
500 |
url = '/astakos/api/tokens'
|
|
517 |
url = reverse('astakos.api.tokens.authenticate')
|
|
501 | 518 |
post_data = """{"auth":{"passwordCredentials":{"username":"%s", |
502 | 519 |
"password":"%s"}, |
503 | 520 |
"tenantName":"%s"}}""" % ( |
... | ... | |
511 | 528 |
self.fail(e) |
512 | 529 |
|
513 | 530 |
try: |
514 |
token = body['token']['id'] |
|
515 |
user = body['user']['id'] |
|
516 |
service_catalog = body['serviceCatalog'] |
|
531 |
token = body['access']['token']['id']
|
|
532 |
user = body['access']['user']['id']
|
|
533 |
service_catalog = body['access']['serviceCatalog']
|
|
517 | 534 |
except KeyError: |
518 | 535 |
self.fail('Invalid response') |
519 | 536 |
|
... | ... | |
522 | 539 |
self.assertEqual(len(service_catalog), 3) |
523 | 540 |
|
524 | 541 |
# Check successful xml response |
525 |
url = '/astakos/api/tokens'
|
|
542 |
url = reverse('astakos.api.tokens.authenticate')
|
|
526 | 543 |
headers = {'HTTP_ACCEPT': 'application/xml'} |
527 | 544 |
post_data = """{"auth":{"passwordCredentials":{"username":"%s", |
528 | 545 |
"password":"%s"}, |
Also available in: Unified diff