diff --git a/.gitignore b/.gitignore index 9f14a9f52..90e9b8a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ ChangeLog build dist python_keystoneclient.egg-info +fiwareclient.egg-info keystoneclient/versioninfo doc/source/api # Development environment files diff --git a/README.rst b/README.rst index 72d85ee54..8698d5363 100644 --- a/README.rst +++ b/README.rst @@ -1,202 +1,9 @@ -Python bindings to the OpenStack Identity API (Keystone) -======================================================== +Python bindings to the FIWARE-IdM modified Keystone +=================================================== -This is a client for the OpenStack Identity API, implemented by Keystone. -There's a Python API (the ``keystoneclient`` module), and a command-line script -(``keystone``). +This is an extended client for the OpenStack Identity API, implemented by Keystone. It adds python bindings to the extra functionality added to Keystone through extensions implemented at `GING Keystone `_. The CLI part of the client is not binded, no work is done in that regard as is not a project-requirement at the time of this writting and because the OpenStack keystoneclient CLI is deprecated. If requested could be developed in the future, although probably as a contribution to `OpenStack Client `_. -Development takes place via the usual OpenStack processes as outlined in the -`developer guide `_. The master -repository is in `Git `_. +The master repository is on `GitHub `_. -This code is a fork of Rackspace's python-novaclient which is in turn a fork of -`Jacobian's python-cloudservers -`_. ``python-keystoneclient`` -is licensed under the Apache License like the rest of OpenStack. +This code is a fork of `OpenStack KeystoneClient `_. The future goal is to contribute back all the bindings for the extensions that get accepted in Keystone from the complementary project `GING Keystone `_. -.. contents:: Contents: - :local: - -Python API ----------- - -By way of a quick-start:: - - # use v2.0 auth with http://example.com:5000/v2.0 - >>> from keystoneclient.v2_0 import client - >>> keystone = client.Client(username=USERNAME, password=PASSWORD, tenant_name=TENANT, auth_url=AUTH_URL) - >>> keystone.tenants.list() - >>> tenant = keystone.tenants.create(tenant_name="test", description="My new tenant!", enabled=True) - >>> tenant.delete() - - -Command-line API ----------------- - -Installing this package gets you a shell command, ``keystone``, that you can -use to interact with OpenStack's Identity API. - -You'll need to provide your OpenStack tenant, username and password. You can do -this with the ``--os-tenant-name``, ``--os-username`` and ``--os-password`` -params, but it's easier to just set them as environment variables:: - - export OS_TENANT_NAME=project - export OS_USERNAME=user - export OS_PASSWORD=pass - -You will also need to define the authentication url with ``--os-auth-url`` and -the version of the API with ``--os-identity-api-version``. Or set them as an -environment variables as well:: - - export OS_AUTH_URL=http://example.com:5000/v2.0 - export OS_IDENTITY_API_VERSION=2.0 - -Alternatively, to bypass username/password authentication, you can provide a -pre-established token. In Keystone, this approach is necessary to bootstrap the -service with an administrative user, tenant & role (to do so, provide the -client with the value of your ``admin_token`` defined in ``keystone.conf`` in -addition to the URL of your admin API deployment, typically on port 35357):: - - export OS_SERVICE_TOKEN=thequickbrownfox-jumpsover-thelazydog - export OS_SERVICE_ENDPOINT=http://example.com:35357/v2.0 - -Since the Identity service can return multiple regions in the service catalog, -you can specify the one you want with ``--os-region-name`` (or ``export -OS_REGION_NAME``):: - - export OS_REGION_NAME=north - -.. WARNING:: - - If a region is not specified and multiple regions are returned by the - Identity service, the client may not access the same region consistently. - -If you need to connect to a server that is TLS-enabled (the auth URL begins -with 'https') and it uses a certificate from a private CA or a self-signed -certificate you will need to specify the path to an appropriate CA certificate -to use to validate the server certificate with ``--os-cacert`` or an -environment variable:: - - export OS_CACERT=/etc/ssl/my-root-cert.pem - -Certificate verification can be turned off using ``--insecure``. This should -be used with caution. - -You'll find complete documentation on the shell by running ``keystone help``:: - - usage: keystone [--version] [--timeout ] - [--os-username ] - [--os-password ] - [--os-tenant-name ] - [--os-tenant-id ] [--os-auth-url ] - [--os-region-name ] - [--os-identity-api-version ] - [--os-token ] - [--os-endpoint ] - [--os-cacert ] [--insecure] - [--os-cert ] [--os-key ] [--os-cache] - [--force-new-token] [--stale-duration ] - ... - - Command-line interface to the OpenStack Identity API. - - Positional arguments: - - catalog - ec2-credentials-create - Create EC2-compatible credentials for user per tenant - ec2-credentials-delete - Delete EC2-compatible credentials - ec2-credentials-get - Display EC2-compatible credentials - ec2-credentials-list - List EC2-compatible credentials for a user - endpoint-create Create a new endpoint associated with a service - endpoint-delete Delete a service endpoint - endpoint-get - endpoint-list List configured service endpoints - password-update Update own password - role-create Create new role - role-delete Delete role - role-get Display role details - role-list List all roles - service-create Add service to Service Catalog - service-delete Delete service from Service Catalog - service-get Display service from Service Catalog - service-list List all services in Service Catalog - tenant-create Create new tenant - tenant-delete Delete tenant - tenant-get Display tenant details - tenant-list List all tenants - tenant-update Update tenant name, description, enabled status - token-get - user-create Create new user - user-delete Delete user - user-get Display user details. - user-list List users - user-password-update - Update user password - user-role-add Add role to user - user-role-list List roles granted to a user - user-role-remove Remove role from user - user-update Update user's name, email, and enabled status - discover Discover Keystone servers, supported API versions and - extensions. - bootstrap Grants a new role to a new user on a new tenant, after - creating each. - bash-completion Prints all of the commands and options to stdout. - help Display help about this program or one of its - subcommands. - - Optional arguments: - --version Shows the client version and exits - --timeout Set request timeout (in seconds) - --os-username - Name used for authentication with the OpenStack - Identity service. Defaults to env[OS_USERNAME] - --os-password - Password used for authentication with the OpenStack - Identity service. Defaults to env[OS_PASSWORD] - --os-tenant-name - Tenant to request authorization on. Defaults to - env[OS_TENANT_NAME] - --os-tenant-id - Tenant to request authorization on. Defaults to - env[OS_TENANT_ID] - --os-auth-url - Specify the Identity endpoint to use for - authentication. Defaults to env[OS_AUTH_URL] - --os-region-name - Defaults to env[OS_REGION_NAME] - --os-identity-api-version - Defaults to env[OS_IDENTITY_API_VERSION] or 2.0 - --os-token - Specify an existing token to use instead of retrieving - one via authentication (e.g. with username & - password). Defaults to env[OS_SERVICE_TOKEN] - --os-endpoint - Specify an endpoint to use instead of retrieving one - from the service catalog (via authentication). - Defaults to env[OS_SERVICE_ENDPOINT] - --os-cacert - Specify a CA bundle file to use in verifying a TLS - (https) server certificate. Defaults to env[OS_CACERT] - --insecure Explicitly allow keystoneclient to perform "insecure" - TLS (https) requests. The server's certificate will - not be verified against any certificate authorities. - This option should be used with caution. - --os-cert - Defaults to env[OS_CERT] - --os-key Defaults to env[OS_KEY] - --os-cache Use the auth token cache. Defaults to env[OS_CACHE] - --force-new-token If the keyring is available and in use, token will - always be stored and fetched from the keyring until - the token has expired. Use this option to request a - new token and replace the existing one in the keyring. - --stale-duration - Stale duration (in seconds) used to determine whether - a token has expired when retrieving it from keyring. - This is useful in mitigating process or network - delays. Default is 30 seconds. - - See "keystone help COMMAND" for help on a specific command. diff --git a/integration_test_oauth2.py b/integration_test_oauth2.py new file mode 100644 index 000000000..0eb4af42a --- /dev/null +++ b/integration_test_oauth2.py @@ -0,0 +1,40 @@ +# TODO(garcianavalon) make proper integration tests +from keystoneclient.v3 import client +from keystoneclient import session +from keystoneclient.v3.contrib.oauth2 import auth +url = 'http://127.0.0.1:5000/v3' +#keystone = client.Client(token='ADMIN',endpoint=url) + +keystone=client.Client(username='admin', password='secrete', + project_name='demo', auth_url=url) + +redirect_uri='https://testuri.com' +scope='all_info' +scopes=[scope] + +#create a consumer +consumer = keystone.oauth2.consumers.create(client_type='confidential', + redirect_uris=[redirect_uri], + grant_type='authorization_code', + scopes=scopes) +consumer_id = consumer.id +consumer_secret = consumer.secret + +#store credentials +keystone.oauth2.authorization_codes.request_authorization(consumer=consumer_id, + redirect_uri=redirect_uri, + scope=scope) +#grant authorization +authorization_code = keystone.oauth2.authorization_codes.authorize( + consumer=consumer_id, + scopes=scopes) +#get an access token +access_token = keystone.oauth2.access_tokens.create(consumer_id=consumer_id, + authorization_code=authorization_code.code, + redirect_uri=redirect_uri, + consumer_secret=consumer_secret) +# log in and get a keystone token +a = auth.OAuth2(url,access_token=access_token.access_token) +s = session.Session(auth=a) +keystone = client.Client(session=s) +keystone.users.list() diff --git a/integration_test_roles.py b/integration_test_roles.py new file mode 100644 index 000000000..d6e5aa6e6 --- /dev/null +++ b/integration_test_roles.py @@ -0,0 +1,106 @@ +from keystoneclient.v3 import client +from keystoneclient import session +from keystoneclient.v3.contrib.oauth2 import auth +url = 'http://127.0.0.1:5000/v3' +#keystone = client.Client(token='ADMIN',endpoint=url) + +keystone = client.Client(username='admin', password='secrete', + project_name='demo', auth_url=url) + +#Basic Role Actions: +if 0: + role1 = keystone.fiware_roles.roles.create(name='Role Test 1') + role1_id = role1.id + role2 = keystone.fiware_roles.roles.create(name='Role Test 2') + role2_id = role2.id + + print('\n Get role 1: \n') + print keystone.fiware_roles.roles.get(role1_id) + keystone.fiware_roles.roles.update(role1, name='Role Cool Test') + print('\n Roles list (after updating the name of role 1): \n') + print keystone.fiware_roles.roles.list() + print('\n Roles list (after deleting role 2): \n') + keystone.fiware_roles.roles.delete(role2_id) + print keystone.fiware_roles.roles.list() + + keystone.fiware_roles.roles.delete(role1_id) + +#Basic Permission Actions: +if 0: + permission1 = keystone.fiware_roles.permissions.create(name='Permission Test 1') + permission1_id = permission1.id + permission2 = keystone.fiware_roles.permissions.create(name='Permission Test 2') + permission2_id = permission2.id + + print('\n Get permission 1: \n') + print keystone.fiware_roles.permissions.get(permission1_id) + keystone.fiware_roles.permissions.update(permission1, name='Permission Cool Test') + print('\n Permission list (after updating the name of permission 1): \n') + print keystone.fiware_roles.permissions.list() + print('\n Permission list (after deleting permission 2): \n') + keystone.fiware_roles.permissions.delete(permission2_id) + print keystone.fiware_roles.permissions.list() + + keystone.fiware_roles.permissions.delete(permission1_id) + +#Roles-Permissions relation +if 0: + role1 = keystone.fiware_roles.roles.create(name='Role Test 1') + role1_id = role1.id + permission1 = keystone.fiware_roles.permissions.create(name='Permission Test 1') + permission1_id = permission1.id + permission2 = keystone.fiware_roles.permissions.create(name='Permission Test 2') + permission2_id = permission2.id + + keystone.fiware_roles.permissions.add_to_role(role1, permission1) + print('\n Permissions that belong to role 1: \n') + print keystone.fiware_roles.permissions.list(role=role1) + + keystone.fiware_roles.permissions.add_to_role(role1, permission2) + print('\n Permissions that belong to role 1 (after adding permission 1 and 2): \n') + print keystone.fiware_roles.permissions.list(role=role1) + + keystone.fiware_roles.permissions.remove_role(role1, permission1) + print('\n Permissions that belong to role 1 (after removing permission 1): \n') + print keystone.fiware_roles.permissions.list(role=role1) + + keystone.fiware_roles.permissions.add_to_role(role1, permission1) + keystone.fiware_roles.permissions.delete(permission1) + print('\n Permissions that belong to role 1 (after deleting permission 1): \n') + print keystone.fiware_roles.permissions.list(role=role1) + + keystone.fiware_roles.roles.delete(role1_id) + keystone.fiware_roles.permissions.delete(permission2_id) + +#Roles-Users relation +if 0: + role1 = keystone.fiware_roles.roles.create(name='Role Test 1') + role1_id = role1.id + role2 = keystone.fiware_roles.roles.create(name='Role Test 2') + role2_id = role2.id + + name = 'testitester' + organization = keystone.projects.create(name=name, domain='default') + user = keystone.users.create(name=name, password='user', project=organization) + + keystone.fiware_roles.roles.add_to_user(role1, user, organization) + print('\n Roles that belong to user: \n') + print keystone.fiware_roles.roles.list(user=user) + + keystone.fiware_roles.roles.add_to_user(role2, user, organization) + print('\n Roles that belong to user (after adding role 1 and 2 to user): \n') + print keystone.fiware_roles.roles.list(user=user) + + keystone.fiware_roles.roles.remove_role(role1, user, organization) + print('\n Roles that belong to user (after removing role 1): \n') + print keystone.fiware_roles.roles.list(user=user) + + keystone.fiware_roles.roles.add_to_user(role1, user) + keystone.fiware_roles.roles.delete(role1) + print('\n Roles that belong to user (after deleting role 1): \n') + print keystone.fiware_roles.roles.list(user=user) + + + keystone.fiware_roles.roles.delete(role2) + keystone.projects.delete(organization) + keystone.users.delete(user) diff --git a/integration_test_two_factor.py b/integration_test_two_factor.py new file mode 100644 index 000000000..623b3dff9 --- /dev/null +++ b/integration_test_two_factor.py @@ -0,0 +1,107 @@ +from keystoneclient.v3 import client +from keystoneclient import session +from keystoneclient.v3.contrib.two_factor import auth +import pyotp + +url = 'http://127.0.0.1:5000/v3' + +def fiwareclient(session=None, request=None):# TODO(garcianavalon) use this + """Encapsulates all the logic for communicating with the modified keystone server. + + The IdM has its own admin account in the keystone server, and uses it to perform + operations like create users, projects, etc. when there is no user with admin rights + (for example, when user registration) to overcome the Keystone limitations. + + Also adds the methods to operate with the OAuth2.0 extension. + """ + # TODO(garcianavalon) find a way to integrate this with the existng keystone api + # TODO(garcianavalon)caching and efficiency with the client object. + if not session: + session = two_factor_session(user='idm_user', password='idm') + keystone = client.Client(session=session) + return keystone + +def two_factor_session(user, password, verification_code=None, device_data=None, domain_id='default'): + auth_object = auth.TwoFactor(auth_url=url, + user_id=user, + password=password, + user_domain_id=domain_id, + device_data=device_data, + verification_code=verification_code) + return session.Session(auth=auth_object) + +def enable_two_factor(keystone, user): + key = keystone.two_factor.keys.generate_new_key(user=user.id, security_question='Who?', security_answer='Me!') + print "Created key for example_user: ", key.two_factor_key + return key + +def remember_device(keystone, **kwargs): + device_data = keystone.two_factor.keys.remember_device(**kwargs) + print "Remembering device for example_user: ", device_data.device_id + return device_data + +def authenticate(keystone, user, password, key=None, use_device_cookie=False, use_two_factor=True): + if use_two_factor: + if keystone.two_factor.keys.check_activated_two_factor(user_id=user.id): + print "Two factor is enabled for example_user!" + + if use_device_cookie: + device_data = remember_device(keystone=keystone, user_name=user.name, domain_id=user.domain_id) + keystone2 = fiwareclient(session=two_factor_session(user=user.id, + password=password, + device_data={'device_id': device_data.device_id, + 'device_token': device_data.device_token})) + used_device_message = ' (remembering device)' + else: + code = pyotp.TOTP(key.two_factor_key).now() + print code + keystone2 = fiwareclient(session=two_factor_session(user=user.id, + password=password, + verification_code=code)) + used_device_message = '' + try: + keystone2.users.get(user.id) + print "Auth with two factor{used_device} worked".format(used_device=used_device_message) + except: + print "Auth with two factor{used_device} didn't work".format(used_device=used_device_message) + else: + print "Two factor is disabled for exampleuser!" + else: + keystone3 = fiwareclient(session=two_factor_session(user=user.id, + password=password)) + try: + keystone3.users.get(user.id) + print "Auth without two factor worked" + except: + print "Auth without two factor didn't work" + +def disable_two_factor(keystone, user): + keystone.two_factor.keys.deactivate_two_factor(user=user.id) + print "Disabling two factor..." + + if not keystone.two_factor.keys.check_activated_two_factor(user_id=user.id): + print "Two factor is disabled for exampleuser!" + +def main(): + keystone = fiwareclient() + + # Create example user + project = keystone.projects.create(name="example_project", domain="default") + role = keystone.roles.create(name="example_role") + user = keystone.users.create(name="example_user", password="example_user", default_project=project) + keystone.roles.grant(role=role, user=user, project=project) + + # Run tests + key = enable_two_factor(keystone, user) + authenticate(keystone, user, password="example_user", key=key, use_two_factor=True) + authenticate(keystone, user, password="example_user", key=key, use_two_factor=True, use_device_cookie=True) + disable_two_factor(keystone, user) + authenticate(keystone, user, password="example_user", use_two_factor=False) + + # Delete example user + keystone.projects.delete(project) + keystone.roles.delete(role) + keystone.users.delete(user) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/integration_test_user_registration.py b/integration_test_user_registration.py new file mode 100644 index 000000000..e6e07bf5d --- /dev/null +++ b/integration_test_user_registration.py @@ -0,0 +1,93 @@ +from keystoneclient.v3 import client +from keystoneclient import session +from keystoneclient.v3.contrib.oauth2 import auth +from keystoneclient.auth.identity import v3 + +def fiwareclient(session=None, request=None):# TODO(garcianavalon) use this + """Encapsulates all the logic for communicating with the modified keystone server. + + The IdM has its own admin account in the keystone server, and uses it to perform + operations like create users, projects, etc. when there is no user with admin rights + (for example, when user registration) to overcome the Keystone limitations. + + Also adds the methods to operate with the OAuth2.0 extension. + """ + # TODO(garcianavalon) find a way to integrate this with the existng keystone api + # TODO(garcianavalon)caching and efficiency with the client object. + if not session: + session = _password_session() + keystone = client.Client(session=session) + return keystone + +def _password_session(): + auth = v3.Password(auth_url="http://localhost:5000/v3", + username='idm', + password='idm', + project_name='idm', + user_domain_id='default', + project_domain_id='default') + return session.Session(auth=auth) + +keystone = fiwareclient() + +#Registrer User: +if 0: + new_user = keystone.user_registration.users.register_user(name='Test User', + domain='default', + password='test', + email='user@test.com') + + print ('\nUser activated? ') + print new_user.enabled + +#Activate User +if 0: + activated_user = keystone.user_registration.users.activate_user(new_user.id, new_user.activation_key) + + print ('\nUser activated? ') + print activated_user.enabled + +#Forgot Password +if 0: + token = keystone.user_registration.token.get_reset_token(new_user.id) + print token + + user = keystone.user_registration.users.reset_password(new_user, token.id, 'newpassword') + print user + +#Delete created user and associated project +if 0: + keystone.users.delete(new_user.id) + project = keystone.projects.find(name=new_user.name) + keystone.projects.delete(project.id) + +#New Activation Key +# The new activation key does not work in these tests. +if 1: + user_reset = keystone.user_registration.users.register_user(name='Test User', + domain='default', + password='test', + email='user@test.com') + project = keystone.projects.find(name=user_reset.name) + print user_reset.id + print project.id + print ('\nUser activated? ') + print user_reset.enabled + + print user_reset.activation_key + + new_activation_key = keystone.user_registration.activation_key.new_activation_key(user_reset.id) + print new_activation_key.id + print user_reset.activation_key + + activated_user = keystone.user_registration.users.activate_user(user_reset.id, new_activation_key.id) + print activated_user + + print ('\nUser activated? ') + print user_reset.enabled + + keystone.users.delete(user_reset.id) + # project = keystone.projects.find(name=user_reset.name) + keystone.projects.delete(project.id) + + diff --git a/keystoneclient/__init__.py b/keystoneclient/__init__.py index 08545c573..35a5e6a7a 100644 --- a/keystoneclient/__init__.py +++ b/keystoneclient/__init__.py @@ -40,7 +40,7 @@ from keystoneclient import v3 -__version__ = pbr.version.VersionInfo('python-keystoneclient').version_string() +#__version__ = pbr.version.VersionInfo('python-keystoneclient').version_string() __all__ = [ # Modules diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 81d5e26ab..17206f7d9 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -368,7 +368,6 @@ def put(self, **kwargs): @filter_kwargs def update(self, **kwargs): url = self.build_url(dict_args_in_out=kwargs) - return self._update( url, {self.key: kwargs}, diff --git a/keystoneclient/openstack/common/_i18n.py b/keystoneclient/openstack/common/_i18n.py index 52a5e8478..8ec37733c 100644 --- a/keystoneclient/openstack/common/_i18n.py +++ b/keystoneclient/openstack/common/_i18n.py @@ -16,7 +16,7 @@ """ -import oslo.i18n +import oslo_i18n # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the @@ -24,7 +24,7 @@ # repository. It is OK to have more than one translation function # using the same domain, since there will still only be one message # catalog. -_translators = oslo.i18n.TranslatorFactory(domain='keystoneclient') +_translators = oslo_i18n.TranslatorFactory(domain='keystoneclient') # The primary translation function using the well-known name "_" _ = _translators.primary diff --git a/keystoneclient/openstack/common/apiclient/base.py b/keystoneclient/openstack/common/apiclient/base.py index 72d7999d1..fdfca59ea 100644 --- a/keystoneclient/openstack/common/apiclient/base.py +++ b/keystoneclient/openstack/common/apiclient/base.py @@ -26,7 +26,7 @@ import abc import copy -from oslo.utils import strutils +from oslo_utils import strutils import six from six.moves.urllib import parse diff --git a/keystoneclient/openstack/common/apiclient/client.py b/keystoneclient/openstack/common/apiclient/client.py index dd560aba5..1b4e14aad 100644 --- a/keystoneclient/openstack/common/apiclient/client.py +++ b/keystoneclient/openstack/common/apiclient/client.py @@ -34,8 +34,8 @@ except ImportError: import json -from oslo.utils import encodeutils -from oslo.utils import importutils +from oslo_utils import encodeutils +from oslo_utils import importutils import requests from keystoneclient.openstack.common._i18n import _ diff --git a/keystoneclient/openstack/common/apiclient/utils.py b/keystoneclient/openstack/common/apiclient/utils.py index 6aa2975aa..a45462134 100644 --- a/keystoneclient/openstack/common/apiclient/utils.py +++ b/keystoneclient/openstack/common/apiclient/utils.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.utils import encodeutils +from oslo_utils import encodeutils import six from keystoneclient.openstack.common._i18n import _ diff --git a/keystoneclient/openstack/common/memorycache.py b/keystoneclient/openstack/common/memorycache.py index 4826865a2..e7e105c73 100644 --- a/keystoneclient/openstack/common/memorycache.py +++ b/keystoneclient/openstack/common/memorycache.py @@ -16,8 +16,8 @@ """Super simple fake memcache client.""" -from oslo.config import cfg -from oslo.utils import timeutils +from oslo_config import cfg +from oslo_utils import timeutils memcache_opts = [ cfg.ListOpt('memcached_servers', diff --git a/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py index b226e32bb..06351783e 100644 --- a/keystoneclient/tests/unit/client_fixtures.py +++ b/keystoneclient/tests/unit/client_fixtures.py @@ -544,12 +544,12 @@ class HackingCode(fixtures.Fixture): oslo_namespace_imports = { 'code': """ - import oslo.utils import oslo_utils - import oslo.utils.encodeutils + import oslo_utils + import oslo_utils.encodeutils import oslo_utils.encodeutils from oslo import utils - from oslo.utils import encodeutils + from oslo_utils import encodeutils from oslo_utils import encodeutils import oslo.serialization diff --git a/keystoneclient/tests/unit/v3/test_endpoint_groups_filters.py b/keystoneclient/tests/unit/v3/test_endpoint_groups_filters.py new file mode 100644 index 000000000..eb48c03bc --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_endpoint_groups_filters.py @@ -0,0 +1,39 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib import endpoint_filter + + +EXTENSION_PATH = 'OS-EP-FILTER' + +class EndpointGroupsFiltersTests(utils.TestCase, utils.CrudTests): + + + def setUp(self): + super(EndpointGroupsFiltersTests, self).setUp() + self.key = 'endpoint_group' + self.collection_key = 'endpoint_groups' + self.model = endpoint_filter.EndpointGroupFilter + self.manager = self.client.endpoint_groups + self.path_prefix = EXTENSION_PATH + + def new_ref(self, **kwargs): + kwargs = super(EndpointGroupsFiltersTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('filters', {}) + return kwargs \ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/test_fiware_roles.py b/keystoneclient/tests/unit/v3/test_fiware_roles.py new file mode 100644 index 000000000..e27e3c19b --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_fiware_roles.py @@ -0,0 +1,492 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 uuid + +from keystoneclient import exceptions +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.fiware_roles import allowed +from keystoneclient.v3.contrib.fiware_roles import roles +from keystoneclient.v3.contrib.fiware_roles import role_assignments +from keystoneclient.v3.contrib.fiware_roles import permissions + + +EXTENSION_PATH = 'OS-ROLES' + +class RoleTests(utils.TestCase, utils.CrudTests): + + + def setUp(self): + super(RoleTests, self).setUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.fiware_roles.roles + self.path_prefix = EXTENSION_PATH + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('is_internal', False) + return kwargs + + # ROLES-USER + def test_add_role_to_user(self): + user_id = uuid.uuid4().hex + role_ref = self.new_ref() + organization_id = uuid.uuid4().hex + app_id = uuid.uuid4().hex + self.stub_url('PUT', + [self.path_prefix, 'users', user_id, + 'organizations', organization_id, + 'applications', app_id, + self.collection_key, role_ref['id']], + status_code=204) + self.manager.add_to_user(role=role_ref['id'], + user=user_id, + organization=organization_id, + application=app_id) + + + def test_remove_role_from_user(self): + user_id = uuid.uuid4().hex + role_ref = self.new_ref() + organization_id = uuid.uuid4().hex + app_id = uuid.uuid4().hex + self.stub_url('DELETE', + [self.path_prefix, 'users', user_id, + 'organizations', organization_id, + 'applications', app_id, + self.collection_key, role_ref['id']], + status_code=204) + + self.manager.remove_from_user(role=role_ref['id'], + user=user_id, + organization=organization_id, + application=app_id) + + + + + + # ROLES-ORGANIZATIONS + def test_add_role_to_organization(self): + organization_id = uuid.uuid4().hex + role_ref = self.new_ref() + app_id = uuid.uuid4().hex + self.stub_url('PUT', + [self.path_prefix, 'organizations', organization_id, + 'applications', app_id, + self.collection_key, role_ref['id']], + status_code=204) + self.manager.add_to_organization(role=role_ref['id'], + organization=organization_id, + application=app_id) + + + def test_remove_role_from_organization(self): + organization_id = uuid.uuid4().hex + role_ref = self.new_ref() + app_id = uuid.uuid4().hex + self.stub_url('DELETE', + [self.path_prefix, 'organizations', organization_id, + 'applications', app_id, + self.collection_key, role_ref['id']], + status_code=204) + + self.manager.remove_from_organization(role=role_ref['id'], + organization=organization_id, + application=app_id) + + +class AllowedTests(utils.TestCase): + + def setUp(self): + super(AllowedTests, self).setUp() + self.manager = self.client.fiware_roles.allowed + self.path_prefix = EXTENSION_PATH + + def test_list_user_allowed_roles_to_assign(self): + user_id = uuid.uuid4().hex + organization_id = uuid.uuid4().hex + allowed_roles_ref = { + 'allowed_roles': { + 'some_application': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + } + self.stub_url('GET', + [self.path_prefix, 'users', user_id, + 'organizations', organization_id, + 'roles/allowed'], + json=allowed_roles_ref) + allowed_roles = self.manager.list_user_allowed_roles_to_assign( + user=user_id, organization=organization_id) + + self.assertEqual(2, len(allowed_roles['some_application'])) + + def test_list_organization_allowed_roles_to_assign(self): + organization_id = uuid.uuid4().hex + allowed_roles_ref = { + 'allowed_roles': { + 'some_application': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + } + self.stub_url('GET', + [self.path_prefix, 'organizations', organization_id, + 'roles/allowed'], + json=allowed_roles_ref) + allowed_roles = self.manager.list_organization_allowed_roles_to_assign( + organization=organization_id) + + self.assertEqual(2, len(allowed_roles['some_application'])) + + def test_list_user_allowed_applications_to_manage(self): + user_id = uuid.uuid4().hex + organization_id = uuid.uuid4().hex + allowed_applications_ref = { + 'allowed_applications': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + self.stub_url('GET', + [self.path_prefix, 'users', user_id, + 'organizations', organization_id, + 'applications/allowed'], + json=allowed_applications_ref) + allowed_applications = self.manager.list_user_allowed_applications_to_manage( + user=user_id, organization=organization_id) + + self.assertEqual(2, len(allowed_applications)) + + def test_list_organization_allowed_applications_to_manage(self): + organization_id = uuid.uuid4().hex + allowed_applications_ref = { + 'allowed_applications': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + self.stub_url('GET', + [self.path_prefix, + 'organizations', organization_id, + 'applications/allowed'], + json=allowed_applications_ref) + allowed_applications = \ + self.manager.list_organization_allowed_applications_to_manage( + organization=organization_id) + + self.assertEqual(2, len(allowed_applications)) + + def test_list_user_allowed_applications_to_manage_roles(self): + user_id = uuid.uuid4().hex + organization_id = uuid.uuid4().hex + allowed_applications_ref = { + 'allowed_applications': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + self.stub_url('GET', + [self.path_prefix, 'users', user_id, + 'organizations', organization_id, + 'applications/allowed_roles'], + json=allowed_applications_ref) + allowed_applications = self.manager.list_user_allowed_applications_to_manage_roles( + user=user_id, organization=organization_id) + + self.assertEqual(2, len(allowed_applications)) + + def test_list_organization_allowed_applications_to_manage_roles(self): + organization_id = uuid.uuid4().hex + allowed_applications_ref = { + 'allowed_applications': [ + uuid.uuid4().hex, + uuid.uuid4().hex, + ] + } + self.stub_url('GET', + [self.path_prefix, + 'organizations', organization_id, + 'applications/allowed_roles'], + json=allowed_applications_ref) + allowed_applications = \ + self.manager.list_organization_allowed_applications_to_manage_roles( + organization=organization_id) + + self.assertEqual(2, len(allowed_applications)) + + +class RoleAssignmentsTests(utils.TestCase, utils.CrudTests): + + def setUp(self): + super(RoleAssignmentsTests, self).setUp() + self.key = 'role_assignment' + self.collection_key = 'role_assignments' + self.model = role_assignments.RoleAssignment + self.manager = self.client.fiware_roles.role_assignments + self.USER_ASSIGNMENT_TEST_LIST = [{ + 'role_id': uuid.uuid4().hex, + 'organization_id': uuid.uuid4().hex, + 'user_id': uuid.uuid4().hex, + 'application_id': uuid.uuid4().hex, + }] + self.ORGANIZATION_ASSIGNMENT_TEST_LIST = [{ + 'role_id': uuid.uuid4().hex, + 'organization_id': uuid.uuid4().hex, + 'application_id': uuid.uuid4().hex, + }] + self.path_prefix = EXTENSION_PATH + + + def _assert_returned_list(self, ref_list, returned_list): + self.assertEqual(len(ref_list), len(returned_list)) + [self.assertIsInstance(r, self.model) for r in returned_list] + + + # ROLE-USER + def test_all_user_assignments_list(self): + ref_list = self.USER_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'users', self.collection_key], + entity=ref_list) + + returned_list = self.manager.list_user_role_assignments() + self._assert_returned_list(ref_list, returned_list) + + kwargs = {} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_organization_user_assignments(self): + ref_list = self.USER_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'users', self.collection_key, + '?organization_id=%s' % self.TEST_TENANT_ID], + entity=ref_list) + + returned_list = self.manager.list_user_role_assignments( + organization=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'organization_id': self.TEST_TENANT_ID} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_application_user_assignments(self): + ref_list = self.USER_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'users', self.collection_key, + '?application_id=%s' % self.TEST_DOMAIN_ID], + entity=ref_list) + + returned_list = self.manager.list_user_role_assignments( + application=self.TEST_DOMAIN_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'application_id': self.TEST_DOMAIN_ID} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_user_user_assignments(self): + ref_list = self.USER_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'users', self.collection_key, + '?user_id=%s' % self.TEST_USER_ID], + entity=ref_list) + + returned_list = self.manager.list_user_role_assignments( + user=self.TEST_USER_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'user_id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_user_and_organization_user_assignments(self): + ref_list = self.USER_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'users', self.collection_key, + '?organization_id=%s&user_id=%s' % + (self.TEST_TENANT_ID, self.TEST_USER_ID)], + entity=ref_list) + + returned_list = self.manager.list_user_role_assignments( + user=self.TEST_USER_ID, organization=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'organization_id': self.TEST_TENANT_ID, + 'user_id': self.TEST_USER_ID} + self.assertQueryStringContains(**kwargs) + + + #ROLE-ORGANIZATION + def test_all_organization_assignments_list(self): + ref_list = self.ORGANIZATION_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'organizations', self.collection_key], + entity=ref_list) + + returned_list = self.manager.list_organization_role_assignments() + self._assert_returned_list(ref_list, returned_list) + + kwargs = {} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_organization_organization_assignments(self): + ref_list = self.ORGANIZATION_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'organizations', self.collection_key, + '?organization_id=%s' % self.TEST_TENANT_ID], + entity=ref_list) + + returned_list = self.manager.list_organization_role_assignments( + organization=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'organization_id': self.TEST_TENANT_ID} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_application_organization_assignments(self): + ref_list = self.ORGANIZATION_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'organizations', self.collection_key, + '?application_id=%s' % self.TEST_DOMAIN_ID], + entity=ref_list) + + returned_list = self.manager.list_organization_role_assignments( + application=self.TEST_DOMAIN_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'application_id': self.TEST_DOMAIN_ID} + self.assertQueryStringContains(**kwargs) + + + def test_filter_by_application_and_organization_organization_assignments(self): + ref_list = self.ORGANIZATION_ASSIGNMENT_TEST_LIST + self.stub_entity('GET', + [self.path_prefix, 'organizations', self.collection_key, + '?organization_id=%s&application_id=%s' % + (self.TEST_TENANT_ID, self.TEST_DOMAIN_ID)], + entity=ref_list) + + returned_list = self.manager.list_organization_role_assignments( + application=self.TEST_DOMAIN_ID, organization=self.TEST_TENANT_ID) + self._assert_returned_list(ref_list, returned_list) + + kwargs = {'organization_id': self.TEST_TENANT_ID, + 'application_id': self.TEST_DOMAIN_ID} + self.assertQueryStringContains(**kwargs) + + + def test_create(self): + # Create not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.create) + + def test_update(self): + # Update not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_delete(self): + # Delete not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.delete) + + def test_get(self): + # Get not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.get) + + def test_find(self): + # Find not supported for role assignments + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) + + +class PermissionTests(utils.TestCase, utils.CrudTests): + + + def setUp(self): + super(PermissionTests, self).setUp() + self.key = 'permission' + self.collection_key = 'permissions' + self.model = permissions.Permission + self.manager = self.client.fiware_roles.permissions + self.path_prefix = EXTENSION_PATH + + def new_ref(self, **kwargs): + kwargs = super(PermissionTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('is_internal', False) + return kwargs + + def test_list_permissions_by_role(self): + role_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + parts=[self.path_prefix, 'roles', role_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(role=role_id) + + self.assertEqual(len(ref_list), len(returned_list)) + for item in returned_list: + self.assertIsInstance(item, self.model) + + def test_add_permission_to_role(self): + + permission_id = uuid.uuid4().hex + role_ref = self.new_ref() + self.stub_url('PUT', + [self.path_prefix, 'roles', role_ref['id'], + self.collection_key, permission_id], + status_code=204) + self.manager.add_to_role(role=role_ref['id'], permission=permission_id) + + # Test invalid args + self.assertRaises(exceptions.ValidationError, + self.manager.add_to_role, + role=role_ref['id'], + permission=None) + self.assertRaises(exceptions.ValidationError, + self.manager.add_to_role, + role=None, + permission=permission_id) + + def test_remove_permission_from_role(self): + + permission_id = uuid.uuid4().hex + role_ref = self.new_ref() + self.stub_url('DELETE', + [self.path_prefix, 'roles', role_ref['id'], + self.collection_key, permission_id], + status_code=204) + self.manager.remove_from_role(role=role_ref['id'], permission=permission_id) + + # Test invalid args + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_role, + role=role_ref['id'], + permission=None) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_role, + role=None, + permission=permission_id) + diff --git a/keystoneclient/tests/unit/v3/test_oauth2.py b/keystoneclient/tests/unit/v3/test_oauth2.py new file mode 100644 index 000000000..5bea0b8e1 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_oauth2.py @@ -0,0 +1,353 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 base64 +import urllib +import uuid + +from keystoneclient import exceptions +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.oauth2 import auth +from keystoneclient.v3.contrib.oauth2 import access_tokens +from keystoneclient.v3.contrib.oauth2 import authorization_codes +from keystoneclient.v3.contrib.oauth2 import consumers + + +class ConsumerTests(utils.TestCase, utils.CrudTests): + + DEFAULT_REDIRECT_URIS = ['https://uri.com'] + DEFAULT_SCOPES = ['all_info'] + DEFAULT_CLIENT_TYPE = 'confidential' + DEFAULT_GRANT_TYPE = 'authorization_code' + + def setUp(self): + super(ConsumerTests, self).setUp() + self.key = 'consumer' + self.collection_key = 'consumers' + self.model = consumers.Consumer + self.manager = self.client.oauth2.consumers + self.path_prefix = 'OS-OAUTH2' + + def new_ref(self, **kwargs): + kwargs = super(ConsumerTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('client_type', self.DEFAULT_CLIENT_TYPE) + kwargs.setdefault('redirect_uris', self.DEFAULT_REDIRECT_URIS) + kwargs.setdefault('scopes', self.DEFAULT_SCOPES) + kwargs.setdefault('grant_type', self.DEFAULT_GRANT_TYPE) + return kwargs + + def _consumer_data(self, name=None, description=None, + client_type=DEFAULT_CLIENT_TYPE, + redirect_uris=DEFAULT_REDIRECT_URIS, + grant_type=DEFAULT_GRANT_TYPE, + scopes=DEFAULT_SCOPES): + if not name: + name = uuid.uuid4().hex + data = { + 'consumer': { + 'name': name, + 'description': description, + 'client_type': client_type, + 'redirect_uris': redirect_uris, + 'grant_type': grant_type, + 'scopes': scopes + } + } + return data + + def _create_consumer(self, consumer_data): + self.stub_url('POST', + [self.path_prefix, self.collection_key], + status_code=201, json=consumer_data) + + consumer = self.manager.create(uuid.uuid4().hex) + return consumer + + def test_create_consumer_defaults(self): + consumer_data = self._consumer_data() + consumer = self._create_consumer(consumer_data) + self.assertEqual(self.DEFAULT_CLIENT_TYPE, consumer.client_type) + #self.assertIsNotNone(consumer.id) + self.assertIsNone(consumer.description) + + def test_list_consumers_by_user(self): + user_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity('GET', + parts=['users', user_id, + self.path_prefix, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(user=user_id) + + self.assertEqual(len(ref_list), len(returned_list)) + for item in returned_list: + self.assertIsInstance(item, self.model) + + + +class AuthorizationCodeTests(utils.TestCase): + + + def setUp(self): + super(AuthorizationCodeTests, self).setUp() + self.model = authorization_codes.AuthorizationCode + self.manager = self.client.oauth2.authorization_codes + self.path_prefix = 'OS-OAUTH2' + + def test_authorize(self): + stub_headers = { + 'Location':'https://foo.com/welcome_back?code=somerandomstring&state=xyz' + } + self.stub_url('POST', + [self.path_prefix, 'authorize',], + status_code=200, headers=stub_headers) + + consumer_id = uuid.uuid4().hex + scopes = [uuid.uuid4().hex] + + # Assert the manager is returning the expected data + + authorization_code = self.manager.authorize( + consumer=consumer_id, + scopes=scopes) + + self.assertIsNotNone(authorization_code.code) + self.assertIsNotNone(authorization_code.state) + self.assertIsNotNone(authorization_code.redirect_uri) + + # Assert that the request was sent in the expected structure + expected_body = { + 'user_auth': { + 'client_id':consumer_id, + 'scopes':scopes + } + } + self.assertRequestBodyIs(json=expected_body) + + def test_request_authorization(self): + scope = [uuid.uuid4().hex] + consumer_id = uuid.uuid4().hex + redirect_uri = uuid.uuid4().hex + state = uuid.uuid4().hex + + scope_string = ' '.join(scope) + # NOTE(garcianavalon) we use a list of tuples to ensure param order + # in the query string + stub_credentials = [ + ('response_type', 'code'), + ('client_id', consumer_id), + ('redirect_uri', redirect_uri), + ('scope', scope_string), + ('state', state) + ] + query_string = '?%s' %urllib.urlencode(stub_credentials) + + # NOTE(garcianavalon) this JSON emulates the provider response body + # but it might not be up-to-date because it's changing continuosly + # during development to adjust to different needs that keep appearing. + # Only take it as mean to test that the request_authorization call + # returns a dict, to know more about the response body check the Keystone + # OAuth2 Extension documentation + stub_body = { + 'data': { + 'consumer': { + 'id':consumer_id + }, + 'redirect_uri':redirect_uri, + 'requested_scopes':scope + } + } + + self.stub_url('GET', [self.path_prefix, 'authorize', query_string], + status_code=201, json=stub_body) + + # Assert the manager is returning a dict with the info from the server + response_body = self.manager.request_authorization( + consumer=consumer_id, + redirect_uri=redirect_uri, + scope=scope, + state=state) + + assert(isinstance(response_body, dict)) + + +class AccessTokenTests(utils.TestCase, utils.CrudTests): + + + def setUp(self): + super(AccessTokenTests, self).setUp() + self.manager = self.client.oauth2.access_tokens + self.model = access_tokens.AccessToken + self.path_prefix = 'OS-OAUTH2' + self.key = 'access_token' + self.collection_key = 'access_tokens' + + def test_create(self): + consumer_id = uuid.uuid4().hex + consumer_secret = uuid.uuid4().hex + redirect_uri = uuid.uuid4().hex + authorization_code = uuid.uuid4().hex + + stub_body = { + 'access_token': uuid.uuid4().hex, + 'refresh_token': uuid.uuid4().hex, + 'expires_in': 3600, + 'scope': ' '.join([uuid.uuid4().hex, uuid.uuid4().hex]), + 'token_type': 'Bearer' + } + self.stub_url('POST', [self.path_prefix, 'access_token'], + status_code=201, json=stub_body) + + # Assert that the manager creates an access token object + access_token = self.manager.create(consumer_id=consumer_id, + consumer_secret=consumer_secret, + authorization_code=authorization_code, + redirect_uri=redirect_uri) + + self.assertIsInstance(access_token, self.model) + self.assertIsNotNone(access_token.access_token) + self.assertIsNotNone(access_token.scope) + self.assertIsNotNone(access_token.expires_in) + + # Assert that the request was sent in the expected structure + expected_body = { + 'token_request' : { + 'grant_type':'authorization_code', + 'code': authorization_code, + 'redirect_uri':redirect_uri + } + } + self.assertRequestBodyIs(json=expected_body) + + auth_string = consumer_id + ':' + consumer_secret + expected_auth = 'Basic ' + base64.b64encode(auth_string) + self.assertRequestHeaderEqual('Authorization', expected_auth) + + def test_list_for_user(self): + user_id = uuid.uuid4().hex + access_tokens_ref = { + 'access_tokens': [ + { + 'id': uuid.uuid4().hex, + }, + { + 'id': uuid.uuid4().hex, + }, + ] + } + self.stub_url('GET', + ['users', user_id, + self.path_prefix, self.collection_key], + status_code=204, + json=access_tokens_ref) + + result = self.manager.list_for_user(user=user_id) + + self.assertEqual(len(access_tokens_ref['access_tokens']), + len(result)) + + def test_list_params(self): + # list not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.list) + + def test_list(self): + # list not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.list) + + def test_update(self): + # Update not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.update) + + def test_delete(self): + # Delete not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.delete) + + def test_get(self): + # Get not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.get) + + def test_find(self): + # Find not supported for access tokens + self.assertRaises(exceptions.MethodNotImplemented, self.manager.find) + +class AuthenticateWithOAuthTests(utils.TestCase): + + + def test_oauth_authenticate_success(self): + access_token = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to oauth2, and add its section. + oauth_token = client_fixtures.unscoped_token() + + oauth_token['methods'] = ["oauth2"] + oauth_token['oauth2'] = { + "access_token_id": access_token + } + self.stub_auth(json=oauth_token) + + a = auth.OAuth2(self.TEST_URL, access_token=access_token) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + OAUTH2_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["oauth2"], + "oauth2": { + "access_token_id": access_token + } + } + } + } + + self.assertRequestBodyIs(json=OAUTH2_REQUEST_BODY) + + def test_oauth_authenticate_scoped_success(self): + + access_token = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to oauth2, and add its section. + oauth_token = client_fixtures.project_scoped_token() + oauth_token['methods'] = ["oauth2"] + oauth_token['oauth2'] = { + "access_token_id": access_token + } + self.stub_auth(json=oauth_token) + + a = auth.OAuth2(self.TEST_URL, access_token=access_token) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + OAUTH2_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["oauth2"], + "oauth2": { + "access_token_id": access_token + } + } + } + } + + self.assertRequestBodyIs(json=OAUTH2_REQUEST_BODY) diff --git a/keystoneclient/tests/unit/v3/test_two_factor.py b/keystoneclient/tests/unit/v3/test_two_factor.py new file mode 100644 index 000000000..9654151bb --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_two_factor.py @@ -0,0 +1,270 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 uuid + +from keystoneclient import session +from keystoneclient.tests.unit.v3 import client_fixtures +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.two_factor import keys +from keystoneclient.v3.contrib.two_factor import auth + + +EXTENSION_PATH = 'OS-TWO-FACTOR' + +class UsersTests(utils.TestCase): + + def setUp(self): + super(UsersTests, self).setUp() + self.model = keys.Key + self.manager = self.client.two_factor.keys + self.path_prefix = EXTENSION_PATH + + def test_generate_new_key(self): + user_id = uuid.uuid4().hex + key_ref = { + 'two_factor_auth': { + 'security_answer': 'Sample answer', + 'security_question': 'Sample question', + 'two_factor_key': uuid.uuid4().hex, + 'user_id': user_id, + 'uri': 'otpauth://example' + } + } + self.stub_url('POST', + ['users/', user_id, self.path_prefix, '/two_factor_auth'], + json=key_ref, + status_code=201) + + self.manager.generate_new_key(user=user_id, + security_question='Sample question', + security_answer='Sample answer') + + def test_deactivate_two_factor(self): + user_id = uuid.uuid4().hex + + self.stub_url('DELETE', + ['users/', user_id, self.path_prefix, '/two_factor_auth'], + status_code=204) + + self.manager.deactivate_two_factor(user=user_id) + + def test_check_activated_two_factor_with_id(self): + user_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.path_prefix, '/two_factor_auth'], + status_code=204) + + self.manager.check_activated_two_factor(user_id=user_id) + + def test_check_activated_two_factor_with_name_and_domain(self): + user_name = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.path_prefix, '/two_factor_auth'], + status_code=204) + + self.manager.check_activated_two_factor(user_name=user_name, domain_id=domain_id) + + def test_get_two_factor_data(self): + user_id = uuid.uuid4().hex + key_ref = { + 'two_factor_auth': { + 'security_question': 'Sample question', + 'user_id': user_id, + } + } + self.stub_url('GET', + ['users/', user_id, self.path_prefix, '/two_factor_data'], + json=key_ref, + status_code=200) + + self.manager.get_two_factor_data(user=user_id) + + def test_check_security_question(self): + user_id = uuid.uuid4().hex + + self.stub_url('HEAD', + ['users/', user_id, self.path_prefix, '/sec_question'], + status_code=204) + + self.manager.check_security_question(user=user_id, + security_answer="Sample answer") + + def test_remember_device(self): + user_name = uuid.uuid4().hex + domain_name = uuid.uuid4().hex + user_id = uuid.uuid4().hex + device_id = uuid.uuid4().hex + device_token = uuid.uuid4().hex + + key_ref = { + 'two_factor_auth': { + 'device_id': device_id, + 'device_token': device_token + } + } + self.stub_url('POST', + [self.path_prefix, '/devices'], + json=key_ref, + status_code=201) + + self.manager.remember_device(user_name=user_name, domain_name=domain_name) + + def test_delete_all_devices(self): + user_id = uuid.uuid4().hex + + self.stub_url('DELETE', + ['users/', user_id, self.path_prefix, '/devices'], + status_code=204) + + self.manager.delete_all_devices(user=user_id) + + + +class TwoFactorAuthTests(utils.TestCase): + + + def test_two_factor_authenticate_success(self): + verification_code = uuid.uuid4().hex + password = uuid.uuid4().hex + user_id = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to password, and add its section. + token = client_fixtures.unscoped_token() + + token['methods'] = ["password"] + token['password'] = { + "verification_code": verification_code, + 'password': password + } + self.stub_auth(json=token) + + a = auth.TwoFactor( + self.TEST_URL, + verification_code=verification_code, + password=password, + user_id=user_id) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + TWO_FACTOR_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + 'user': { + "verification_code": verification_code, + 'password': password, + 'id': user_id + } + } + } + } + } + + self.assertRequestBodyIs(json=TWO_FACTOR_REQUEST_BODY) + + def test_two_factor_authenticate_scoped_success(self): + + verification_code = uuid.uuid4().hex + password = uuid.uuid4().hex + user_id = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to password, and add its section. + token = client_fixtures.project_scoped_token() + token['methods'] = ["password"] + token['password'] = { + "verification_code": verification_code, + 'password': password + } + self.stub_auth(json=token) + + a = auth.TwoFactor( + self.TEST_URL, + verification_code=verification_code, + password=password, + user_id=user_id) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + TWO_FACTOR_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + 'user': { + 'verification_code': verification_code, + 'password': password, + 'id': user_id + } + } + } + } + } + + self.assertRequestBodyIs(json=TWO_FACTOR_REQUEST_BODY) + + def test_two_factor_device_authenticate_success(self): + password = uuid.uuid4().hex + user_id = uuid.uuid4().hex + + device_id = uuid.uuid4().hex + device_token = uuid.uuid4().hex + + # Just use an existing project scoped token and change + # the methods to password, and add its section. + token = client_fixtures.unscoped_token() + + token['methods'] = ["password"] + token['password'] = { + "device_data": { "device_id": device_id, + "device_token": device_token}, + 'password': password + } + self.stub_auth(json=token) + + a = auth.TwoFactor( + self.TEST_URL, + device_data={ "device_id": device_id, + "device_token": device_token}, + password=password, + user_id=user_id) + s = session.Session(auth=a) + t = s.get_token() + self.assertEqual(self.TEST_TOKEN, t) + + TWO_FACTOR_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + 'user': { + "device_data": { "device_id": device_id, + "device_token": device_token}, + 'password': password, + 'id': user_id + } + } + } + } + } + + self.assertRequestBodyIs(json=TWO_FACTOR_REQUEST_BODY) \ No newline at end of file diff --git a/keystoneclient/tests/unit/v3/test_user_registration.py b/keystoneclient/tests/unit/v3/test_user_registration.py new file mode 100644 index 000000000..c1c7b6182 --- /dev/null +++ b/keystoneclient/tests/unit/v3/test_user_registration.py @@ -0,0 +1,119 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 uuid + +from keystoneclient.tests.unit.v3 import utils +from keystoneclient.v3.contrib.user_registration import users +from keystoneclient.v3.contrib.user_registration import activation_key +from keystoneclient.v3.contrib.user_registration import token + + +EXTENSION_PATH = 'OS-REGISTRATION' + +class UsersTests(utils.TestCase): + + def setUp(self): + super(UsersTests, self).setUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.Users + self.manager = self.client.user_registration.users + self.path_prefix = EXTENSION_PATH + + + def test_register_user(self): + name = uuid.uuid4().hex + user_ref = { + 'user': { + 'id': uuid.uuid4().hex, + 'name': name, + 'activation_key': uuid.uuid4().hex, + } + } + self.stub_url('POST', + [self.path_prefix, self.collection_key], + json=user_ref, + status_code=201) + + self.manager.register_user(name=name) + + def test_activate_user(self): + user_id = uuid.uuid4().hex + activation_key = uuid.uuid4().hex + self.stub_url('PATCH', + [self.path_prefix, 'activate', activation_key, + 'users', user_id], + status_code=200) + self.manager.activate_user(user=user_id, activation_key=activation_key) + + def test_reset_password(self): + user_id = uuid.uuid4().hex + token = uuid.uuid4().hex + password = uuid.uuid4().hex + self.stub_url('PATCH', + [self.path_prefix, 'reset_password', token, + self.collection_key, user_id], + status_code=204) + self.manager.reset_password(user=user_id, + reset_token=token, + new_password=password) + +class ActivationKeyTests(utils.TestCase): + + def setUp(self): + super(ActivationKeyTests, self).setUp() + self.key = 'activation_key' + self.collection_key = 'activate' + self.model = activation_key.ActivationKey + self.manager = self.client.user_registration.activation_key + self.path_prefix = EXTENSION_PATH + + def test_new_activation_key(self): + user_id = uuid.uuid4().hex + activation_key_ref = { + 'activation_key': { + 'id': uuid.uuid4().hex + } + } + self.stub_url('GET', + [self.path_prefix, 'users', + user_id, 'activate'], + json=activation_key_ref, + status_code=200) + self.manager.new_activation_key(user=user_id) + +class TokenTest(utils.TestCase): + + def setUp(self): + super(TokenTest, self).setUp() + self.key = 'token' + self.collection_key = 'reset_password' + self.model = token.Token + self.manager = self.client.user_registration.token + self.path_prefix = EXTENSION_PATH + + def test_get_reset_token(self): + user_id = uuid.uuid4().hex + reset_token_ref = { + 'reset_token': { + 'id': uuid.uuid4().hex + } + } + self.stub_url('GET', + [self.path_prefix, 'users', + user_id, self.collection_key], + json=reset_token_ref, + status_code=204) + self.manager.get_reset_token(user=user_id) diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 8becfab92..ebb239ae8 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -23,9 +23,13 @@ from keystoneclient.i18n import _ from keystoneclient.v3.contrib import endpoint_filter from keystoneclient.v3.contrib import endpoint_policy +from keystoneclient.v3.contrib import fiware_roles from keystoneclient.v3.contrib import federation from keystoneclient.v3.contrib import oauth1 +from keystoneclient.v3.contrib import oauth2 from keystoneclient.v3.contrib import trusts +from keystoneclient.v3.contrib import user_registration +from keystoneclient.v3.contrib import two_factor from keystoneclient.v3 import credentials from keystoneclient.v3 import domains from keystoneclient.v3 import endpoints @@ -179,6 +183,13 @@ def __init__(self, **kwargs): self.federation = federation.FederationManager(self._adapter) self.groups = groups.GroupManager(self._adapter) self.oauth1 = oauth1.create_oauth_manager(self._adapter) + # TODO(garcianavalon) document this + self.endpoint_groups = endpoint_filter.EndpointGroupFilterManager( + self._adapter) + self.fiware_roles = fiware_roles.FiwareRolesManager(self) + self.oauth2 = oauth2.create_oauth_manager(self) + self.user_registration = user_registration.UserRegistrationManager(self) + self.two_factor = two_factor.TwoFactorManager(self) self.policies = policies.PolicyManager(self._adapter) self.projects = projects.ProjectManager(self._adapter) self.regions = regions.RegionManager(self._adapter) diff --git a/keystoneclient/v3/contrib/activation.py b/keystoneclient/v3/contrib/activation.py new file mode 100644 index 000000000..1cc82b461 --- /dev/null +++ b/keystoneclient/v3/contrib/activation.py @@ -0,0 +1,38 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.user_registration.utils import REGISTRATION_PATH + +class Activation(base.Resource): + pass + +class ActivationManager(base.CrudManager): + """Manager class for activating user in the USER REGISTRATION extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = Activation + collection_key = 'activate' + key = 'activation_key' + base_url = REGISTRATION_PATH + + def new_activation_key(self, user): + base_url = self.base_url + '/users/{0}/'.format(base.getid(user)) + return super(ActivationManager, self).get(base_url) + + def activate_user(self, user, activation_key): + base_url = self.base_url + '/users/{0}/'.format(base.getid(user)) + return super(ActivationManager, self).update(base_url, activation_key) diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py index 0da79b81b..04e97fa82 100644 --- a/keystoneclient/v3/contrib/endpoint_filter.py +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -18,17 +18,17 @@ from keystoneclient.v3 import endpoints from keystoneclient.v3 import projects +OS_EP_FILTER_EXT = 'OS-EP-FILTER' class EndpointFilterManager(base.Manager): """Manager class for manipulating project-endpoint associations.""" - OS_EP_FILTER_EXT = '/OS-EP-FILTER' - + def _build_base_url(self, project=None, endpoint=None): project_id = base.getid(project) endpoint_id = base.getid(endpoint) if project_id and endpoint_id: - api_path = '/projects/%s/endpoints/%s' % (project_id, endpoint_id) + api_path = '/projects/%s/endpoints/%s' % (endpoint_id, project_id) elif project_id: api_path = '/projects/%s/endpoints' % (project_id) elif endpoint_id: @@ -37,7 +37,7 @@ def _build_base_url(self, project=None, endpoint=None): msg = _('Must specify a project, an endpoint, or both') raise exceptions.ValidationError(msg) - return self.OS_EP_FILTER_EXT + api_path + return '/' + OS_EP_FILTER_EXT + api_path def add_endpoint_to_project(self, project, endpoint): """Create a project-endpoint association.""" @@ -87,3 +87,112 @@ def list_projects_for_endpoint(self, endpoint): base_url, projects.ProjectManager.collection_key, obj_class=projects.ProjectManager.resource_class) + + +class EndpointGroupFilter(base.Resource): + pass + + +class EndpointGroupFilterManager(base.CrudManager): + """Manager class for Endpoint Group Filters.""" + + resource_class = EndpointGroupFilter + collection_key = 'endpoint_groups' + key = 'endpoint_group' + base_url = OS_EP_FILTER_EXT + + def create(self, name, description=None, filters=None, **kwargs): + filters = filters if filters else {} + + return super(EndpointGroupFilterManager, self).create( + name=name, + description=description, + filters=filters, + **kwargs) + + def get(self, endpoint_group): + return super(EndpointGroupFilterManager, self).get( + endpoint_group_id=base.getid(endpoint_group)) + + + def update(self, endpoint_group, name=None, description=None, filters=None, **kwargs): + return super(EndpointGroupFilterManager, self).update( + endpoint_group_id=base.getid(endpoint_group), + name=name, + description=description, + filters=filters, + **kwargs) + + + def delete(self, endpoint_group): + return super(EndpointGroupFilterManager, self).delete( + endpoint_group_id=base.getid(endpoint_group)) + + + def list(self, **kwargs): + base_url = self.base_url + return super(EndpointGroupFilterManager, self).list(base_url=base_url, **kwargs) + + def _build_base_url(self, project=None, endpoint_group=None): + project_id = base.getid(project) + endpoint_group_id = base.getid(endpoint_group) + + if project_id and endpoint_group_id: + api_path = '/endpoint_groups/{0}/projects/{1}'.format(endpoint_group_id, + project_id) + elif project_id: + api_path = '/projects/{0}/endpoint_groups'.format(project_id) + elif endpoint_group_id: + api_path = '/endpoint_groups/{0}/projects'.format(endpoint_group_id) + else: + msg = _('Must specify a project, an endpoint_group, or both') + raise exceptions.ValidationError(msg) + + return '/' + OS_EP_FILTER_EXT + api_path + + def add_endpoint_group_to_project(self, project, endpoint_group): + """Create a project-endpoint_group association. + PUT /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects/{project_id} + """ + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointGroupFilterManager, self)._put(url=base_url) + + def delete_endpoint_group_from_project(self, project, endpoint_group): + """Remove a project-endpoint_group association. + DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects/{project_id} + """ + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointGroupFilterManager, self)._delete(url=base_url) + + def check_endpoint_group_in_project(self, project, endpoint_group): + """Checks if project-endpoint_group association exist. + HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects/{project_id} + """ + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointGroupFilterManager, self)._head(url=base_url) + + def list_endpoint_groups_for_project(self, project): + """List all endpoints for a given project. + GET /OS-EP-FILTER/projects/{project_id}/endpoint_groups + + """ + if not project: + raise ValueError(_('project is required')) + + base_url = self._build_base_url(project=project) + return super(EndpointGroupFilterManager, self)._list( + base_url, + self.collection_key, + obj_class=self.resource_class) \ No newline at end of file diff --git a/keystoneclient/v3/contrib/fiware_roles/__init__.py b/keystoneclient/v3/contrib/fiware_roles/__init__.py new file mode 100644 index 000000000..7028e2eb5 --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.fiware_roles.core import * \ No newline at end of file diff --git a/keystoneclient/v3/contrib/fiware_roles/allowed.py b/keystoneclient/v3/contrib/fiware_roles/allowed.py new file mode 100644 index 000000000..66995c77b --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/allowed.py @@ -0,0 +1,84 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 + +from keystoneclient import base +from keystoneclient.v3.contrib.fiware_roles.utils import ROLES_PATH + + + +class AllowedManager(base.Manager): + """Manager class for obtaining allowed items based on internal permissions + in the FIWARE ROLES extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + base_url = ROLES_PATH + + def list_user_allowed_roles_to_assign(self, user, organization): + """Obtain a list of all the roles the user is allowed to assign + for every application. + """ + endpoint = (self.base_url + '/users/{0}/organizations/{1}/roles/allowed' + ).format(base.getid(user), base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_roles = json.loads(resp.content)['allowed_roles'] + return allowed_roles + + def list_organization_allowed_roles_to_assign(self, organization): + """Obtain a list of all the roles the user is allowed to assign + for every application. + """ + endpoint = self.base_url + '/organizations/{0}/roles/allowed'.format( + base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_roles = json.loads(resp.content)['allowed_roles'] + return allowed_roles + + def list_user_allowed_applications_to_manage(self, user, organization): + """Obtain a list of all the applications the user is allowed to manage. + """ + endpoint = (self.base_url + '/users/{0}/organizations/{1}/applications/allowed' + ).format(base.getid(user), base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_applications = json.loads(resp.content)['allowed_applications'] + return allowed_applications + + def list_organization_allowed_applications_to_manage(self, organization): + """Obtain a list of all the applications the user is allowed to manage. + """ + endpoint = self.base_url + '/organizations/{0}/applications/allowed'.format( + base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_applications = json.loads(resp.content)['allowed_applications'] + return allowed_applications + + def list_user_allowed_applications_to_manage_roles(self, user, organization): + """Obtain a list of all the applications the user is allowed to manage. + """ + endpoint = (self.base_url + '/users/{0}/organizations/{1}/applications/allowed_roles' + ).format(base.getid(user), base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_applications = json.loads(resp.content)['allowed_applications'] + return allowed_applications + + def list_organization_allowed_applications_to_manage_roles(self, organization): + """Obtain a list of all the applications the user is allowed to manage. + """ + endpoint = self.base_url + '/organizations/{0}/applications/allowed_roles'.format( + base.getid(organization)) + resp, body = self.client.get(endpoint) + allowed_applications = json.loads(resp.content)['allowed_applications'] + return allowed_applications \ No newline at end of file diff --git a/keystoneclient/v3/contrib/fiware_roles/core.py b/keystoneclient/v3/contrib/fiware_roles/core.py new file mode 100644 index 000000000..4963d539e --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/core.py @@ -0,0 +1,27 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.fiware_roles import allowed +from keystoneclient.v3.contrib.fiware_roles import roles +from keystoneclient.v3.contrib.fiware_roles import role_assignments +from keystoneclient.v3.contrib.fiware_roles import permissions + + +class FiwareRolesManager(object): + def __init__(self, api): + self.roles = roles.RoleManager(api) + self.permissions = permissions.PermissionManager(api) + self.role_assignments = role_assignments.RoleAssignmentManager(api) + self.allowed = allowed.AllowedManager(api) + \ No newline at end of file diff --git a/keystoneclient/v3/contrib/fiware_roles/permissions.py b/keystoneclient/v3/contrib/fiware_roles/permissions.py new file mode 100644 index 000000000..e068bf96e --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/permissions.py @@ -0,0 +1,83 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.fiware_roles.utils import ROLES_PATH + +class Permission(base.Resource): + pass + +class PermissionManager(base.CrudManager): + """Manager class for manipulating permissions in the FIWARE ROLES extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = Permission + collection_key = 'permissions' + key = 'permission' + base_url = ROLES_PATH + + def _require_role_and_permission(self, role, permission): + if not (role and permission): + msg = 'Specify both a role and a permission' + raise exceptions.ValidationError(msg) + + def create(self, name, is_internal=False, application=None, **kwargs): + return super(PermissionManager, self).create( + name=name, + is_internal=is_internal, + application_id=base.getid(application), + **kwargs) + def get(self, permission): + return super(PermissionManager, self).get( + permission_id=base.getid(permission)) + + def update(self, permission, name=None, is_internal=False, + application=None, **kwargs): + return super(PermissionManager, self).update( + permission_id=base.getid(permission), + name=name, + is_internal=is_internal, + application_id=base.getid(application), + **kwargs) + + + def delete(self, permission): + return super(PermissionManager, self).delete( + permission_id=base.getid(permission)) + + def list(self, role=None, **kwargs): + if role: + base_url = self.base_url + '/roles/%s' % base.getid(role) + + else: + base_url = self.base_url + return super(PermissionManager, self).list(base_url=base_url, **kwargs) + + def add_to_role(self, role, permission): + self._require_role_and_permission(role, permission) + base_url = self.base_url + '/roles/%s' % base.getid(role) + + return super(PermissionManager, self).put( + base_url=base_url, + permission_id=base.getid(permission)) + + def remove_from_role(self, role, permission): + self._require_role_and_permission(role, permission) + base_url = self.base_url + '/roles/%s' % base.getid(role) + + return super(PermissionManager, self).delete( + base_url=base_url, + permission_id=base.getid(permission)) diff --git a/keystoneclient/v3/contrib/fiware_roles/role_assignments.py b/keystoneclient/v3/contrib/fiware_roles/role_assignments.py new file mode 100644 index 000000000..2289d563f --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/role_assignments.py @@ -0,0 +1,118 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.fiware_roles.utils import ROLES_PATH + +class RoleAssignment(base.Resource): + pass + + +class RoleAssignmentManager(base.CrudManager): + + """Manager class for manipulating user and organization roles assignments.""" + resource_class = RoleAssignment + collection_key = 'role_assignments' + key = 'role_assignment' + base_url = ROLES_PATH + + def list_user_role_assignments(self, user=None, organization=None, + application=None, default_organization=False): + """Lists role assignments for users. + + If no arguments are provided, all role assignments in the + system will be listed. + + :param user: User to be used as query filter. (optional) + :param organization: Project to be used as query filter. + (optional) + :param application: Application to be used as query + filter. (optional) + :param default_organization: If set to true, the endpoint will filter role assignments + only in the default_project_id and the organization param is ignored. (optional) + """ + + query_params = {} + if user: + query_params['user_id'] = base.getid(user) + if organization: + query_params['organization_id'] = base.getid(organization) + if application: + query_params['application_id'] = base.getid(application) + if default_organization: + query_params['default_organization'] = True + + base_url = self.base_url + '/users' + return super(RoleAssignmentManager, self).list(base_url=base_url, + **query_params) + + def list_organization_role_assignments(self, organization=None, + application=None): + """Lists role assignments for organizations. + + If no arguments are provided, all role assignments in the + system will be listed. + + :param organization: Project to be used as query filter. + (optional) + :param application: Domain to be used as query + filter. (optional) + """ + + query_params = {} + if organization: + query_params['organization_id'] = base.getid(organization) + if application: + query_params['application_id'] = base.getid(application) + + base_url = self.base_url + '/organizations' + return super(RoleAssignmentManager, self).list(base_url=base_url, + **query_params) + + def create(self, **kwargs): + raise exceptions.MethodNotImplemented('Create not supported for' + ' role assignments') + + def update(self, **kwargs): + raise exceptions.MethodNotImplemented('Update not supported for' + ' role assignments') + + def get(self, **kwargs): + raise exceptions.MethodNotImplemented('Get not supported for' + ' role assignments') + + def find(self, **kwargs): + raise exceptions.MethodNotImplemented('Find not supported for' + ' role assignments') + + def put(self, **kwargs): + raise exceptions.MethodNotImplemented('Put not supported for' + ' role assignments') + + def delete(self, **kwargs): + raise exceptions.MethodNotImplemented('Delete not supported for' + ' role assignments') diff --git a/keystoneclient/v3/contrib/fiware_roles/roles.py b/keystoneclient/v3/contrib/fiware_roles/roles.py new file mode 100644 index 000000000..2e1665224 --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/roles.py @@ -0,0 +1,93 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient.v3.contrib.fiware_roles.utils import ROLES_PATH + + +class Role(base.Resource): + pass + +class RoleManager(base.CrudManager): + """Manager class for manipulating roles in the FIWARE ROLES extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = Role + collection_key = 'roles' + key = 'role' + base_url = ROLES_PATH + + def create(self, name, is_internal=False, application=None, **kwargs): + return super(RoleManager, self).create( + name=name, + is_internal=is_internal, + application_id=base.getid(application), + **kwargs) + + + def get(self, role): + return super(RoleManager, self).get(role_id=base.getid(role)) + + + def update(self, role, name=None, is_internal=False, + application=None, **kwargs): + return super(RoleManager, self).update(role_id=base.getid(role), + name=name, + is_internal=is_internal, + application_id=base.getid(application), + **kwargs) + + + def delete(self, role): + return super(RoleManager, self).delete(role_id=base.getid(role)) + + + def list(self, **kwargs): + base_url = self.base_url + return super(RoleManager, self).list(base_url=base_url, **kwargs) + + + # ROLE-USER + def add_to_user(self, role, user, organization, application): + base_url = (self.base_url + '/users/{0}/organizations/{1}/applications/{2}' + ).format(base.getid(user), base.getid(organization), + base.getid(application)) + + return super(RoleManager, self).put(base_url=base_url, + role_id=base.getid(role)) + + + def remove_from_user(self, role, user, organization, application): + base_url = (self.base_url + '/users/{0}/organizations/{1}/applications/{2}' + ).format(base.getid(user), base.getid(organization), + base.getid(application)) + + return super(RoleManager, self).delete(base_url=base_url, + role_id=base.getid(role)) + + # ROLES-ORGANIZATIONS + def add_to_organization(self, role, organization, application): + base_url = (self.base_url + '/organizations/{0}/applications/{1}' + ).format(base.getid(organization), base.getid(application)) + + return super(RoleManager, self).put(base_url=base_url, + role_id=base.getid(role)) + + + def remove_from_organization(self, role, organization, application): + base_url = (self.base_url + '/organizations/{0}/applications/{1}' + ).format(base.getid(organization), base.getid(application)) + return super(RoleManager, self).delete(base_url=base_url, + role_id=base.getid(role)) \ No newline at end of file diff --git a/keystoneclient/v3/contrib/fiware_roles/utils.py b/keystoneclient/v3/contrib/fiware_roles/utils.py new file mode 100644 index 000000000..2cd330589 --- /dev/null +++ b/keystoneclient/v3/contrib/fiware_roles/utils.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +ROLES_PATH = '/OS-ROLES' \ No newline at end of file diff --git a/keystoneclient/v3/contrib/oauth2/__init__.py b/keystoneclient/v3/contrib/oauth2/__init__.py new file mode 100644 index 000000000..0e621d89e --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.oauth2.core import * \ No newline at end of file diff --git a/keystoneclient/v3/contrib/oauth2/access_tokens.py b/keystoneclient/v3/contrib/oauth2/access_tokens.py new file mode 100644 index 000000000..4ba479dbd --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/access_tokens.py @@ -0,0 +1,92 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 base64 +import json + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.oauth2 import utils + +class AccessToken(base.Resource): + pass + + +class AccessTokenManager(base.CrudManager): + """Manager class for manipulating identity OAuth access tokens.""" + resource_class = AccessToken + collection_key = 'access_tokens' + key = 'access_token' + base_url = utils.OAUTH2_PATH + + def create(self, consumer_id, consumer_secret, authorization_code, + redirect_uri): + endpoint = self.base_url + '/access_token' + + headers, body = self._generate_json_request(consumer_id, consumer_secret, + authorization_code, redirect_uri) + + resp, body = self.client.post(endpoint, headers=headers, body=body) + + token = json.loads(resp.content) + return self.resource_class(self, token) + + def list_for_user(self, user, **kwargs): + """lists all the created access token for a user.""" + base_url = '/users/{0}'.format(base.getid(user)) + self.base_url + return super(AccessTokenManager, self).list( + base_url=base_url, **kwargs) + + def list(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'List not supported for access_tokens') + + def update(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'Update not supported for access_tokens') + + def get(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'Get not supported for access_tokens') + + def find(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'Find not supported for access_tokens') + + def put(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'Put not supported for access_tokens') + + def delete(self, **kwargs): + raise exceptions.MethodNotImplemented( + 'Delete not supported for access_tokens') + + def _generate_json_request(self, consumer_id, consumer_secret, + authorization_code, redirect_uri): + body = { + 'token_request' : { + 'grant_type':'authorization_code', + 'code': authorization_code, + 'redirect_uri':redirect_uri + } + } + headers = { + 'Authorization': self._http_basic(consumer_id, consumer_secret) + } + return headers, body + + def _http_basic(self, consumer_id, consumer_secret): + auth_string = consumer_id + ':' + consumer_secret + return 'Basic ' + base64.b64encode(auth_string) + diff --git a/keystoneclient/v3/contrib/oauth2/auth.py b/keystoneclient/v3/contrib/oauth2/auth.py new file mode 100644 index 000000000..bccedfa0f --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/auth.py @@ -0,0 +1,40 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.auth.identity import v3 + + +class OAuth2Method(v3.AuthMethod): + + _method_parameters = ['access_token'] + + def __init__(self, **kwargs): + """Construct an OAuth based authentication method. + :param string access_token: Access token id. + """ + super(OAuth2Method, self).__init__(**kwargs) + + + def get_auth_data(self, session, auth, headers, **kwargs): + # Build the data for our custom auth method. Check the OAuth2.0 keystone + # auth plugin for more info: https://www.github.com/ging/keystone + auth_data = { + 'access_token_id':self.access_token + } + name = 'oauth2' + return name, auth_data + + +class OAuth2(v3.AuthConstructor): + _auth_method_class = OAuth2Method diff --git a/keystoneclient/v3/contrib/oauth2/authorization_codes.py b/keystoneclient/v3/contrib/oauth2/authorization_codes.py new file mode 100644 index 000000000..e56ec9eff --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/authorization_codes.py @@ -0,0 +1,121 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 urllib +import six +from six.moves.urllib import parse as urlparse + +from keystoneclient import base +from keystoneclient.v3.contrib.oauth2 import utils + + +class AuthorizationCode(base.Resource): + """ TODO(garcianavalon) + """ + pass + + +class AuthorizationCodeManager(base.CrudManager): + """Manager class for manipulating identity OAuth authorization codes.""" + resource_class = AuthorizationCode + collection_key = 'authorization_codes' + key = 'authorization_code' + base_url = utils.OAUTH2_PATH + + def authorize(self, consumer, scopes, redirect=False): + """Authorize a Consumer for certain scopes, getting an authorization code. + + The way the provider (Keystone) will return the code is in the header, as an + HTTP redirection: + 'Location': 'https://foo.com/welcome_back?code=somerandomstring&state=xyz' + + Utilize Identity API operation: + POST /OS-OAUTH2/authorize/ + + :param user: the user granting authorization + :param consumer: the client that will be authorized, and + will exchange the authorization code for an access token. + :param scopes: a list of scopes. They are provided by the consumer + in the authorization request + :param redirect: The Keystone OAuth2 extension returns an HTTP 302 to + comply with RFC 6749 but in general we dont want the redirect to happen + if we are using the keystoneclient. + """ + endpoint = self.base_url + '/authorize' + body = { + 'user_auth': { + 'client_id':base.getid(consumer), + 'scopes':scopes + } + } + response, body = self.client.post(endpoint, body=body, redirect=redirect) + + redirect_uri = response.headers.get('Location') + + parsed = urlparse.urlparse(redirect_uri) + query = dict(urlparse.parse_qsl(parsed.query)) + authorization_code = { + 'redirect_uri':redirect_uri, + 'state': query['state'] + } + code = query.get('code', None) + if code: + authorization_code['code'] = code + + token = query.get('token', None) + if token: + authorization_code['token'] = token + + return self.resource_class(self, authorization_code) + + def request_authorization(self, consumer, redirect_uri, scope, response_type='code', state=None): + """ Send the consumer credentials to the OAuth2 provider. + + The user then will be asked to authorize the client for the requested scopes. In + the OAuth2 flow this happens when the client(consumer) redirects the resource + owner(user) through his user agent to the authorization server(provider). + Therefore, this call is done by the user but with the data provided by the + consumer. + + Utilize Identity API operation: + GET /OS-OAUTH2/authorize/?client_id=&redirect_uri=&response_type=code&state= + + :param consumer: the consumer asking for authorization + :param redirect_uri: The url the user will be redirected to. It must be + registered in the server asociated with the requesting consumer. + :param scope: list of strings with the requested scopes from the ones + defined by the provider. + :param state: Optional, a string for consumer use. + """ + # Transform the array with the requested scopes into a list of + # space-delimited, case-sensitive strings as specified in RFC 6749 + # http://tools.ietf.org/html/rfc6749#section-3.3 + scope_string = ' '.join(scope) + + # NOTE(garcianavalon) we use a list of tuples to ensure param order + # in the query string to be able to mock it during testing. + credentials = [ + ('response_type', response_type), + ('client_id', base.getid(consumer)), + ('redirect_uri', redirect_uri), + ('scope', scope_string), + ('state', state) + ] + query = urllib.urlencode(credentials) + endpoint = self.base_url + '/authorize?%s' %query + + response, body = self.client.get(endpoint) + # TODO(garcianavalon) figure out the return. Do we need a separated manager? + return json.loads(response.content) diff --git a/keystoneclient/v3/contrib/oauth2/consumers.py b/keystoneclient/v3/contrib/oauth2/consumers.py new file mode 100644 index 000000000..d1d8d10fe --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/consumers.py @@ -0,0 +1,85 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 logging + +from keystoneclient import base +from keystoneclient.v3.contrib.oauth2 import utils + +LOG = logging.getLogger(__name__) + +class Consumer(base.Resource): + """Represents an OAuth2 consumer. + Attributes: + * id: a uuid that identifies the consumer + * description: a short description of the consumer + * secret: string used by consumers to sign certain requests + * client_type: string OAuth2 client type. The extension defines which client types + are accepted + * redirect_uris: List of strings with valid urls. This are the only urls the + consumer is allowed to redirect to + * grant_type: string with OAuth2 grant type. The extension defines which + ones are accepted + * response_type: string with the response types associated with the grant. The + extension handles assigning this value to the correspoding one for grant_type. See + https://oauthlib.readthedocs.org/en/latest/oauth2/oauth2.html for more info + * scopes: list of strings with the scopes the consumer is going to use. Defined + at the extension + """ + pass + +class ConsumerManager(base.CrudManager): + """Manager class for manipulating identity consumers.""" + resource_class = Consumer + collection_key = 'consumers' + key = 'consumer' + base_url = utils.OAUTH2_PATH + + def create(self, name, description=None, client_type=None, redirect_uris=[], + grant_type=None, scopes=[], **kwargs): + return super(ConsumerManager, self).create( + name=name, + description=description, + client_type=client_type, + redirect_uris=redirect_uris, + grant_type=grant_type, + scopes=scopes, + **kwargs) + + def get(self, consumer): + return super(ConsumerManager, self).get( + consumer_id=base.getid(consumer)) + + def update(self, consumer, name=None, description=None, client_type=None, + redirect_uris=[], grant_type=None, scopes=[], **kwargs): + return super(ConsumerManager, self).update( + consumer_id=base.getid(consumer), + name=name, + description=description, + client_type=client_type, + redirect_uris=redirect_uris, + grant_type=grant_type, + scopes=scopes, + **kwargs) + + def delete(self, consumer): + return super(ConsumerManager, self).delete( + consumer_id=base.getid(consumer)) + + def list(self, user=None, **kwargs): + if user: + base_url = '/users/{0}'.format(base.getid(user)) + self.base_url + + else: + base_url = self.base_url + return super(ConsumerManager, self).list(base_url=base_url, **kwargs) \ No newline at end of file diff --git a/keystoneclient/v3/contrib/oauth2/core.py b/keystoneclient/v3/contrib/oauth2/core.py new file mode 100644 index 000000000..8e5d022c5 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/core.py @@ -0,0 +1,28 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.oauth2 import access_tokens +from keystoneclient.v3.contrib.oauth2 import authorization_codes +from keystoneclient.v3.contrib.oauth2 import consumers + + +def create_oauth_manager(self): + # TODO(garcianavalon) this is no longer necesary, remove + return OAuthManager(self) + +class OAuthManager(object): + def __init__(self, api): + self.access_tokens = access_tokens.AccessTokenManager(api) + self.consumers = consumers.ConsumerManager(api) + self.authorization_codes = authorization_codes.AuthorizationCodeManager(api) \ No newline at end of file diff --git a/keystoneclient/v3/contrib/oauth2/utils.py b/keystoneclient/v3/contrib/oauth2/utils.py new file mode 100644 index 000000000..3e1bc79d9 --- /dev/null +++ b/keystoneclient/v3/contrib/oauth2/utils.py @@ -0,0 +1,20 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +OAUTH2_PATH = '/OS-OAUTH2' + + +# TODO(garcianavalon) this seems to be unnecesary, refactor it + + diff --git a/keystoneclient/v3/contrib/two_factor/__init__.py b/keystoneclient/v3/contrib/two_factor/__init__.py new file mode 100644 index 000000000..056aac325 --- /dev/null +++ b/keystoneclient/v3/contrib/two_factor/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.two_factor.core import * \ No newline at end of file diff --git a/keystoneclient/v3/contrib/two_factor/auth.py b/keystoneclient/v3/contrib/two_factor/auth.py new file mode 100644 index 000000000..9a83e0665 --- /dev/null +++ b/keystoneclient/v3/contrib/two_factor/auth.py @@ -0,0 +1,70 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 copy + +from oslo_config import cfg + +from keystoneclient.auth.identity import v3 + + +class TwoFactorMethod(v3.PasswordMethod): + """Construct a User/Password based authentication method with an extra verification code. + + :param string password: Password for authentication. + :param string username: Username for authentication. + :param string user_id: User ID for authentication. + :param string user_domain_id: User's domain ID for authentication. + :param string user_domain_name: User's domain name for authentication. + :param string verification_code: Code generated through the key and the timestamp. + :param dict device_data: Info from the client cookie to bypass verification code. + """ + + _method_parameters = [ + 'user_id', + 'username', + 'user_domain_id', + 'user_domain_name', + 'password', + 'verification_code', + 'device_data' + ] + + + def get_auth_data(self, session, auth, headers, **kwargs): + method, payload = super(TwoFactorMethod, self).get_auth_data(session, auth, headers, **kwargs) + + if self.verification_code: + payload['user']['verification_code'] = self.verification_code + if self.device_data: + payload['user']['device_data'] = self.device_data + + return method, payload + + +class TwoFactor(v3.Password): + """AuthPlugin for TwoFactorMethod.""" + _auth_method_class = TwoFactorMethod + + @classmethod + def get_options(cls): + options = super(TwoFactor, cls).get_options() + + options.extend([ + cfg.StrOpt('verification-code', help='Generated code by timestamp'), + cfg.DictOpt('device_data', help='Cached device data') + ]) + + return options \ No newline at end of file diff --git a/keystoneclient/v3/contrib/two_factor/core.py b/keystoneclient/v3/contrib/two_factor/core.py new file mode 100644 index 000000000..dacb1cd19 --- /dev/null +++ b/keystoneclient/v3/contrib/two_factor/core.py @@ -0,0 +1,20 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.two_factor import keys + + +class TwoFactorManager(object): + def __init__(self, api): + self.keys = keys.KeyManager(api) \ No newline at end of file diff --git a/keystoneclient/v3/contrib/two_factor/keys.py b/keystoneclient/v3/contrib/two_factor/keys.py new file mode 100644 index 000000000..aded34fc0 --- /dev/null +++ b/keystoneclient/v3/contrib/two_factor/keys.py @@ -0,0 +1,96 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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 logging + +import urllib + +from keystoneclient import base + + +LOG = logging.getLogger(__name__) + +EXTENSION_PATH = '/OS-TWO-FACTOR' + +class Key(base.Resource): + pass + + +class KeyManager(base.Manager): + """Manager class for creating/deleting two factor keys.""" + + resource_class = Key + auth_url = '/two_factor_auth' + security_question_url = '/sec_question' + two_factor_data_url = '/two_factor_data' + devices_url = '/devices' + + def _url(self, user): + return '/users/{user_id}'.format(user_id=base.getid(user)) + EXTENSION_PATH + + def _auth_url(self, user): + return self._url(user) + self.auth_url + + def _security_question_url(self, user): + return self._url(user) + self.security_question_url + + def _two_factor_data_url(self, user): + return self._url(user) + self.two_factor_data_url + + def _devices_url(self, user): + return self._url(user) + self.devices_url + + def _check_base_url(self): + return EXTENSION_PATH + self.auth_url + + def generate_new_key(self, user, security_question, security_answer): + if security_question and security_answer: + data = {} + data["two_factor_auth"] = {} + data["two_factor_auth"]["security_question"] = security_question + data["two_factor_auth"]["security_answer"] = security_answer + else: + data = None + + return super(KeyManager, self)._post(body=data, + url=self._auth_url(user), + response_key="two_factor_auth") + + def deactivate_two_factor(self, user): + return super(KeyManager, self)._delete(url=self._auth_url(user)) + + def check_activated_two_factor(self, **kwargs): + try: + super(KeyManager, self)._head(url=self._check_base_url() + '?' + urllib.urlencode(kwargs)) + return True + except: + return False + + def get_two_factor_data(self, user): + return super(KeyManager, self)._get(url=self._two_factor_data_url(user), + response_key="two_factor_auth") + + def check_security_question(self, user, security_answer): + + return super(KeyManager, self)._head(url=self._security_question_url(user) + '?sec_answer=' + security_answer) + + def remember_device(self, **kwargs): + return super(KeyManager, self)._post(body={}, + url=EXTENSION_PATH+'/devices?'+urllib.urlencode(kwargs), + response_key="two_factor_auth") + + def delete_all_devices(self, user): + return super(KeyManager, self)._delete(url=self._devices_url(user)) + + def check_for_device(self, **kwargs): + return super(KeyManager, self)._head(url=EXTENSION_PATH+'/devices?'+urllib.urlencode(kwargs)) diff --git a/keystoneclient/v3/contrib/user_registration/__init__.py b/keystoneclient/v3/contrib/user_registration/__init__.py new file mode 100644 index 000000000..46a07774a --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.user_registration.core import * \ No newline at end of file diff --git a/keystoneclient/v3/contrib/user_registration/activation_key.py b/keystoneclient/v3/contrib/user_registration/activation_key.py new file mode 100644 index 000000000..3a4e634ea --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/activation_key.py @@ -0,0 +1,36 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.user_registration.utils import REGISTRATION_PATH + +class ActivationKey(base.Resource): + pass + +class ActivationKeyManager(base.CrudManager): + """Manager class for activating user in the USER REGISTRATION extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = ActivationKey + collection_key = 'activate' + key = 'activation_key' + base_url = REGISTRATION_PATH + + def new_activation_key(self, user): + base_url = self.base_url + '/users/{0}'.format(base.getid(user)) + return super(ActivationKeyManager, self).get(base_url=base_url) + + \ No newline at end of file diff --git a/keystoneclient/v3/contrib/user_registration/core.py b/keystoneclient/v3/contrib/user_registration/core.py new file mode 100644 index 000000000..c48edc184 --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/core.py @@ -0,0 +1,21 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient.v3.contrib.user_registration import users, activation_key, token + +class UserRegistrationManager(object): + def __init__(self, api): + self.users = users.UsersManager(api) + self.activation_key = activation_key.ActivationKeyManager(api) + self.token = token.TokenManager(api) diff --git a/keystoneclient/v3/contrib/user_registration/token.py b/keystoneclient/v3/contrib/user_registration/token.py new file mode 100644 index 000000000..f88da1647 --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/token.py @@ -0,0 +1,37 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.user_registration.utils import REGISTRATION_PATH + +class Token(base.Resource): + pass + +class TokenManager(base.CrudManager): + """Manager class for manipulating passwords and tokens in + the USER REGISTRATION extension for Keystone. + + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = Token + collection_key = 'reset_password' + key = 'reset_token' + base_url = REGISTRATION_PATH + + def get_reset_token(self, user): + base_url = self.base_url + '/users/{0}'.format(base.getid(user)) + return super(TokenManager, self).get(base_url=base_url) + + diff --git a/keystoneclient/v3/contrib/user_registration/users.py b/keystoneclient/v3/contrib/user_registration/users.py new file mode 100644 index 000000000..72fc6f256 --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/users.py @@ -0,0 +1,68 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +from keystoneclient import base +from keystoneclient import exceptions +from keystoneclient.v3.contrib.user_registration.utils import REGISTRATION_PATH + +class Users(base.Resource): + pass + +class UsersManager(base.CrudManager): + """Manager class for manipulating user registration in the USER REGISTRATION + extension for Keystone. + For more information about the extension: https://www.github.com/ging/keystone + """ + resource_class = Users + collection_key = 'users' + key = 'user' + base_url = REGISTRATION_PATH + + + def register_user(self, name, domain=None, password=None, + username=None, description=None, **kwargs): + + user_data = base.filter_none(name=name, + domain_id=base.getid(domain), + password=password, + username=username, + description=description, + **kwargs) + + return self._create(self.base_url+'/users', {'user': user_data}, 'user', + log=not bool(password)) + + def activate_user(self, user, activation_key): + url = self.base_url + '/activate/{0}/users/{1}'.format( + base.getid(activation_key), + base.getid(user)) + return self._update( + url, + None, + self.key, + method='PATCH') + + def reset_password(self, user, reset_token, new_password): + url = self.base_url + '/reset_password/{0}/users/{1}'.format( + base.getid(reset_token), base.getid(user)) + body = { + 'user' : { + 'password' : new_password, + } + } + return self._update( + url, + body=body, + response_key=self.key, + method='PATCH') \ No newline at end of file diff --git a/keystoneclient/v3/contrib/user_registration/utils.py b/keystoneclient/v3/contrib/user_registration/utils.py new file mode 100644 index 000000000..f75472580 --- /dev/null +++ b/keystoneclient/v3/contrib/user_registration/utils.py @@ -0,0 +1,15 @@ +# Copyright (C) 2014 Universidad Politecnica de Madrid +# 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. + +REGISTRATION_PATH = '/OS-REGISTRATION' diff --git a/requirements.txt b/requirements.txt index caed2a785..e6baec81b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,17 +2,17 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -pbr>=0.6,!=0.7,<1.0 +pbr>=0.11.1,<=1.8.1 argparse -Babel>=1.3 -iso8601>=0.1.9 -netaddr>=0.7.12 -oslo.config>=1.6.0 # Apache-2.0 -oslo.i18n>=1.3.0 # Apache-2.0 -oslo.serialization>=1.2.0 # Apache-2.0 -oslo.utils>=1.2.0 # Apache-2.0 -PrettyTable>=0.7,<0.8 -requests>=2.2.0,!=2.4.0 -six>=1.9.0 -stevedore>=1.1.0 # Apache-2.0 +Babel>=1.3,<=2.1.1 +iso8601==0.1.10 +netaddr==0.7.13 +oslo.config>=1.6.0,<=3.0.0 # Apache-2.0 +oslo.i18n==1.3.1 # Apache-2.0 +oslo.serialization==1.2.0 # Apache-2.0 +oslo.utils>=1.4.1,<=1.4.2 # Apache-2.0 +prettytable==0.7.2 +requests>=2.2.1,<=2.8.1 +six==1.9.0 +stevedore>=1.2.0,<=1.9.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index e88046e85..ce2c66662 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [metadata] name = python-keystoneclient -summary = Client Library for OpenStack Identity +summary = Modified Client Library for OpenStack Identity to work with FIWARE modified Keystone description-file = README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = http://www.openstack.org/ +author = UPM-GING +author-email = garcianavalon@gmail.com +home-page = http://www.github.com/ging classifier = Environment :: OpenStack Intended Audience :: Information Technology