From 908ededed96b54760188a010e6bd44b7325cf68c Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:11:47 -0500 Subject: Changes to also return role references as a part of user when get token call is made for a specific tenant. --- keystone/db/sqlalchemy/api.py | 5 +++++ keystone/logic/service.py | 8 +++++++- keystone/logic/types/auth.py | 10 +++++++-- keystone/logic/types/role.py | 12 +++++++++-- test/unit/test_common.py | 8 ++++++++ test/unit/test_token.py | 48 ++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 83 insertions(+), 8 deletions(-) diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py index 6676c101..cdd3e948 100644 --- a/keystone/db/sqlalchemy/api.py +++ b/keystone/db/sqlalchemy/api.py @@ -129,6 +129,11 @@ def role_ref_get_all_global_roles(user_id,session=None): if not session: session = get_session() return session.query(models.UserRoleAssociation).filter_by(user_id=user_id).filter("tenant_id is null").all() + +def role_ref_get_all_tenant_roles(user_id, tenant_id, session=None): + if not session: + session = get_session() + return session.query(models.UserRoleAssociation).filter_by(user_id=user_id).filter_by(tenant_id = tenant_id).all() def role_ref_get(id, session=None): if not session: diff --git a/keystone/logic/service.py b/keystone/logic/service.py index d503257c..86d66449 100644 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -838,7 +838,13 @@ class IdentityService(object): gs.append(auth.Group(dgroup.id)) user = auth.User(duser.id, dtoken.tenant_id, gs) """ - user = auth.User(duser.id, duser.tenant_id, None) + ts=[] + if dtoken.tenant_id: + droleRefs = db_api.role_ref_get_all_tenant_roles(duser.id, dtoken.tenant_id) + for droleRef in droleRefs: + ts.append(roles.RoleRef(droleRef.id, droleRef.role_id, + droleRef.tenant_id)) + user = auth.User(duser.id, duser.tenant_id, None, roles.RoleRefs(ts, [])) return auth.AuthData(token, user) def __validate_token(self, token_id, admin=True): diff --git a/keystone/logic/types/auth.py b/keystone/logic/types/auth.py index 63dde05c..187f5cd6 100644 --- a/keystone/logic/types/auth.py +++ b/keystone/logic/types/auth.py @@ -19,7 +19,7 @@ import json from lxml import etree import keystone.logic.types.fault as fault - +import keystone.logic.types.role as roles class PasswordCredentials(object): """Credentials based on username, password, and (optional) tenant_id. @@ -109,10 +109,11 @@ class Groups(object): class User(object): "A user." - def __init__(self, username, tenant_id, groups): + def __init__(self, username, tenant_id, groups , role_refs = None): self.username = username self.tenant_id = tenant_id self.groups = groups + self.role_refs = role_refs class AuthData(object): @@ -141,6 +142,8 @@ class AuthData(object): groups.append(g) user.append(groups) """ + if self.user.role_refs != None: + user.append(self.user.role_refs.to_dom()) dom.append(token) dom.append(user) return etree.tostring(dom) @@ -154,6 +157,9 @@ class AuthData(object): user = {} user["username"] = self.user.username user["tenantId"] = self.user.tenant_id + if self.user.role_refs != None: + user["roleRefs"] = self.user.role_refs.to_json_values() + """group = [] for g in self.user.groups.values: grp = {} diff --git a/keystone/logic/types/role.py b/keystone/logic/types/role.py index a88b9faf..2774ad36 100644 --- a/keystone/logic/types/role.py +++ b/keystone/logic/types/role.py @@ -189,6 +189,10 @@ class RoleRefs(object): self.links = links def to_xml(self): + dom = self.to_dom() + return etree.tostring(dom) + + def to_dom(self): dom = etree.Element("roleRefs") dom.set(u"xmlns", "http://docs.openstack.org/identity/api/v2.0") @@ -198,10 +202,14 @@ class RoleRefs(object): for t in self.links: dom.append(t.to_dom()) - return etree.tostring(dom) + return dom + def to_json(self): values = [t.to_dict()["roleRef"] for t in self.values] links = [t.to_dict()["links"] for t in self.links] return json.dumps({"roleRefs": {"values": values, "links": links}}) - \ No newline at end of file + + def to_json_values(self): + values = [t.to_dict()["roleRef"] for t in self.values] + return values \ No newline at end of file diff --git a/test/unit/test_common.py b/test/unit/test_common.py index 11d6a33d..e77f7fc8 100644 --- a/test/unit/test_common.py +++ b/test/unit/test_common.py @@ -739,6 +739,14 @@ def create_role_ref_xml(user_id, role_id, tenant_id, auth_token): "X-Auth-Token": auth_token, "ACCEPT": "application/xml"}) return (resp, content) + +def delete_role_ref(user, role_ref_id, auth_token): + header = httplib2.Http(".cache") + url = '%susers/%s/roleRefs/%s' % (URL, user, role_ref_id) + resp, content = header.request(url, "DELETE", body='', + headers={"Content-Type": "application/json", + "X-Auth-Token": str(auth_token)}) + return (resp, content) def create_role_xml(role_id, auth_token): header = httplib2.Http(".cache") diff --git a/test/unit/test_token.py b/test/unit/test_token.py index bfef2be3..0754f777 100644 --- a/test/unit/test_token.py +++ b/test/unit/test_token.py @@ -21,14 +21,16 @@ import sys sys.path.append(os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..', '..', 'keystone'))) import unittest - import test_common as utils - +import json +import keystone.logic.types.fault as fault +from lxml import etree class ValidateToken(unittest.TestCase): def setUp(self): self.tenant = utils.get_tenant() + self.user = 'joeuser' self.token = utils.get_token('joeuser', 'secrete', self.tenant, 'token') #self.user = utils.get_user() @@ -36,8 +38,19 @@ class ValidateToken(unittest.TestCase): self.auth_token = utils.get_auth_token() self.exp_auth_token = utils.get_exp_auth_token() #self.disabled_token = utils.get_disabled_token() + resp, content = utils.create_role_ref(self.user, 'Admin', self.tenant, str(self.auth_token)) + obj = json.loads(content) + if not "roleRef" in obj: + raise fault.BadRequestFault("Expecting RoleRef") + roleRef = obj["roleRef"] + if not "id" in roleRef: + self.role_ref_id = None + else: + self.role_ref_id = roleRef["id"] + def tearDown(self): + resp, content = utils.delete_role_ref(self.user, self.role_ref_id, self.auth_token) utils.delete_token(self.token, self.auth_token) def test_validate_token_true(self): @@ -53,6 +66,14 @@ class ValidateToken(unittest.TestCase): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) self.assertEqual('application/json', utils.content_type(resp)) + #verify content + obj = json.loads(content) + if not "auth" in obj: + raise self.fail("Expecting Auth") + role_refs = obj["auth"]["user"]["roleRefs"] + role_ref = role_refs[0] + role_ref_id = role_ref["id"] + self.assertEqual(self.role_ref_id, role_ref_id) def test_validate_token_true_xml(self): header = httplib2.Http(".cache") @@ -67,7 +88,28 @@ class ValidateToken(unittest.TestCase): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) self.assertEqual('application/xml', utils.content_type(resp)) - + #verify content + dom = etree.Element("root") + dom.append(etree.fromstring(content)) + auth = dom.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "auth") + if auth == None: + self.fail("Expecting Auth") + + user = auth.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "user") + if user == None: + self.fail("Expecting User") + roleRefs = user.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "roleRefs") + if roleRefs == None: + self.fail("Expecting Role Refs") + roleRef = roleRefs.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "roleRef") + if roleRef == None: + self.fail("Expecting Role Refs") + self.assertEqual(str(self.role_ref_id), roleRef.get("id")) + def test_validate_token_expired(self): header = httplib2.Http(".cache") url = '%stokens/%s?belongsTo=%s' % (utils.URL, self.exp_auth_token, -- cgit From 98b251e91bda8a7b7daaa511263650aa3c8c0620 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:27:05 -0500 Subject: Meging changes --- bin/sampledata.sh | 6 ++- docs/guide/src/docbkx/identitydevguide.xml | 6 +-- keystone/__init__.py | 2 +- keystone/content/identitydevguide.pdf | Bin 287930 -> 285331 bytes keystone/db/sqlalchemy/api.py | 1 + keystone/logic/service.py | 83 ++++++++++++++++++----------- keystone/logic/types/auth.py | 26 +++++++++ keystone/logic/types/baseURL.py | 35 +++++++----- keystone/logic/types/fault.py | 3 +- keystone/logic/types/tenant.py | 19 ++++--- keystone/server.py | 80 +++++++++++++++------------ setup.py | 4 +- 12 files changed, 170 insertions(+), 95 deletions(-) diff --git a/bin/sampledata.sh b/bin/sampledata.sh index 5506e8d3..4568c414 100755 --- a/bin/sampledata.sh +++ b/bin/sampledata.sh @@ -26,16 +26,20 @@ # Users ./keystone-manage $* user add joeuser secrete 1234 +./keystone-manage $* user add joeadmin secrete 1234 ./keystone-manage $* user add admin secrete 1234 ./keystone-manage $* user add disabled secrete 1234 ./keystone-manage $* user disable disabled # Roles ./keystone-manage $* role add Admin -./keystone-manage $* role grant Admin admin +./keystone-manage $* role grant Admin admin +./keystone-manage $* role grant Admin joeadmin 1234 +./keystone-manage $* role grant Admin joeadmin ANOTHER:TENANT #BaseURLs ./keystone-manage $* baseURLs add DFW cloudFiles public.cloudfiles.com admin.cloudfiles.com internal.cloudfiles.com 1 + # Groups #./keystone-manage $* group add Admin 1234 #./keystone-manage $* group add Default 1234 diff --git a/docs/guide/src/docbkx/identitydevguide.xml b/docs/guide/src/docbkx/identitydevguide.xml index cb67b683..5af5c532 100644 --- a/docs/guide/src/docbkx/identitydevguide.xml +++ b/docs/guide/src/docbkx/identitydevguide.xml @@ -60,7 +60,7 @@ API v2.0 Keystone - OpenStack Identity - 2011-05-27 + 2011-06-01 Copyright details are filled in by the template. @@ -756,7 +756,7 @@ Host: identity.api.openstack.org/v1.1/ &GET; /tenants - Get a list of tenants accessible with suplied token. + Get a list of tenants accessible with supplied token. @@ -865,7 +865,7 @@ Host: identity.api.openstack.org/v1.1/ &GET; - /tokens + /tokens/tokenId Validate a token. diff --git a/keystone/__init__.py b/keystone/__init__.py index b70c0009..643310e5 100644 --- a/keystone/__init__.py +++ b/keystone/__init__.py @@ -44,4 +44,4 @@ from auth_protocols.auth_openid \ #Remote Auth handler from middleware.remoteauth \ import filter_factory as remoteauth_factory -''' \ No newline at end of file +''' diff --git a/keystone/content/identitydevguide.pdf b/keystone/content/identitydevguide.pdf index ce4207aa..b3a7f978 100644 Binary files a/keystone/content/identitydevguide.pdf and b/keystone/content/identitydevguide.pdf differ diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py index cdd3e948..f2dda017 100644 --- a/keystone/db/sqlalchemy/api.py +++ b/keystone/db/sqlalchemy/api.py @@ -716,6 +716,7 @@ def user_role_add(values): def user_tenant_create(values): + #TODO(ZIAD): Update model / fix this user_tenant_ref = models.UserTenantAssociation() user_tenant_ref.update(values) user_tenant_ref.save() diff --git a/keystone/logic/service.py b/keystone/logic/service.py index 86d66449..9bf7d3bc 100644 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -27,6 +27,7 @@ import keystone.logic.types.role as roles import keystone.logic.types.user as users import keystone.logic.types.baseURL as baseURLs + class IdentityService(object): "This is the logical implemenation of the Identity service" @@ -73,7 +74,7 @@ class IdentityService(object): dtoken.expires = datetime.now() + timedelta(days=1) db_api.token_create(dtoken) - return self.__get_auth_data(dtoken, duser) + return self.__get_auth_data(dtoken) def validate_token(self, admin_token, token_id, belongs_to=None): self.__validate_token(admin_token) @@ -88,7 +89,7 @@ class IdentityService(object): if not user.enabled: raise fault.UserDisabledFault("The user %s has been disabled!" % user.id) - return self.__get_auth_data(token, user) + return self.__get_validate_data(token, user) def revoke_token(self, admin_token, token_id): self.__validate_token(admin_token) @@ -383,7 +384,6 @@ class IdentityService(object): db_api.user_tenant_group_delete(user, group) return None - # # Private Operations # @@ -398,7 +398,6 @@ class IdentityService(object): user = db_api.user_get(token.user_id) return (token, user) - # # User Operations # @@ -432,11 +431,9 @@ class IdentityService(object): duser.enabled = user.enabled duser.tenant_id = tenant_id db_api.user_create(duser) - return user - def get_tenant_users(self, admin_token, tenant_id, marker, limit, url): self.__validate_token(admin_token) @@ -822,11 +819,19 @@ class IdentityService(object): # - def __get_auth_data(self, dtoken, duser): - """return AuthData object for a token/user pair""" + def __get_auth_data(self, dtoken): + """return AuthData object for a token""" token = auth.Token(dtoken.expires, dtoken.token_id, dtoken.tenant_id) + return auth.AuthData(token) + + def __get_validate_data(self, dtoken, duser): + """return ValidateData object for a token/user pair""" + + token = auth.Token(dtoken.expires, dtoken.token_id, dtoken.tenant_id) + +<<<<<<< HEAD """gs = [] for ug in duser.groups: dgroup = db_api.group_get(ug.group_id) @@ -846,6 +851,11 @@ class IdentityService(object): droleRef.tenant_id)) user = auth.User(duser.id, duser.tenant_id, None, roles.RoleRefs(ts, [])) return auth.AuthData(token, user) +======= + user = auth.User(duser.id, duser.tenant_id, None) + + return auth.ValidateData(token, user) +>>>>>>> rackspace/master def __validate_token(self, token_id, admin=True): if not token_id: @@ -867,7 +877,7 @@ class IdentityService(object): raise fault.UnauthorizedFault("You are not authorized " "to make this call") return (token, user) - + def create_role(self, admin_token, role): self.__validate_token(admin_token) @@ -885,7 +895,7 @@ class IdentityService(object): drole.desc = role.desc db_api.role_create(drole) return role - + def get_roles(self, admin_token, marker, limit, url): self.__validate_token(admin_token) @@ -911,24 +921,24 @@ class IdentityService(object): if not drole: raise fault.ItemNotFoundFault("The role could not be found") return roles.Role(drole.id, drole.desc) - + def create_role_ref(self, admin_token, user_id, roleRef): self.__validate_token(admin_token) duser = db_api.user_get(user_id) if not duser: raise fault.ItemNotFoundFault("The user could not be found") - + if not isinstance(roleRef, roles.RoleRef): raise fault.BadRequestFault("Expecting a Role Ref") if roleRef.role_id == None: raise fault.BadRequestFault("Expecting a Role Id") - + drole = db_api.role_get(roleRef.role_id) if drole == None: raise fault.ItemNotFoundFault("The role not found") - + if roleRef.tenant_id != None: dtenant = db_api.tenant_get(roleRef.tenant_id) if dtenant == None: @@ -942,12 +952,12 @@ class IdentityService(object): user_role_ref = db_api.user_role_add(drole_ref) roleRef.role_ref_id = user_role_ref.id return roleRef - + def delete_role_ref(self, admin_token, role_ref_id): self.__validate_token(admin_token) db_api.role_ref_delete(role_ref_id) return None - + def get_user_roles(self, admin_token, marker, limit, url, user_id): self.__validate_token(admin_token) duser = db_api.user_get(user_id) @@ -958,7 +968,7 @@ class IdentityService(object): ts = [] droleRefs = db_api.role_ref_get_page(marker, limit, user_id) for droleRef in droleRefs: - ts.append(roles.RoleRef(droleRef.id,droleRef.role_id, + ts.append(roles.RoleRef(droleRef.id, droleRef.role_id, droleRef.tenant_id)) prev, next = db_api.role_ref_get_page_markers(user_id, marker, limit) links = [] @@ -969,14 +979,18 @@ class IdentityService(object): links.append(atom.Link('next', "%s?'marker=%s&limit=%s'" \ % (url, next, limit))) return roles.RoleRefs(ts, links) - + def get_baseurls(self, admin_token, marker, limit, url): self.__validate_token(admin_token) ts = [] dbaseurls = db_api.baseurls_get_page(marker, limit) for dbaseurl in dbaseurls: - ts.append(baseURLs.BaseURL(dbaseurl.id, dbaseurl.region, dbaseurl.service, dbaseurl.public_url, dbaseurl.admin_url, dbaseurl.internal_url, dbaseurl.enabled)) + ts.append(baseURLs.BaseURL(dbaseurl.id, dbaseurl.region, + dbaseurl.service, dbaseurl.public_url, + dbaseurl.admin_url, + dbaseurl.internal_url, + dbaseurl.enabled)) prev, next = db_api.baseurls_get_page_markers(marker, limit) links = [] if prev: @@ -993,9 +1007,11 @@ class IdentityService(object): dbaseurl = db_api.baseurls_get(baseurl_id) if not dbaseurl: raise fault.ItemNotFoundFault("The base URL could not be found") - return baseURLs.BaseURL(dbaseurl.id, dbaseurl.region, dbaseurl.service, dbaseurl.public_url, dbaseurl.admin_url, dbaseurl.internal_url, dbaseurl.enabled) - - def get_tenant_baseURLs(self, admin_token, marker, limit, url, tenant_id): + return baseURLs.BaseURL(dbaseurl.id, dbaseurl.region, dbaseurl.service, + dbaseurl.public_url, dbaseurl.admin_url, + dbaseurl.internal_url, dbaseurl.enabled) + + def get_tenant_baseURLs(self, admin_token, marker, limit, url, tenant_id): self.__validate_token(admin_token) if tenant_id == None: raise fault.BadRequestFault("Expecting a Tenant Id") @@ -1004,14 +1020,18 @@ class IdentityService(object): raise fault.ItemNotFoundFault("The tenant not found") ts = [] - - dtenantBaseURLAssociations = db_api.baseurls_ref_get_by_tenant_get_page(tenant_id, marker, + + dtenantBaseURLAssociations = \ + db_api.baseurls_ref_get_by_tenant_get_page(tenant_id, marker, limit) for dtenantBaseURLAssociation in dtenantBaseURLAssociations: - ts.append(baseURLs.BaseURLRef(dtenantBaseURLAssociation.id, url + '/baseURLs/' + str(dtenantBaseURLAssociation.baseURLs_id))) + ts.append(baseURLs.BaseURLRef(dtenantBaseURLAssociation.id, + url + '/baseURLs/' + \ + str(dtenantBaseURLAssociation.baseURLs_id))) links = [] if ts.__len__(): - prev, next = db_api.baseurls_ref_get_by_tenant_get_page_markers(tenant_id, + prev, next = \ + db_api.baseurls_ref_get_by_tenant_get_page_markers(tenant_id, marker, limit) if prev: links.append(atom.Link('prev', "%s?'marker=%s&limit=%s'" % @@ -1021,7 +1041,8 @@ class IdentityService(object): (url, next, limit))) return baseURLs.BaseURLRefs(ts, links) - def create_baseurl_ref_to_tenant(self, admin_token, tenant_id, baseurl, url): + def create_baseurl_ref_to_tenant(self, admin_token, + tenant_id, baseurl, url): self.__validate_token(admin_token) if tenant_id == None: raise fault.BadRequestFault("Expecting a Tenant Id") @@ -1036,12 +1057,12 @@ class IdentityService(object): dbaseurl_ref.tenant_id = tenant_id dbaseurl_ref.baseURLs_id = baseurl.id dbaseurl_ref = db_api.baseurls_ref_add(dbaseurl_ref) - baseurlRef = baseURLs.BaseURLRef(dbaseurl_ref.id, url + '/baseURLs/' + dbaseurl_ref.baseURLs_id) + baseurlRef = baseURLs.BaseURLRef(dbaseurl_ref.id, url + \ + '/baseURLs/' + \ + dbaseurl_ref.baseURLs_id) return baseurlRef - + def delete_baseurls_ref(self, admin_token, baseurls_id): self.__validate_token(admin_token) db_api.baseurls_ref_delete(baseurls_id) return None - - \ No newline at end of file diff --git a/keystone/logic/types/auth.py b/keystone/logic/types/auth.py index 187f5cd6..b010eea5 100644 --- a/keystone/logic/types/auth.py +++ b/keystone/logic/types/auth.py @@ -119,6 +119,32 @@ class User(object): class AuthData(object): "Authentation Information returned upon successful login." + def __init__(self, token): + self.token = token + + def to_xml(self): + dom = etree.Element("auth", + xmlns="http://docs.openstack.org/identity/api/v2.0") + token = etree.Element("token", + expires=self.token.expires.isoformat()) + token.set("id", self.token.token_id) + dom.append(token) + return etree.tostring(dom) + + def to_json(self): + token = {} + token["id"] = self.token.token_id + token["expires"] = self.token.expires.isoformat() + auth = {} + auth["token"] = token + ret = {} + ret["auth"] = auth + return json.dumps(ret) + + +class ValidateData(object): + "Authentation Information returned upon successful token validation." + def __init__(self, token, user): self.token = token self.user = user diff --git a/keystone/logic/types/baseURL.py b/keystone/logic/types/baseURL.py index f5b770fd..c73536bf 100644 --- a/keystone/logic/types/baseURL.py +++ b/keystone/logic/types/baseURL.py @@ -18,6 +18,8 @@ from lxml import etree import string import keystone.logic.types.fault as fault + + class BaseURL(object): @staticmethod def from_xml(xml_str): @@ -35,7 +37,8 @@ class BaseURL(object): admin_url = root.get("adminURL") internal_url = root.get("internalURL") enabled = root.get("enabled") - return BaseURL(id, region, service, public_url, admin_url, internal_url, enabled) + return BaseURL(id, region, service, public_url, admin_url, + internal_url, enabled) except etree.LxmlError as e: raise fault.BadRequestFault("Cannot parse baseURL", str(e)) @@ -62,22 +65,24 @@ class BaseURL(object): if 'region' in baseURL: region = baseURL["region"] - if 'serviceName' in baseURL: + if 'serviceName' in baseURL: service = baseURL["serviceName"] - if 'publicURL' in baseURL: + if 'publicURL' in baseURL: public_url = baseURL["publicURL"] if 'adminURL' in baseURL: admin_url = baseURL["adminURL"] - if 'internalURL' in baseURL: + if 'internalURL' in baseURL: internal_url = baseURL["internalURL"] if 'enabled' in baseURL: enabled = baseURL["enabled"] - - return BaseURL(id, region, service, public_url, admin_url, internal_url, enabled) + + return BaseURL(id, region, service, public_url, admin_url, + internal_url, enabled) except (ValueError, TypeError) as e: raise fault.BadRequestFault("Cannot parse baseURL", str(e)) - def __init__(self, id, region, service, public_url, admin_url, internal_url, enabled): + def __init__(self, id, region, service, public_url, admin_url, + internal_url, enabled): self.id = id self.region = region self.service = service @@ -85,7 +90,7 @@ class BaseURL(object): self.admin_url = admin_url self.internal_url = internal_url self.enabled = enabled - + def to_dom(self): dom = etree.Element("baseURL", xmlns="http://docs.openstack.org/identity/api/v2.0") @@ -128,7 +133,8 @@ class BaseURL(object): def to_json(self): return json.dumps(self.to_dict()) - + + class BaseURLs(object): "A collection of baseURls." @@ -151,14 +157,14 @@ class BaseURLs(object): def to_json(self): values = [t.to_dict()["baseURL"] for t in self.values] links = [t.to_dict()["links"] for t in self.links] - return json.dumps({"baseURLs": {"values": values, "links": links}}) - + return json.dumps({"baseURLs": {"values": values, "links": links}}) + class BaseURLRef(object): def __init__(self, id, href): self.id = id self.href = href - + def to_dom(self): dom = etree.Element("baseURLRef", xmlns="http://docs.openstack.org/identity/api/v2.0") @@ -178,10 +184,11 @@ class BaseURLRef(object): if self.href: baseURLRef["href"] = self.href return {'baseURLRef': baseURLRef} - + def to_json(self): return json.dumps(self.to_dict()) + class BaseURLRefs(object): "A collection of baseURlRefs." @@ -204,4 +211,4 @@ class BaseURLRefs(object): def to_json(self): values = [t.to_dict()["baseURLRef"] for t in self.values] links = [t.to_dict()["links"] for t in self.links] - return json.dumps({"baseURLRefs": {"values": values, "links": links}}) + return json.dumps({"baseURLRefs": {"values": values, "links": links}}) diff --git a/keystone/logic/types/fault.py b/keystone/logic/types/fault.py index cfcd15d2..fa69dd13 100644 --- a/keystone/logic/types/fault.py +++ b/keystone/logic/types/fault.py @@ -33,7 +33,7 @@ class IdentityFault(Exception): def to_xml(self): dom = etree.Element(self.key, - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") dom.set("code", str(self.code)) msg = etree.Element("message") msg.text = self.msg @@ -160,6 +160,7 @@ class UserGroupConflictFault(IdentityFault): super(UserGroupConflictFault, self).__init__(msg, details, code) self.key = "userGroupConflict" + class RoleConflictFault(IdentityFault): "The User already exists?" diff --git a/keystone/logic/types/tenant.py b/keystone/logic/types/tenant.py index e3c7690f..652e3741 100644 --- a/keystone/logic/types/tenant.py +++ b/keystone/logic/types/tenant.py @@ -33,7 +33,8 @@ class Tenant(object): try: dom = etree.Element("root") dom.append(etree.fromstring(xml_str)) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}tenant") + root = dom.find( + "{http://docs.openstack.org/identity/api/v2.0}tenant") if root == None: raise fault.BadRequestFault("Expecting Tenant") tenant_id = root.get("id") @@ -77,8 +78,8 @@ class Tenant(object): def to_dom(self): dom = etree.Element("tenant", - xmlns="http://docs.openstack.org/identity/api/v2.0", - enabled=string.lower(str(self.enabled))) + xmlns="http://docs.openstack.org/identity/api/v2.0", + enabled=string.lower(str(self.enabled))) if self.tenant_id: dom.set("id", self.tenant_id) desc = etree.Element("description") @@ -142,7 +143,8 @@ class Group(object): try: dom = etree.Element("root") dom.append(etree.fromstring(xml_str)) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}group") + root = dom.find( \ + "{http://docs.openstack.org/identity/api/v2.0}group") if root == None: raise fault.BadRequestFault("Expecting Group") group_id = root.get("id") @@ -188,7 +190,7 @@ class Group(object): def to_dom(self): dom = etree.Element("group", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") if self.group_id: dom.set("id", self.group_id) if self.tenant_id: @@ -251,7 +253,8 @@ class GlobalGroup(object): try: dom = etree.Element("root") dom.append(etree.fromstring(xml_str)) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}group") + root = dom.find(\ + "{http://docs.openstack.org/identity/api/v2.0}group") if root == None: raise fault.BadRequestFault("Expecting Group") group_id = root.get("id") @@ -287,7 +290,7 @@ class GlobalGroup(object): def to_dom(self): dom = etree.Element("group", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") if self.group_id: dom.set("id", self.group_id) @@ -354,7 +357,7 @@ class User(object): def to_dom(self): dom = etree.Element("user", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") if self.group_id != None: dom.set("group_id", self.group_id) if self.user_id: diff --git a/keystone/server.py b/keystone/server.py index 6f4a333b..fd4b50d7 100644 --- a/keystone/server.py +++ b/keystone/server.py @@ -223,7 +223,7 @@ class TenantController(wsgi.Controller): @utils.wrap_error def get_tenants(self, req): - marker, limit, url = get_marker_limit_and_url(req) + marker, limit, url = get_marker_limit_and_url(req) tenants = service.get_tenants(utils.get_auth_token(req), marker, limit, url) return utils.send_result(200, req, tenants) @@ -452,31 +452,33 @@ class RolesController(wsgi.Controller): roles = service.get_roles(utils.get_auth_token(req), marker, limit, url) return utils.send_result(200, req, roles) - - @utils.wrap_error + + @utils.wrap_error def get_role(self, req, role_id): role = service.get_role(utils.get_auth_token(req), role_id) return utils.send_result(200, req, role) - - @utils.wrap_error + + @utils.wrap_error def create_role_ref(self, req, user_id): roleRef = utils.get_normalized_request_content(roles.RoleRef, req) - return utils.send_result(201, req, service.create_role_ref(utils.get_auth_token(req), user_id, roleRef)) - + return utils.send_result(201, req, service.create_role_ref( + utils.get_auth_token(req), user_id, roleRef)) + @utils.wrap_error def get_role_refs(self, req, user_id): marker, limit, url = get_marker_limit_and_url(req) roleRefs = service.get_user_roles(utils.get_auth_token(req), - marker, limit, url,user_id) + marker, limit, url, user_id) return utils.send_result(200, req, roleRefs) - + @utils.wrap_error def delete_role_ref(self, req, user_id, role_ref_id): rval = service.delete_role_ref(utils.get_auth_token(req), role_ref_id) return utils.send_result(204, req, rval) - + + class BaseURLsController(wsgi.Controller): """ BaseURL Controller - @@ -485,7 +487,7 @@ class BaseURLsController(wsgi.Controller): def __init__(self, options): self.options = options - + @utils.wrap_error def get_baseurls(self, req): marker, limit, url = get_marker_limit_and_url(req) @@ -497,26 +499,29 @@ class BaseURLsController(wsgi.Controller): def get_baseurl(self, req, baseURLId): baseurl = service.get_baseurl(utils.get_auth_token(req), baseURLId) return utils.send_result(200, req, baseurl) - + @utils.wrap_error def get_baseurls_for_tenant(self, req, tenant_id): marker, limit, url = get_marker_limit_and_url(req) baseURLRefs = service.get_tenant_baseURLs(utils.get_auth_token(req), marker, limit, url, tenant_id) return utils.send_result(200, req, baseURLRefs) - - @utils.wrap_error + + @utils.wrap_error def add_baseurls_to_tenant(self, req, tenant_id): baseurl = utils.get_normalized_request_content(baseURLs.BaseURL, req) return utils.send_result(201, req, - service.create_baseurl_ref_to_tenant(utils.get_auth_token(req), - tenant_id, baseurl, get_url(req))) - @utils.wrap_error + service.create_baseurl_ref_to_tenant( + utils.get_auth_token(req), + tenant_id, baseurl, get_url(req))) + + @utils.wrap_error def remove_baseurls_from_tenant(self, req, tenant_id, baseurls_ref_id): rval = service.delete_baseurls_ref(utils.get_auth_token(req), baseurls_ref_id) return utils.send_result(204, req, rval) + def get_marker_limit_and_url(req): marker = None limit = 10 @@ -528,7 +533,8 @@ def get_marker_limit_and_url(req): limit = req.GET["limit"] url = get_url(req) return (marker, limit, url) - + + def get_marker_and_limit(req): marker = None limit = 10 @@ -539,14 +545,15 @@ def get_marker_and_limit(req): if "limit" in req.GET: limit = req.GET["limit"] + def get_url(req): url = '%s://%s:%s%s' % (req.environ['wsgi.url_scheme'], req.environ.get("SERVER_NAME"), req.environ.get("SERVER_PORT"), req.environ['PATH_INFO']) return url - - + + class KeystoneAPI(wsgi.Router): """WSGI entry point for public Keystone API requests.""" @@ -573,10 +580,8 @@ class KeystoneAPI(wsgi.Router): # Token Operations mapper.connect("/v2.0/tokens", controller=auth_controller, - action="authenticate") - mapper.connect("/v2.0/tokens/{token_id}", controller=auth_controller, - action="delete_token", - conditions=dict(method=["DELETE"])) + action="authenticate", + conditions=dict(method=["POST"])) # Tenant Operations tenant_controller = TenantController(options) @@ -639,7 +644,8 @@ class KeystoneAdminAPI(wsgi.Router): # Token Operations auth_controller = AuthController(options) mapper.connect("/v2.0/tokens", controller=auth_controller, - action="authenticate") + action="authenticate", + conditions=dict(method=["POST"])) mapper.connect("/v2.0/tokens/{token_id}", controller=auth_controller, action="validate_token", conditions=dict(method=["GET"])) @@ -785,16 +791,22 @@ class KeystoneAdminAPI(wsgi.Router): baseurls_controller = BaseURLsController(options) mapper.connect("/v2.0/baseURLs", controller=baseurls_controller, action="get_baseurls", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/baseURLs/{baseURLId}", controller=baseurls_controller, + mapper.connect("/v2.0/baseURLs/{baseURLId}", + controller=baseurls_controller, action="get_baseurl", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}/baseURLRefs", controller=baseurls_controller, - action="get_baseurls_for_tenant", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}/baseURLRefs", controller=baseurls_controller, - action="add_baseurls_to_tenant", conditions=dict(method=["POST"])) - mapper.connect("/v2.0/tenants/{tenant_id}/baseURLRefs/{baseurls_ref_id}", controller=baseurls_controller, - action="remove_baseurls_from_tenant", conditions=dict(method=["DELETE"])) - - + mapper.connect("/v2.0/tenants/{tenant_id}/baseURLRefs", + controller=baseurls_controller, + action="get_baseurls_for_tenant", + conditions=dict(method=["GET"])) + mapper.connect("/v2.0/tenants/{tenant_id}/baseURLRefs", + controller=baseurls_controller, + action="add_baseurls_to_tenant", + conditions=dict(method=["POST"])) + mapper.connect( + "/v2.0/tenants/{tenant_id}/baseURLRefs/{baseurls_ref_id}", + controller=baseurls_controller, + action="remove_baseurls_from_tenant", + conditions=dict(method=["DELETE"])) # Miscellaneous Operations version_controller = VersionController(options) diff --git a/setup.py b/setup.py index da2c3c20..2c54da52 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ from setuptools import setup, find_packages -version = '1.0' +version = '1.0' setup( name='keystone', @@ -38,7 +38,7 @@ setup( 'paste.app_factory': ['main=identity:app_factory'], 'paste.filter_factory': [ 'remoteauth=keystone:remoteauth_factory', - 'tokenauth=keystone:tokenauth_factory', + 'tokenauth=keystone.auth_protocols.auth_token:filter_factory', ], }, ) -- cgit From 06093efd33b13520d23a24358337548ce5aa813b Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:28:43 -0500 Subject: Merging changes --- docs/swift-quick-start.txt | 121 ++++++++++++++++++++ test/unit/__init__.py | 0 test/unit/base.py | 275 +++++++++++++++++++++++++++++++++++++++++++++ test/unit/decorators.py | 49 ++++++++ test/unit/test_authn_v2.py | 89 +++++++++++++++ 5 files changed, 534 insertions(+) create mode 100644 docs/swift-quick-start.txt create mode 100644 test/unit/__init__.py create mode 100644 test/unit/base.py create mode 100644 test/unit/decorators.py create mode 100644 test/unit/test_authn_v2.py diff --git a/docs/swift-quick-start.txt b/docs/swift-quick-start.txt new file mode 100644 index 00000000..50b380d8 --- /dev/null +++ b/docs/swift-quick-start.txt @@ -0,0 +1,121 @@ +Quick Start to Integrating Swift and Keystone +--------------------------------------------- + +1. Install Swift with the included TempAuth. This step is beyond the scope of + this quick start; see http://swift.openstack.org/development_saio.html for + a Swift development set up guide. + +2. Obtain and install a source copy of Keystone:: + + git clone https://github.com/khussein/keystone.git ~/keystone + cd ~/keystone && sudo python setup.py develop + +3. Move included configuration out of the way:: + + mv ~/keystone/etc ~/keystone/etc-orig + +4. Create /etc/keystone configuration directory:: + + sudo mkdir /etc/keystone + sudo chmod : /etc/keystone + +5. Create /etc/keystone/keystone.conf:: + + [DEFAULT] + verbose = True + debug = True + default_store = sqlite + log_file = /etc/keystone/keystone.log + sql_connection = sqlite:////etc/keystone/keystone.db + sql_idle_timeout = 30 + + [app:admin] + paste.app_factory = keystone.server:admin_app_factory + bind_host = 0.0.0.0 + bind_port = 8081 + + [app:server] + paste.app_factory = keystone.server:app_factory + bind_host = 0.0.0.0 + bind_port = 8080 + +6. Start up the Keystone service:: + + ~/keystone/bin/keystone + +7. Create the sample data entries:: + + cd ~/keystone/bin && ./sampledata.sh + +8. Configure Swift's proxy server to use Keystone instead of TempAuth. Here's + an example /etc/swift/proxy-server.conf:: + + [DEFAULT] + bind_port = 8888 + user = + + [pipeline:main] + pipeline = catch_errors cache keystone proxy-server + + [app:proxy-server] + use = egg:swift#proxy + allow_account_management = true + + [filter:keystone] + use = egg:keystone#tokenauth + auth_protocol = http + auth_host = 127.0.0.1 + auth_port = 8081 + admin_token = 999888777666 + delay_auth_decision = 0 + service_protocol = http + service_host = 127.0.0.1 + service_port = 8100 + service_pass = dTpw + + [filter:cache] + use = egg:swift#memcache + set log_name = cache + + [filter:catch_errors] + use = egg:swift#catch_errors + +9. Restart the Swift proxy to invoke the new configuration:: + + swift-init proxy restart + +10. Obtain an x-auth-token to use:: + + curl -i http://127.0.0.1:8080/v1.0 \ + -H 'x-auth-user: joeuser' -H 'x-auth-key: secrete' + +11. Create an account in Swift using the x-auth-token from above:: + + curl -X PUT http://127.0.0.1:8888/v1/joeuser \ + -H 'x-auth-token: ' + +12. Create a container in Swift:: + + curl -X PUT http://127.0.0.1:8888/v1/joeuser/container \ + -H 'x-auth-token: ' + +13. Upload an object:: + + curl -X PUT http://127.0.0.1:8888/v1/joeuser/container/object \ + -H 'x-auth-token: ' --data-binary 'test object' + +14. Do some listings:: + + curl http://127.0.0.1:8888/v1/joeuser -H 'x-auth-token: ' + curl http://127.0.0.1:8888/v1/joeuser/container \ + -H 'x-auth-token: ' + + +Notes +----- + +* Keystone does not yet return x-storage-url, so standard Swift tools won't + work yet. +* Keystone currently allows any valid token to do anything. + +But, it works as a demo! diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/base.py b/test/unit/base.py new file mode 100644 index 00000000..d9ace1da --- /dev/null +++ b/test/unit/base.py @@ -0,0 +1,275 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base test case classes for the unit tests""" + +import datetime +import functools +import json +import httplib +import logging +import pprint +import unittest + +from lxml import etree, objectify +import webob + +from keystone import server +from keystone.db.sqlalchemy import api as db_api + +logger = logging.getLogger('test.unit.base') + + +class ServiceAPITest(unittest.TestCase): + + """ + Base test case class for any unit test that tests the main service API. + """ + + """ + The `api` attribute for this base class is the `server.KeystoneAPI` + controller. + """ + api_class = server.KeystoneAPI + + """ + Dict of configuration options to pass to the API controller + """ + options = {'sql_connection': 'sqlite:///', # in-memory db + 'verbose': False, + 'debug': False} + + """ + Set of dicts of tenant attributes we start each test case with + """ + tenant_fixtures = [ + {'id': 'tenant1', + 'enabled': True, + 'desc': 'tenant1'} + ] + + """ + Attributes of the user the test creates for each test case that + will authenticate against the API. The `auth_user` attribute + will contain the created user with the following attributes. + """ + auth_user_attrs = {'id': 'auth_user', + 'password': 'auth_pass', + 'email': 'auth_user@example.com', + 'enabled': True, + 'tenant_id': 'tenant1'} + """ + Special attribute that is the identifier of the token we use in + authenticating. Makes it easy to test the authentication process. + """ + auth_token_id = 'SPECIALAUTHTOKEN' + + """ + Content-type of requests. Generally, you don't need to manually + change this. Instead, :see test.unit.decorators + """ + content_type = 'json' + + """ + Version of the API to test + """ + api_version = '2.0' + + def setUp(self): + self.api = self.api_class(self.options) + + self.expires = datetime.datetime.utcnow() + self.clear_all_data() + + # Create all our base tenants + for tenant in self.tenant_fixtures: + self.fixture_create_tenant(**tenant) + + # Create the user we will authenticate with + self.auth_user = self.fixture_create_user(**self.auth_user_attrs) + self.auth_token = self.fixture_create_token( + user_id=self.auth_user['id'], + tenant_id=self.auth_user['tenant_id'], + expires=self.expires, + token_id=self.auth_token_id) + + self.add_verify_status_helpers() + + def tearDown(self): + self.clear_all_data() + setattr(self, 'req', None) + setattr(self, 'res', None) + + def clear_all_data(self): + """ + Purges the database of all data + """ + db_api.unregister_models() + logger.debug("Cleared all data from database") + db_api.register_models() + + def fixture_create_tenant(self, **kwargs): + """ + Creates a tenant fixture. + + :params **kwargs: Attributes of the tenant to create + """ + values = kwargs.copy() + tenant = db_api.tenant_create(values) + logger.debug("Created tenant fixture %s", values['id']) + return tenant + + def fixture_create_user(self, **kwargs): + """ + Creates a user fixture. If the user's tenant ID is set, and the tenant + does not exist in the database, the tenant is created. + + :params **kwargs: Attributes of the user to create + """ + values = kwargs.copy() + tenant_id = values.get('tenant_id') + if tenant_id: + if not db_api.tenant_get(tenant_id): + db_api.tenant_create({'id': tenant_id, + 'enabled': True, + 'desc': tenant_id}) + user = db_api.user_create(values) + logger.debug("Created user fixture %s", values['id']) + return user + + def fixture_create_token(self, **kwargs): + """ + Creates a token fixture. + + :params **kwargs: Attributes of the token to create + """ + values = kwargs.copy() + token = db_api.token_create(values) + logger.debug("Created token fixture %s", values['token_id']) + return token + + def get_request(self, method, url, headers=None): + """ + Sets the `req` attribute to a `webob.Request` object that + is constructed with the supplied method and url. Supplied + headers are added to appropriate Content-type headers. + """ + headers = headers or {} + self.req = webob.Request.blank('/v%s/%s' % (self.api_version, + url.lstrip('/'))) + self.req.method = method + self.req.headers = headers + if 'content-type' not in headers: + ct = 'application/%s' % self.content_type + self.req.headers['content-type'] = ct + self.req.headers['accept'] = ct + return self.req + + def get_response(self): + """ + Sets the appropriate headers for the `req` attribute for + the current content type, then calls `req.get_response()` and + sets the `res` attribute to the returned `webob.Response` object + """ + self.res = self.req.get_response(self.api) + logger.debug("%s %s returned %s", self.req.method, self.req.path_qs, + self.res.status) + if self.res.status_int != httplib.OK: + logger.debug("Response Body:") + for line in self.res.body.split("\n"): + logger.debug(line) + return self.res + + def verify_status(self, status_code): + """ + Simple convenience wrapper for validating a response's status + code. + """ + if not getattr(self, 'res'): + raise RuntimeError("Called verify_status() before calling " + "get_response()!") + + self.assertEqual(status_code, self.res.status_int, + "Incorrect status code %d. Expected %d" % + (self.res.status_int, status_code)) + + def add_verify_status_helpers(self): + """ + Adds some convenience helpers using partials... + """ + self.status_ok = functools.partial(self.verify_status, httplib.OK) + + def assert_dict_equal(self, expected, got): + """ + Compares two dicts for equality and prints the dictionaries + nicely formatted for easy comparison if there is a failure. + """ + self.assertEqual(expected, got, "Mappings are not equal.\n" + "Got:\n%s\nExpected:\n%s" % + (pprint.pformat(got), + pprint.pformat(expected))) + + def assert_xml_strings_equal(self, expected, got): + """ + Compares two XML strings for equality by parsing them both + into DOMs. Prints the DOMs nicely formatted for easy comparison + if there is a failure. + """ + # This is a nice little trick... objectify.fromstring() returns + # a DOM different from etree.fromstring(). The objectify version + # removes any different whitespacing... + got = objectify.fromstring(got) + expected = objectify.fromstring(expected) + self.assertEqual(etree.tostring(expected), + etree.tostring(got), "DOMs are not equal.\n" + "Got:\n%s\nExpected:\n%s" % + (etree.tostring(got, pretty_print=True), + etree.tostring(expected, pretty_print=True))) + + +class AdminAPITest(ServiceAPITest): + + """ + Base test case class for any unit test that tests the admin API. The + """ + + """ + The `api` attribute for this base class is the `server.KeystoneAdminAPI` + controller. + """ + api_class = server.KeystoneAdminAPI + + """ + Set of dicts of tenant attributes we start each test case with + """ + tenant_fixtures = [ + {'id': 'tenant1', + 'enabled': True, + 'desc': 'tenant1'}, + {'id': 'tenant2', + 'enabled': True, + 'desc': 'tenant2'} + ] + + """ + Attributes of the user the test creates for each test case that + will authenticate against the API. + """ + auth_user_attrs = {'id': 'admin_user', + 'password': 'admin_pass', + 'email': 'admin_user@example.com', + 'enabled': True, + 'tenant_id': 'tenant2'} diff --git a/test/unit/decorators.py b/test/unit/decorators.py new file mode 100644 index 00000000..17a7d432 --- /dev/null +++ b/test/unit/decorators.py @@ -0,0 +1,49 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Decorators useful in unit tests""" + +import functools + + +def content_type(func, content_type='json'): + """ + Decorator for a test case method that sets the test case's + content_type to 'json' or 'xml' and resets it afterwards to + the original setting. This also asserts that if there is a + value for the test object's `res` attribute, that the content-type + header of the response is correct. + """ + @functools.wraps(func) + def wrapped(*a, **kwargs): + test_obj = a[0] + orig_content_type = test_obj.content_type + try: + test_obj.content_type = content_type + func(*a, **kwargs) + if getattr(test_obj, 'res'): + expected = 'application/%s' % content_type + got = test_obj.res.headers['content-type'].split(';')[0] + test_obj.assertEqual(expected, got, + "Bad content type: %s. Expected: %s" % + (got, expected)) + finally: + test_obj.content_type = orig_content_type + return wrapped + + +jsonify = functools.partial(content_type, content_type='json') +xmlify = functools.partial(content_type, content_type='xml') diff --git a/test/unit/test_authn_v2.py b/test/unit/test_authn_v2.py new file mode 100644 index 00000000..b3550033 --- /dev/null +++ b/test/unit/test_authn_v2.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging + +from test.unit import base +from test.unit.decorators import jsonify, xmlify +from test.unit import test_common as utils + +logger = logging.getLogger('test.unit.test_authn_v2') + + +class TestAuthnV2(base.ServiceAPITest): + + """ + Tests for the /v2.0/tokens auth endpoint + """ + + api_version = '2.0' + + @jsonify + def test_authn_json(self): + url = "/tokens" + req = self.get_request('GET', url) + body = { + "passwordCredentials": { + "username": self.auth_user['id'], + "password": self.auth_user['password'], + "tenantId": self.auth_user['tenant_id'] + } + } + req.body = json.dumps(body) + self.get_response() + self.status_ok() + + expected = { + u'auth': { + u'token': { + u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f"), + u'id': self.auth_token_id, + u'tenantId': self.auth_user['tenant_id'] + }, + u'user': { + u'username': self.auth_user['id'], + u'tenantId': self.auth_user['tenant_id'] + } + } + } + self.assert_dict_equal(expected, json.loads(self.res.body)) + + @xmlify + def test_authn_xml(self): + url = "/tokens" + req = self.get_request('GET', url) + req.body = ' \ + ' % (self.auth_user['password'], + self.auth_user['id'], + self.auth_user['tenant_id']) + self.get_response() + self.status_ok() + + expected = """ + + + + + """ % (self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f"), + self.auth_token_id, + self.auth_user['tenant_id'], + self.auth_user['id'], + self.auth_user['tenant_id']) + self.assert_xml_strings_equal(expected, self.res.body) -- cgit From 878794491433f3b6f558c32d92c8d0f2a4e87bb1 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:29:15 -0500 Subject: Merging changes --- test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit From 7a38f7438caa979c9b1579a3ab17ce5ed10199d4 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 14:01:49 -0500 Subject: Merging changes --- keystone/logic/service.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/keystone/logic/service.py b/keystone/logic/service.py index 23f6ed8e..926fa4b6 100644 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -849,9 +849,6 @@ class IdentityService(object): ts.append(roles.RoleRef(droleRef.id, droleRef.role_id, droleRef.tenant_id)) user = auth.User(duser.id, duser.tenant_id, None, roles.RoleRefs(ts, [])) - return auth.AuthData(token, user) - user = auth.User(duser.id, duser.tenant_id, None) - return auth.ValidateData(token, user) def __validate_token(self, token_id, admin=True): -- cgit From 9a6f3d54e94d31f7be2b8ccfceeb8b835a89cddc Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 17:29:01 -0500 Subject: Changes on auth basic middleware component to return roles.Also changes on the application to return roles not tied to a tenant. --- examples/echo/echo/server.py | 4 ++++ keystone/auth_protocols/auth_token.py | 11 ++++++++++- keystone/logic/service.py | 6 +++++- 3 files changed, 19 insertions(+), 2 deletions(-) mode change 100644 => 100755 keystone/logic/service.py diff --git a/examples/echo/echo/server.py b/examples/echo/echo/server.py index 8c24aa8f..69919a88 100644 --- a/examples/echo/echo/server.py +++ b/examples/echo/echo/server.py @@ -31,6 +31,7 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'echo', '__init__.py')): # also use the local keystone KEYSTONE_TOPDIR = os.path.normpath(os.path.join(POSSIBLE_TOPDIR, + os.pardir, os.pardir)) if os.path.exists(os.path.join(KEYSTONE_TOPDIR, 'keystone', @@ -75,6 +76,9 @@ class EchoApp(object): print ' Tenant :', self.envr['HTTP_X_TENANT'] if 'HTTP_X_GROUP' in self.envr: print ' Group :', self.envr['HTTP_X_GROUP'] + if 'HTTP_X_ROLES' in self.envr: + print ' Roles :', self.envr['HTTP_X_ROLES'] + accept = self.envr.get("HTTP_ACCEPT", "application/json") if accept == "application/xml": diff --git a/keystone/auth_protocols/auth_token.py b/keystone/auth_protocols/auth_token.py index 1484bf58..e690b335 100644 --- a/keystone/auth_protocols/auth_token.py +++ b/keystone/auth_protocols/auth_token.py @@ -167,6 +167,8 @@ class AuthProtocol(object): self._decorate_request('X_USER', claims['user']) if 'group' in claims: self._decorate_request('X_GROUP', claims['group']) + if 'roles' in claims: + self._decorate_request('X_ROLES', claims['roles']) self.expanded = True #Send request downstream @@ -263,8 +265,15 @@ class AuthProtocol(object): token_info = json.loads(data) #TODO(Ziad): make this more robust #first_group = token_info['auth']['user']['groups']['group'][0] + roles =[] + role_refs =token_info["auth"]["user"]["roleRefs"] + for role_ref in role_refs: + roles.append(role_ref["roleId"]) + verified_claims = {'user': token_info['auth']['user']['username'], - 'tenant': token_info['auth']['user']['tenantId']} + 'tenant': token_info['auth']['user']['tenantId'], 'roles':roles} + + # TODO(Ziad): removed groups for now # ,'group': '%s/%s' % (first_group['id'], # first_group['tenantId'])} diff --git a/keystone/logic/service.py b/keystone/logic/service.py old mode 100644 new mode 100755 index 926fa4b6..e028705e --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -129,7 +129,7 @@ class IdentityService(object): ## GET Tenants with Pagination ## def get_tenants(self, admin_token, marker, limit, url): - self.__validate_token(admin_token) + (token, user) = self.__validate_token(admin_token, False) ts = [] dtenants = db_api.tenant_get_page(marker, limit) @@ -848,6 +848,10 @@ class IdentityService(object): for droleRef in droleRefs: ts.append(roles.RoleRef(droleRef.id, droleRef.role_id, droleRef.tenant_id)) + droleRefs = db_api.role_ref_get_all_global_roles(duser.id) + for droleRef in droleRefs: + ts.append(roles.RoleRef(droleRef.id, droleRef.role_id, + droleRef.tenant_id)) user = auth.User(duser.id, duser.tenant_id, None, roles.RoleRefs(ts, [])) return auth.ValidateData(token, user) -- cgit From 38b977efd20092c40dd93337dfc647082d37722d Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Thu, 2 Jun 2011 12:57:06 -0500 Subject: Changes to support getTenants call for user with admin privelage and regular user. --- keystone/db/sqlalchemy/api.py | 62 +++++++++++++++++++++++++++++++++++++++++-- keystone/logic/service.py | 50 +++++++++++++++++++++++----------- test/unit/test_tenants.py | 12 ++++----- 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py index f2dda017..5a60eef8 100644 --- a/keystone/db/sqlalchemy/api.py +++ b/keystone/db/sqlalchemy/api.py @@ -169,6 +169,65 @@ def tenant_get_all(session=None): session = get_session() return session.query(models.Tenant).all() +def tenants_for_user_get_page(user, marker, limit, session=None): + if not session: + session = get_session() + ura = aliased(models.UserRoleAssociation) + tenant = aliased(models.Tenant) + q1 = session.query(tenant).join((ura, ura.tenant_id == tenant.id)).\ + filter(ura.user_id == user.id) + q2 = session.query(tenant).filter(tenant.id == user.tenant_id) + q3 = q1.union(q2) + if marker: + return q3.filter("tenant.id>:marker").params(\ + marker='%s' % marker).order_by(\ + tenant.id.desc()).limit(limit).all() + else: + return q3.order_by(\ + tenant.id.desc()).limit(limit).all() + +def tenants_for_user_get_page_markers(user, marker, limit, session=None): + if not session: + session = get_session() + ura = aliased(models.UserRoleAssociation) + tenant = aliased(models.Tenant) + q1 = session.query(tenant).join((ura, ura.tenant_id == tenant.id)).\ + filter(ura.user_id == user.id) + q2 = session.query(tenant).filter(tenant.id == user.tenant_id) + q3 = q1.union(q2) + + first = q3.order_by(\ + tenant.id).first() + last = q3.order_by(\ + tenant.id.desc()).first() + if first is None: + return (None, None) + if marker is None: + marker = first.id + next = q3.filter(tenant.id > marker).order_by(\ + tenant.id).limit(limit).all() + prev = q3.filter(tenant.id > marker).order_by(\ + tenant.id.desc()).limit(int(limit)).all() + if len(next) == 0: + next = last + else: + for t in next: + next = t + if len(prev) == 0: + prev = first + else: + for t in prev: + prev = t + if prev.id == marker: + prev = None + else: + prev = prev.id + if next.id == last.id: + next = None + else: + next = next.id + return (prev, next) + def tenant_get_page(marker, limit, session=None): if not session: @@ -181,8 +240,7 @@ def tenant_get_page(marker, limit, session=None): else: return session.query(models.Tenant).order_by(\ models.Tenant.id.desc()).limit(limit).all() - - + def tenant_get_page_markers(marker, limit, session=None): if not session: session = get_session() diff --git a/keystone/logic/service.py b/keystone/logic/service.py index e028705e..2d88f182 100755 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -129,22 +129,40 @@ class IdentityService(object): ## GET Tenants with Pagination ## def get_tenants(self, admin_token, marker, limit, url): - (token, user) = self.__validate_token(admin_token, False) - - ts = [] - dtenants = db_api.tenant_get_page(marker, limit) - for dtenant in dtenants: - ts.append(tenants.Tenant(dtenant.id, - dtenant.desc, dtenant.enabled)) - prev, next = db_api.tenant_get_page_markers(marker, limit) - links = [] - if prev: - links.append(atom.Link('prev', "%s?'marker=%s&limit=%s'" \ - % (url, prev, limit))) - if next: - links.append(atom.Link('next', "%s?'marker=%s&limit=%s'" \ - % (url, next, limit))) - return tenants.Tenants(ts, links) + try: + (token, user) = self.__validate_token(admin_token) + # If Global admin return all tenants. + ts = [] + dtenants = db_api.tenant_get_page(marker, limit) + for dtenant in dtenants: + ts.append(tenants.Tenant(dtenant.id, + dtenant.desc, dtenant.enabled)) + prev, next = db_api.tenant_get_page_markers(marker, limit) + links = [] + if prev: + links.append(atom.Link('prev', "%s?'marker=%s&limit=%s'" \ + % (url, prev, limit))) + if next: + links.append(atom.Link('next', "%s?'marker=%s&limit=%s'" \ + % (url, next, limit))) + return tenants.Tenants(ts, links) + except fault.UnauthorizedFault: + #If not global admin ,return tenants specific to user. + (token, user) = self.__validate_token(admin_token, False) + ts = [] + dtenants = db_api.tenants_for_user_get_page(user, marker, limit) + for dtenant in dtenants: + ts.append(tenants.Tenant(dtenant.id, + dtenant.desc, dtenant.enabled)) + prev, next = db_api.tenants_for_user_get_page_markers(user, marker, limit) + links = [] + if prev: + links.append(atom.Link('prev', "%s?'marker=%s&limit=%s'" \ + % (url, prev, limit))) + if next: + links.append(atom.Link('next', "%s?'marker=%s&limit=%s'" \ + % (url, next, limit))) + return tenants.Tenants(ts, links) def get_tenant(self, admin_token, tenant_id): self.__validate_token(admin_token) diff --git a/test/unit/test_tenants.py b/test/unit/test_tenants.py index ec2a2569..bc639904 100644 --- a/test/unit/test_tenants.py +++ b/test/unit/test_tenants.py @@ -328,7 +328,7 @@ class CreateTenantTest(TenantTest): class GetTenantsTest(TenantTest): - def test_get_tenants(self): + def test_get_tenants_using_admin_token(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -342,7 +342,7 @@ class GetTenantsTest(TenantTest): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) - def test_get_tenants_xml(self): + def test_get_tenants_using_admin_token_xml(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -357,7 +357,7 @@ class GetTenantsTest(TenantTest): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) - def test_get_tenants_unauthorized_token(self): + def test_get_tenants_using_user_token(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -369,9 +369,9 @@ class GetTenantsTest(TenantTest): self.fail('Identity Fault') elif int(resp['status']) == 503: self.fail('Service Not Available') - self.assertEqual(401, int(resp['status'])) + self.assertEqual(200, int(resp['status'])) - def test_get_tenants_unauthorized_token_xml(self): + def test_get_tenants_using_user_token_xml(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -384,7 +384,7 @@ class GetTenantsTest(TenantTest): self.fail('Identity Fault') elif int(resp['status']) == 503: self.fail('Service Not Available') - self.assertEqual(401, int(resp['status'])) + self.assertEqual(200, int(resp['status'])) def test_get_tenants_exp_token(self): header = httplib2.Http(".cache") -- cgit From c13a04f1eec659d50059d158a2f7a3b0ce7ac916 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Thu, 2 Jun 2011 14:11:58 -0500 Subject: Adding roles as comma seperated values on a single header. --- examples/echo/echo/server.py | 4 ++-- keystone/auth_protocols/auth_token.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) mode change 100644 => 100755 keystone/auth_protocols/auth_token.py diff --git a/examples/echo/echo/server.py b/examples/echo/echo/server.py index 69919a88..bde8341a 100644 --- a/examples/echo/echo/server.py +++ b/examples/echo/echo/server.py @@ -76,8 +76,8 @@ class EchoApp(object): print ' Tenant :', self.envr['HTTP_X_TENANT'] if 'HTTP_X_GROUP' in self.envr: print ' Group :', self.envr['HTTP_X_GROUP'] - if 'HTTP_X_ROLES' in self.envr: - print ' Roles :', self.envr['HTTP_X_ROLES'] + if 'HTTP_X_ROLE' in self.envr: + print ' Roles :', self.envr['HTTP_X_ROLE'] accept = self.envr.get("HTTP_ACCEPT", "application/json") diff --git a/keystone/auth_protocols/auth_token.py b/keystone/auth_protocols/auth_token.py old mode 100644 new mode 100755 index e690b335..95d8fa2a --- a/keystone/auth_protocols/auth_token.py +++ b/keystone/auth_protocols/auth_token.py @@ -167,8 +167,14 @@ class AuthProtocol(object): self._decorate_request('X_USER', claims['user']) if 'group' in claims: self._decorate_request('X_GROUP', claims['group']) - if 'roles' in claims: - self._decorate_request('X_ROLES', claims['roles']) + if 'roles' in claims and len(claims['roles']) > 0: + if claims['roles'] != None: + roles = '' + for role in claims['roles']: + if len(roles) > 0: + roles += ',' + roles += role + self._decorate_request('X_ROLE', roles) self.expanded = True #Send request downstream @@ -267,8 +273,9 @@ class AuthProtocol(object): #first_group = token_info['auth']['user']['groups']['group'][0] roles =[] role_refs =token_info["auth"]["user"]["roleRefs"] - for role_ref in role_refs: - roles.append(role_ref["roleId"]) + if role_refs != None: + for role_ref in role_refs: + roles.append(role_ref["roleId"]) verified_claims = {'user': token_info['auth']['user']['username'], 'tenant': token_info['auth']['user']['tenantId'], 'roles':roles} -- cgit From 70d0933386056601d191983df1ac9042fe0b8b02 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Thu, 2 Jun 2011 14:28:41 -0500 Subject: Removing remerged comments. --- keystone/logic/service.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/keystone/logic/service.py b/keystone/logic/service.py index 2d88f182..43b45e12 100755 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -848,18 +848,6 @@ class IdentityService(object): """return ValidateData object for a token/user pair""" token = auth.Token(dtoken.expires, dtoken.token_id, dtoken.tenant_id) - - """gs = [] - for ug in duser.groups: - dgroup = db_api.group_get(ug.group_id) - if dtoken.tenant_id: - if dgroup.tenant_id == dtoken.tenant_id: - gs.append(auth.Group(dgroup.id, dgroup.tenant_id)) - else: - if dgroup.tenant_id == None: - gs.append(auth.Group(dgroup.id)) - user = auth.User(duser.id, dtoken.tenant_id, gs) - """ ts=[] if dtoken.tenant_id: droleRefs = db_api.role_ref_get_all_tenant_roles(duser.id, dtoken.tenant_id) -- cgit